Skip to content

Commit 3a49a6e

Browse files
Adds ability to use JSON pointer for the user_claim (#204)
1 parent 7e0f211 commit 3a49a6e

File tree

4 files changed

+217
-61
lines changed

4 files changed

+217
-61
lines changed

path_login.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,16 @@ func (b *jwtAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Reques
182182
// createIdentity creates an alias and set of groups aliases based on the role
183183
// definition and received claims.
184184
func (b *jwtAuthBackend) createIdentity(ctx context.Context, allClaims map[string]interface{}, role *jwtRole, tokenSource oauth2.TokenSource) (*logical.Alias, []*logical.Alias, error) {
185-
userClaimRaw, ok := allClaims[role.UserClaim]
186-
if !ok {
185+
var userClaimRaw interface{}
186+
if role.UserClaimJSONPointer {
187+
userClaimRaw = getClaim(b.Logger(), allClaims, role.UserClaim)
188+
} else {
189+
userClaimRaw = allClaims[role.UserClaim]
190+
}
191+
if userClaimRaw == nil {
187192
return nil, nil, fmt.Errorf("claim %q not found in token", role.UserClaim)
188193
}
194+
189195
userName, ok := userClaimRaw.(string)
190196
if !ok {
191197
return nil, nil, fmt.Errorf("claim %q could not be converted to string", role.UserClaim)

path_oidc_test.go

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,140 @@ func TestOIDC_AuthURL_max_age(t *testing.T) {
469469
}
470470
}
471471

472+
// TestOIDC_UserClaim_JSON_Pointer tests the ability to use JSON
473+
// pointer syntax for the user_claim of roles. For claims used
474+
// in assertions, see the sampleClaims function.
475+
func TestOIDC_UserClaim_JSON_Pointer(t *testing.T) {
476+
b, storage, s := getBackendAndServer(t, false)
477+
defer s.server.Close()
478+
479+
type args struct {
480+
userClaim string
481+
userClaimJSONPointer bool
482+
}
483+
tests := []struct {
484+
name string
485+
args args
486+
wantAliasName string
487+
wantErr bool
488+
}{
489+
{
490+
name: "user_claim without JSON pointer",
491+
args: args{
492+
userClaim: "email",
493+
userClaimJSONPointer: false,
494+
},
495+
wantAliasName: "bob@example.com",
496+
},
497+
{
498+
name: "user_claim without JSON pointer using claim that could be JSON pointer",
499+
args: args{
500+
userClaim: "/nested/username",
501+
userClaimJSONPointer: false,
502+
},
503+
wantAliasName: "non_nested_username",
504+
},
505+
{
506+
name: "user_claim without JSON pointer not found",
507+
args: args{
508+
userClaim: "other",
509+
userClaimJSONPointer: false,
510+
},
511+
wantErr: true,
512+
},
513+
{
514+
name: "user_claim with JSON pointer nested",
515+
args: args{
516+
userClaim: "/nested/username",
517+
userClaimJSONPointer: true,
518+
},
519+
wantAliasName: "nested_username",
520+
},
521+
{
522+
name: "user_claim with JSON pointer not nested",
523+
args: args{
524+
userClaim: "/email",
525+
userClaimJSONPointer: true,
526+
},
527+
wantAliasName: "bob@example.com",
528+
},
529+
{
530+
name: "user_claim with JSON pointer not found",
531+
args: args{
532+
userClaim: "/nested/username/email",
533+
userClaimJSONPointer: true,
534+
},
535+
wantErr: true,
536+
},
537+
}
538+
for _, tt := range tests {
539+
t.Run(tt.name, func(t *testing.T) {
540+
// Update the role's user_claim config
541+
data := map[string]interface{}{
542+
"user_claim": tt.args.userClaim,
543+
"user_claim_json_pointer": tt.args.userClaimJSONPointer,
544+
}
545+
req := &logical.Request{
546+
Operation: logical.CreateOperation,
547+
Path: "role/test",
548+
Storage: storage,
549+
Data: data,
550+
}
551+
resp, err := b.HandleRequest(context.Background(), req)
552+
require.NoError(t, err)
553+
require.False(t, resp.IsError())
554+
555+
// Generate an auth URL
556+
data = map[string]interface{}{
557+
"role": "test",
558+
"redirect_uri": "https://example.com",
559+
}
560+
req = &logical.Request{
561+
Operation: logical.UpdateOperation,
562+
Path: "oidc/auth_url",
563+
Storage: storage,
564+
Data: data,
565+
}
566+
resp, err = b.HandleRequest(context.Background(), req)
567+
require.NoError(t, err)
568+
require.False(t, resp.IsError())
569+
570+
// Parse the state and nonce from the auth URL
571+
authURL := resp.Data["auth_url"].(string)
572+
state := getQueryParam(t, authURL, "state")
573+
nonce := getQueryParam(t, authURL, "nonce")
574+
575+
// Set test provider custom claims, expected auth code, expected code challenge
576+
s.codeChallenge = getQueryParam(t, authURL, "code_challenge")
577+
s.customClaims = sampleClaims(nonce)
578+
s.code = "abc"
579+
580+
// Complete authentication by invoking the callback handler
581+
req = &logical.Request{
582+
Operation: logical.ReadOperation,
583+
Path: "oidc/callback",
584+
Storage: storage,
585+
Data: map[string]interface{}{
586+
"state": state,
587+
"code": "abc",
588+
},
589+
}
590+
591+
// Assert that we get the expected alias name
592+
resp, err = b.HandleRequest(context.Background(), req)
593+
if tt.wantErr {
594+
require.True(t, resp.IsError())
595+
return
596+
}
597+
require.NoError(t, err)
598+
require.False(t, resp.IsError())
599+
require.NotNil(t, resp.Auth)
600+
require.NotNil(t, resp.Auth.Alias)
601+
require.Equal(t, tt.wantAliasName, resp.Auth.Alias.Name)
602+
})
603+
}
604+
}
605+
472606
// TestOIDC_ResponseTypeIDToken tests authentication using an implicit flow
473607
// by setting oidc_response_types=id_token and oidc_response_mode=form_post.
474608
// This means that there is no exchange of an authorization code for tokens.
@@ -1474,14 +1608,16 @@ func getBackendAndServer(t *testing.T, boundCIDRs bool) (logical.Backend, logica
14741608

14751609
func sampleClaims(nonce string) map[string]interface{} {
14761610
return map[string]interface{}{
1477-
"nonce": nonce,
1478-
"email": "bob@example.com",
1479-
"COLOR": "green",
1480-
"sk": "42",
1611+
"nonce": nonce,
1612+
"email": "bob@example.com",
1613+
"/nested/username": "non_nested_username",
1614+
"COLOR": "green",
1615+
"sk": "42",
14811616
"nested": map[string]interface{}{
14821617
"Size": "medium",
14831618
"Groups": []string{"a", "b"},
14841619
"secret_code": "bar",
1620+
"username": "nested_username",
14851621
},
14861622
"password": "foo",
14871623
}

path_role.go

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`,
125125
Type: framework.TypeString,
126126
Description: `The claim to use for the Identity entity alias name`,
127127
},
128+
"user_claim_json_pointer": {
129+
Type: framework.TypeBool,
130+
Description: `If true, the user_claim value will use JSON pointer syntax
131+
for referencing claims.`,
132+
},
128133
"groups_claim": {
129134
Type: framework.TypeString,
130135
Description: `The claim to use for the Identity group alias names`,
@@ -196,17 +201,18 @@ type jwtRole struct {
196201
ClockSkewLeeway time.Duration `json:"clock_skew_leeway"`
197202

198203
// Role binding properties
199-
BoundAudiences []string `json:"bound_audiences"`
200-
BoundSubject string `json:"bound_subject"`
201-
BoundClaimsType string `json:"bound_claims_type"`
202-
BoundClaims map[string]interface{} `json:"bound_claims"`
203-
ClaimMappings map[string]string `json:"claim_mappings"`
204-
UserClaim string `json:"user_claim"`
205-
GroupsClaim string `json:"groups_claim"`
206-
OIDCScopes []string `json:"oidc_scopes"`
207-
AllowedRedirectURIs []string `json:"allowed_redirect_uris"`
208-
VerboseOIDCLogging bool `json:"verbose_oidc_logging"`
209-
MaxAge time.Duration `json:"max_age"`
204+
BoundAudiences []string `json:"bound_audiences"`
205+
BoundSubject string `json:"bound_subject"`
206+
BoundClaimsType string `json:"bound_claims_type"`
207+
BoundClaims map[string]interface{} `json:"bound_claims"`
208+
ClaimMappings map[string]string `json:"claim_mappings"`
209+
UserClaim string `json:"user_claim"`
210+
GroupsClaim string `json:"groups_claim"`
211+
OIDCScopes []string `json:"oidc_scopes"`
212+
AllowedRedirectURIs []string `json:"allowed_redirect_uris"`
213+
VerboseOIDCLogging bool `json:"verbose_oidc_logging"`
214+
MaxAge time.Duration `json:"max_age"`
215+
UserClaimJSONPointer bool `json:"user_claim_json_pointer"`
210216

211217
// Deprecated by TokenParams
212218
Policies []string `json:"policies"`
@@ -299,21 +305,22 @@ func (b *jwtAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request,
299305

300306
// Create a map of data to be returned
301307
d := map[string]interface{}{
302-
"role_type": role.RoleType,
303-
"expiration_leeway": int64(role.ExpirationLeeway.Seconds()),
304-
"not_before_leeway": int64(role.NotBeforeLeeway.Seconds()),
305-
"clock_skew_leeway": int64(role.ClockSkewLeeway.Seconds()),
306-
"bound_audiences": role.BoundAudiences,
307-
"bound_subject": role.BoundSubject,
308-
"bound_claims_type": role.BoundClaimsType,
309-
"bound_claims": role.BoundClaims,
310-
"claim_mappings": role.ClaimMappings,
311-
"user_claim": role.UserClaim,
312-
"groups_claim": role.GroupsClaim,
313-
"allowed_redirect_uris": role.AllowedRedirectURIs,
314-
"oidc_scopes": role.OIDCScopes,
315-
"verbose_oidc_logging": role.VerboseOIDCLogging,
316-
"max_age": int64(role.MaxAge.Seconds()),
308+
"role_type": role.RoleType,
309+
"expiration_leeway": int64(role.ExpirationLeeway.Seconds()),
310+
"not_before_leeway": int64(role.NotBeforeLeeway.Seconds()),
311+
"clock_skew_leeway": int64(role.ClockSkewLeeway.Seconds()),
312+
"bound_audiences": role.BoundAudiences,
313+
"bound_subject": role.BoundSubject,
314+
"bound_claims_type": role.BoundClaimsType,
315+
"bound_claims": role.BoundClaims,
316+
"claim_mappings": role.ClaimMappings,
317+
"user_claim": role.UserClaim,
318+
"user_claim_json_pointer": role.UserClaimJSONPointer,
319+
"groups_claim": role.GroupsClaim,
320+
"allowed_redirect_uris": role.AllowedRedirectURIs,
321+
"oidc_scopes": role.OIDCScopes,
322+
"verbose_oidc_logging": role.VerboseOIDCLogging,
323+
"max_age": int64(role.MaxAge.Seconds()),
317324
}
318325

319326
role.PopulateTokenData(d)
@@ -506,6 +513,10 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
506513
return logical.ErrorResponse("a user claim must be defined on the role"), nil
507514
}
508515

516+
if userClaimJSONPointer, ok := data.GetOk("user_claim_json_pointer"); ok {
517+
role.UserClaimJSONPointer = userClaimJSONPointer.(bool)
518+
}
519+
509520
if groupsClaim, ok := data.GetOk("groups_claim"); ok {
510521
role.GroupsClaim = groupsClaim.(string)
511522
}

path_role_test.go

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@ func TestPath_Create(t *testing.T) {
4242
b, storage := getBackend(t)
4343

4444
data := map[string]interface{}{
45-
"role_type": "jwt",
46-
"bound_subject": "testsub",
47-
"bound_audiences": "vault",
48-
"user_claim": "user",
49-
"groups_claim": "groups",
50-
"bound_cidrs": "127.0.0.1/8",
51-
"policies": "test",
52-
"period": "3s",
53-
"ttl": "1s",
54-
"num_uses": 12,
55-
"max_ttl": "5s",
56-
"max_age": "60s",
45+
"role_type": "jwt",
46+
"bound_subject": "testsub",
47+
"bound_audiences": "vault",
48+
"user_claim": "user",
49+
"user_claim_json_pointer": true,
50+
"groups_claim": "groups",
51+
"bound_cidrs": "127.0.0.1/8",
52+
"policies": "test",
53+
"period": "3s",
54+
"ttl": "1s",
55+
"num_uses": 12,
56+
"max_ttl": "5s",
57+
"max_age": "60s",
5758
}
5859

5960
expectedSockAddr, err := sockaddr.NewSockAddr("127.0.0.1/8")
@@ -70,23 +71,24 @@ func TestPath_Create(t *testing.T) {
7071
TokenNumUses: 12,
7172
TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
7273
},
73-
RoleType: "jwt",
74-
Policies: []string{"test"},
75-
Period: 3 * time.Second,
76-
BoundSubject: "testsub",
77-
BoundAudiences: []string{"vault"},
78-
BoundClaimsType: "string",
79-
UserClaim: "user",
80-
GroupsClaim: "groups",
81-
TTL: 1 * time.Second,
82-
MaxTTL: 5 * time.Second,
83-
ExpirationLeeway: 0,
84-
NotBeforeLeeway: 0,
85-
ClockSkewLeeway: 0,
86-
NumUses: 12,
87-
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
88-
AllowedRedirectURIs: []string(nil),
89-
MaxAge: 60 * time.Second,
74+
RoleType: "jwt",
75+
Policies: []string{"test"},
76+
Period: 3 * time.Second,
77+
BoundSubject: "testsub",
78+
BoundAudiences: []string{"vault"},
79+
BoundClaimsType: "string",
80+
UserClaim: "user",
81+
UserClaimJSONPointer: true,
82+
GroupsClaim: "groups",
83+
TTL: 1 * time.Second,
84+
MaxTTL: 5 * time.Second,
85+
ExpirationLeeway: 0,
86+
NotBeforeLeeway: 0,
87+
ClockSkewLeeway: 0,
88+
NumUses: 12,
89+
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
90+
AllowedRedirectURIs: []string(nil),
91+
MaxAge: 60 * time.Second,
9092
}
9193

9294
req := &logical.Request{
@@ -767,6 +769,7 @@ func TestPath_Read(t *testing.T) {
767769
"allowed_redirect_uris": []string{"http://127.0.0.1"},
768770
"oidc_scopes": []string{"email", "profile"},
769771
"user_claim": "user",
772+
"user_claim_json_pointer": false,
770773
"groups_claim": "groups",
771774
"token_policies": []string{"test"},
772775
"policies": []string{"test"},

0 commit comments

Comments
 (0)