Skip to content

Commit

Permalink
Adds ability to use JSON pointer for the user_claim (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
austingebauer committed May 16, 2022
1 parent 7e0f211 commit 3a49a6e
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 61 deletions.
10 changes: 8 additions & 2 deletions path_login.go
Expand Up @@ -182,10 +182,16 @@ func (b *jwtAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Reques
// createIdentity creates an alias and set of groups aliases based on the role
// definition and received claims.
func (b *jwtAuthBackend) createIdentity(ctx context.Context, allClaims map[string]interface{}, role *jwtRole, tokenSource oauth2.TokenSource) (*logical.Alias, []*logical.Alias, error) {
userClaimRaw, ok := allClaims[role.UserClaim]
if !ok {
var userClaimRaw interface{}
if role.UserClaimJSONPointer {
userClaimRaw = getClaim(b.Logger(), allClaims, role.UserClaim)
} else {
userClaimRaw = allClaims[role.UserClaim]
}
if userClaimRaw == nil {
return nil, nil, fmt.Errorf("claim %q not found in token", role.UserClaim)
}

userName, ok := userClaimRaw.(string)
if !ok {
return nil, nil, fmt.Errorf("claim %q could not be converted to string", role.UserClaim)
Expand Down
144 changes: 140 additions & 4 deletions path_oidc_test.go
Expand Up @@ -469,6 +469,140 @@ func TestOIDC_AuthURL_max_age(t *testing.T) {
}
}

// TestOIDC_UserClaim_JSON_Pointer tests the ability to use JSON
// pointer syntax for the user_claim of roles. For claims used
// in assertions, see the sampleClaims function.
func TestOIDC_UserClaim_JSON_Pointer(t *testing.T) {
b, storage, s := getBackendAndServer(t, false)
defer s.server.Close()

type args struct {
userClaim string
userClaimJSONPointer bool
}
tests := []struct {
name string
args args
wantAliasName string
wantErr bool
}{
{
name: "user_claim without JSON pointer",
args: args{
userClaim: "email",
userClaimJSONPointer: false,
},
wantAliasName: "bob@example.com",
},
{
name: "user_claim without JSON pointer using claim that could be JSON pointer",
args: args{
userClaim: "/nested/username",
userClaimJSONPointer: false,
},
wantAliasName: "non_nested_username",
},
{
name: "user_claim without JSON pointer not found",
args: args{
userClaim: "other",
userClaimJSONPointer: false,
},
wantErr: true,
},
{
name: "user_claim with JSON pointer nested",
args: args{
userClaim: "/nested/username",
userClaimJSONPointer: true,
},
wantAliasName: "nested_username",
},
{
name: "user_claim with JSON pointer not nested",
args: args{
userClaim: "/email",
userClaimJSONPointer: true,
},
wantAliasName: "bob@example.com",
},
{
name: "user_claim with JSON pointer not found",
args: args{
userClaim: "/nested/username/email",
userClaimJSONPointer: true,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Update the role's user_claim config
data := map[string]interface{}{
"user_claim": tt.args.userClaim,
"user_claim_json_pointer": tt.args.userClaimJSONPointer,
}
req := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/test",
Storage: storage,
Data: data,
}
resp, err := b.HandleRequest(context.Background(), req)
require.NoError(t, err)
require.False(t, resp.IsError())

// Generate an auth URL
data = map[string]interface{}{
"role": "test",
"redirect_uri": "https://example.com",
}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "oidc/auth_url",
Storage: storage,
Data: data,
}
resp, err = b.HandleRequest(context.Background(), req)
require.NoError(t, err)
require.False(t, resp.IsError())

// Parse the state and nonce from the auth URL
authURL := resp.Data["auth_url"].(string)
state := getQueryParam(t, authURL, "state")
nonce := getQueryParam(t, authURL, "nonce")

// Set test provider custom claims, expected auth code, expected code challenge
s.codeChallenge = getQueryParam(t, authURL, "code_challenge")
s.customClaims = sampleClaims(nonce)
s.code = "abc"

// Complete authentication by invoking the callback handler
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "oidc/callback",
Storage: storage,
Data: map[string]interface{}{
"state": state,
"code": "abc",
},
}

// Assert that we get the expected alias name
resp, err = b.HandleRequest(context.Background(), req)
if tt.wantErr {
require.True(t, resp.IsError())
return
}
require.NoError(t, err)
require.False(t, resp.IsError())
require.NotNil(t, resp.Auth)
require.NotNil(t, resp.Auth.Alias)
require.Equal(t, tt.wantAliasName, resp.Auth.Alias.Name)
})
}
}

// TestOIDC_ResponseTypeIDToken tests authentication using an implicit flow
// by setting oidc_response_types=id_token and oidc_response_mode=form_post.
// This means that there is no exchange of an authorization code for tokens.
Expand Down Expand Up @@ -1474,14 +1608,16 @@ func getBackendAndServer(t *testing.T, boundCIDRs bool) (logical.Backend, logica

func sampleClaims(nonce string) map[string]interface{} {
return map[string]interface{}{
"nonce": nonce,
"email": "bob@example.com",
"COLOR": "green",
"sk": "42",
"nonce": nonce,
"email": "bob@example.com",
"/nested/username": "non_nested_username",
"COLOR": "green",
"sk": "42",
"nested": map[string]interface{}{
"Size": "medium",
"Groups": []string{"a", "b"},
"secret_code": "bar",
"username": "nested_username",
},
"password": "foo",
}
Expand Down
63 changes: 37 additions & 26 deletions path_role.go
Expand Up @@ -125,6 +125,11 @@ Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`,
Type: framework.TypeString,
Description: `The claim to use for the Identity entity alias name`,
},
"user_claim_json_pointer": {
Type: framework.TypeBool,
Description: `If true, the user_claim value will use JSON pointer syntax
for referencing claims.`,
},
"groups_claim": {
Type: framework.TypeString,
Description: `The claim to use for the Identity group alias names`,
Expand Down Expand Up @@ -196,17 +201,18 @@ type jwtRole struct {
ClockSkewLeeway time.Duration `json:"clock_skew_leeway"`

// Role binding properties
BoundAudiences []string `json:"bound_audiences"`
BoundSubject string `json:"bound_subject"`
BoundClaimsType string `json:"bound_claims_type"`
BoundClaims map[string]interface{} `json:"bound_claims"`
ClaimMappings map[string]string `json:"claim_mappings"`
UserClaim string `json:"user_claim"`
GroupsClaim string `json:"groups_claim"`
OIDCScopes []string `json:"oidc_scopes"`
AllowedRedirectURIs []string `json:"allowed_redirect_uris"`
VerboseOIDCLogging bool `json:"verbose_oidc_logging"`
MaxAge time.Duration `json:"max_age"`
BoundAudiences []string `json:"bound_audiences"`
BoundSubject string `json:"bound_subject"`
BoundClaimsType string `json:"bound_claims_type"`
BoundClaims map[string]interface{} `json:"bound_claims"`
ClaimMappings map[string]string `json:"claim_mappings"`
UserClaim string `json:"user_claim"`
GroupsClaim string `json:"groups_claim"`
OIDCScopes []string `json:"oidc_scopes"`
AllowedRedirectURIs []string `json:"allowed_redirect_uris"`
VerboseOIDCLogging bool `json:"verbose_oidc_logging"`
MaxAge time.Duration `json:"max_age"`
UserClaimJSONPointer bool `json:"user_claim_json_pointer"`

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

// Create a map of data to be returned
d := map[string]interface{}{
"role_type": role.RoleType,
"expiration_leeway": int64(role.ExpirationLeeway.Seconds()),
"not_before_leeway": int64(role.NotBeforeLeeway.Seconds()),
"clock_skew_leeway": int64(role.ClockSkewLeeway.Seconds()),
"bound_audiences": role.BoundAudiences,
"bound_subject": role.BoundSubject,
"bound_claims_type": role.BoundClaimsType,
"bound_claims": role.BoundClaims,
"claim_mappings": role.ClaimMappings,
"user_claim": role.UserClaim,
"groups_claim": role.GroupsClaim,
"allowed_redirect_uris": role.AllowedRedirectURIs,
"oidc_scopes": role.OIDCScopes,
"verbose_oidc_logging": role.VerboseOIDCLogging,
"max_age": int64(role.MaxAge.Seconds()),
"role_type": role.RoleType,
"expiration_leeway": int64(role.ExpirationLeeway.Seconds()),
"not_before_leeway": int64(role.NotBeforeLeeway.Seconds()),
"clock_skew_leeway": int64(role.ClockSkewLeeway.Seconds()),
"bound_audiences": role.BoundAudiences,
"bound_subject": role.BoundSubject,
"bound_claims_type": role.BoundClaimsType,
"bound_claims": role.BoundClaims,
"claim_mappings": role.ClaimMappings,
"user_claim": role.UserClaim,
"user_claim_json_pointer": role.UserClaimJSONPointer,
"groups_claim": role.GroupsClaim,
"allowed_redirect_uris": role.AllowedRedirectURIs,
"oidc_scopes": role.OIDCScopes,
"verbose_oidc_logging": role.VerboseOIDCLogging,
"max_age": int64(role.MaxAge.Seconds()),
}

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

if userClaimJSONPointer, ok := data.GetOk("user_claim_json_pointer"); ok {
role.UserClaimJSONPointer = userClaimJSONPointer.(bool)
}

if groupsClaim, ok := data.GetOk("groups_claim"); ok {
role.GroupsClaim = groupsClaim.(string)
}
Expand Down
61 changes: 32 additions & 29 deletions path_role_test.go
Expand Up @@ -42,18 +42,19 @@ func TestPath_Create(t *testing.T) {
b, storage := getBackend(t)

data := map[string]interface{}{
"role_type": "jwt",
"bound_subject": "testsub",
"bound_audiences": "vault",
"user_claim": "user",
"groups_claim": "groups",
"bound_cidrs": "127.0.0.1/8",
"policies": "test",
"period": "3s",
"ttl": "1s",
"num_uses": 12,
"max_ttl": "5s",
"max_age": "60s",
"role_type": "jwt",
"bound_subject": "testsub",
"bound_audiences": "vault",
"user_claim": "user",
"user_claim_json_pointer": true,
"groups_claim": "groups",
"bound_cidrs": "127.0.0.1/8",
"policies": "test",
"period": "3s",
"ttl": "1s",
"num_uses": 12,
"max_ttl": "5s",
"max_age": "60s",
}

expectedSockAddr, err := sockaddr.NewSockAddr("127.0.0.1/8")
Expand All @@ -70,23 +71,24 @@ func TestPath_Create(t *testing.T) {
TokenNumUses: 12,
TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
},
RoleType: "jwt",
Policies: []string{"test"},
Period: 3 * time.Second,
BoundSubject: "testsub",
BoundAudiences: []string{"vault"},
BoundClaimsType: "string",
UserClaim: "user",
GroupsClaim: "groups",
TTL: 1 * time.Second,
MaxTTL: 5 * time.Second,
ExpirationLeeway: 0,
NotBeforeLeeway: 0,
ClockSkewLeeway: 0,
NumUses: 12,
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
AllowedRedirectURIs: []string(nil),
MaxAge: 60 * time.Second,
RoleType: "jwt",
Policies: []string{"test"},
Period: 3 * time.Second,
BoundSubject: "testsub",
BoundAudiences: []string{"vault"},
BoundClaimsType: "string",
UserClaim: "user",
UserClaimJSONPointer: true,
GroupsClaim: "groups",
TTL: 1 * time.Second,
MaxTTL: 5 * time.Second,
ExpirationLeeway: 0,
NotBeforeLeeway: 0,
ClockSkewLeeway: 0,
NumUses: 12,
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: expectedSockAddr}},
AllowedRedirectURIs: []string(nil),
MaxAge: 60 * time.Second,
}

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

0 comments on commit 3a49a6e

Please sign in to comment.