Merge pull request #1634 from concourse/pr/oidc-username-key-sync
Support username, email and groups claim in OIDC connector
This commit is contained in:
		| @@ -8,8 +8,6 @@ Prominent examples of OpenID Connect providers include Google Accounts, Salesfor | ||||
|  | ||||
| ## Caveats | ||||
|  | ||||
| This connector does not support the "groups" claim. Progress for this is tracked in [issue #1065][issue-1065]. | ||||
|  | ||||
| When using refresh tokens, changes to the upstream claims aren't propagated to the id_token returned by dex. If a user's email changes, the "email" claim returned by dex won't change unless the user logs in again. Progress for this is tracked in [issue #863][issue-863]. | ||||
|  | ||||
| ## Configuration | ||||
| @@ -75,11 +73,10 @@ connectors: | ||||
|     # getUserInfo: true | ||||
|  | ||||
|     # The set claim is used as user id. | ||||
|     # Default: sub | ||||
|     # Claims list at https://openid.net/specs/openid-connect-core-1_0.html#Claims | ||||
|     # | ||||
|     # Default: sub | ||||
|     # userIDKey: nickname | ||||
|      | ||||
|  | ||||
|     # The set claim is used as user name. | ||||
|     # Default: name | ||||
|     # userNameKey: nickname | ||||
| @@ -88,9 +85,25 @@ connectors: | ||||
|     # However this is not supported by all OIDC providers, some of them support different | ||||
|     # value for prompt, like "prompt=login" or "prompt=none" | ||||
|     # promptType: consent | ||||
|  | ||||
|     # Some providers return non-standard claims (eg. mail). | ||||
|     # Use claimMapping to map those claims to standard claims: | ||||
|     # https://openid.net/specs/openid-connect-core-1_0.html#Claims | ||||
|     # claimMapping can only map a non-standard claim to a standard one if it's not returned in the id_token. | ||||
|     claimMapping: | ||||
|       # The set claim is used as preferred username. | ||||
|       # Default: preferred_username | ||||
|       # preferred_username: other_user_name | ||||
|  | ||||
|       # The set claim is used as email. | ||||
|       # Default: email | ||||
|       # email: mail | ||||
|  | ||||
|       # The set claim is used as groups. | ||||
|       # Default: groups | ||||
|       # groups: "cognito:groups" | ||||
| ``` | ||||
|  | ||||
| [oidc-doc]: openid-connect.md | ||||
| [issue-863]: https://github.com/dexidp/dex/issues/863 | ||||
| [issue-1065]: https://github.com/dexidp/dex/issues/1065 | ||||
| [azure-ad-v1]: https://github.com/coreos/go-oidc/issues/133 | ||||
|   | ||||
| @@ -69,7 +69,7 @@ Dex implements the following connectors: | ||||
| | [GitHub](Documentation/connectors/github.md) | yes | yes | yes | stable | | | ||||
| | [SAML 2.0](Documentation/connectors/saml.md) | no | yes | no | stable | | ||||
| | [GitLab](Documentation/connectors/gitlab.md) | yes | yes | yes | beta | | | ||||
| | [OpenID Connect](Documentation/connectors/oidc.md) | yes | no ([#1065][issue-1065]) | no | beta | Includes Salesforce, Azure, etc. | | ||||
| | [OpenID Connect](Documentation/connectors/oidc.md) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. | | ||||
| | [Google](Documentation/connectors/google.md) | yes | yes | yes | alpha | | | ||||
| | [LinkedIn](Documentation/connectors/linkedin.md) | yes | no | no | beta | | | ||||
| | [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | | | ||||
|   | ||||
| @@ -49,14 +49,23 @@ type Config struct { | ||||
| 	// id tokens | ||||
| 	GetUserInfo bool `json:"getUserInfo"` | ||||
|  | ||||
| 	// Configurable key which contains the user id claim | ||||
| 	UserIDKey string `json:"userIDKey"` | ||||
|  | ||||
| 	// Configurable key which contains the user name claim | ||||
| 	UserNameKey string `json:"userNameKey"` | ||||
|  | ||||
| 	// PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent) | ||||
| 	PromptType string `json:"promptType"` | ||||
|  | ||||
| 	ClaimMapping struct { | ||||
| 		// Configurable key which contains the preferred username claims | ||||
| 		PreferredUsernameKey string `json:"preferred_username"` // defaults to "preferred_username" | ||||
|  | ||||
| 		// Configurable key which contains the email claims | ||||
| 		EmailKey string `json:"email"` // defaults to "email" | ||||
|  | ||||
| 		// Configurable key which contains the groups claims | ||||
| 		GroupsKey string `json:"groups"` // defaults to "groups" | ||||
| 	} `json:"claimMapping"` | ||||
| } | ||||
|  | ||||
| // Domains that don't support basic auth. golang.org/x/oauth2 has an internal | ||||
| @@ -141,9 +150,12 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e | ||||
| 		insecureSkipEmailVerified: c.InsecureSkipEmailVerified, | ||||
| 		insecureEnableGroups:      c.InsecureEnableGroups, | ||||
| 		getUserInfo:               c.GetUserInfo, | ||||
| 		promptType:                c.PromptType, | ||||
| 		userIDKey:                 c.UserIDKey, | ||||
| 		userNameKey:               c.UserNameKey, | ||||
| 		promptType:                c.PromptType, | ||||
| 		preferredUsernameKey:      c.ClaimMapping.PreferredUsernameKey, | ||||
| 		emailKey:                  c.ClaimMapping.EmailKey, | ||||
| 		groupsKey:                 c.ClaimMapping.GroupsKey, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| @@ -163,9 +175,12 @@ type oidcConnector struct { | ||||
| 	insecureSkipEmailVerified bool | ||||
| 	insecureEnableGroups      bool | ||||
| 	getUserInfo               bool | ||||
| 	promptType                string | ||||
| 	userIDKey                 string | ||||
| 	userNameKey               string | ||||
| 	promptType                string | ||||
| 	preferredUsernameKey      string | ||||
| 	emailKey                  string | ||||
| 	groupsKey                 string | ||||
| } | ||||
|  | ||||
| func (c *oidcConnector) Close() error { | ||||
| @@ -273,6 +288,11 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I | ||||
| 		return identity, fmt.Errorf("missing \"%s\" claim", userNameKey) | ||||
| 	} | ||||
|  | ||||
| 	preferredUsername, found := claims["preferred_username"].(string) | ||||
| 	if !found { | ||||
| 		preferredUsername, _ = claims[c.preferredUsernameKey].(string) | ||||
| 	} | ||||
|  | ||||
| 	hasEmailScope := false | ||||
| 	for _, s := range c.oauth2Config.Scopes { | ||||
| 		if s == "email" { | ||||
| @@ -281,9 +301,16 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	email, found := claims["email"].(string) | ||||
| 	var email string | ||||
| 	emailKey := "email" | ||||
| 	email, found = claims[emailKey].(string) | ||||
| 	if !found && c.emailKey != "" { | ||||
| 		emailKey = c.emailKey | ||||
| 		email, found = claims[emailKey].(string) | ||||
| 	} | ||||
|  | ||||
| 	if !found && hasEmailScope { | ||||
| 		return identity, errors.New("missing \"email\" claim") | ||||
| 		return identity, fmt.Errorf("missing email claim, not found \"%s\" key", emailKey) | ||||
| 	} | ||||
|  | ||||
| 	emailVerified, found := claims["email_verified"].(bool) | ||||
| @@ -294,8 +321,28 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I | ||||
| 			return identity, errors.New("missing \"email_verified\" claim") | ||||
| 		} | ||||
| 	} | ||||
| 	hostedDomain, _ := claims["hd"].(string) | ||||
|  | ||||
| 	var groups []string | ||||
| 	if c.insecureEnableGroups { | ||||
| 		groupsKey := "groups" | ||||
| 		vs, found := claims[groupsKey].([]interface{}) | ||||
| 		if !found { | ||||
| 			groupsKey = c.groupsKey | ||||
| 			vs, found = claims[groupsKey].([]interface{}) | ||||
| 		} | ||||
|  | ||||
| 		if found { | ||||
| 			for _, v := range vs { | ||||
| 				if s, ok := v.(string); ok { | ||||
| 					groups = append(groups, s) | ||||
| 				} else { | ||||
| 					return identity, fmt.Errorf("malformed \"%v\" claim", groupsKey) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	hostedDomain, _ := claims["hd"].(string) | ||||
| 	if len(c.hostedDomains) > 0 { | ||||
| 		found := false | ||||
| 		for _, domain := range c.hostedDomains { | ||||
| @@ -320,11 +367,13 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I | ||||
| 	} | ||||
|  | ||||
| 	identity = connector.Identity{ | ||||
| 		UserID:        idToken.Subject, | ||||
| 		Username:      name, | ||||
| 		Email:         email, | ||||
| 		EmailVerified: emailVerified, | ||||
| 		ConnectorData: connData, | ||||
| 		UserID:            idToken.Subject, | ||||
| 		Username:          name, | ||||
| 		PreferredUsername: preferredUsername, | ||||
| 		Email:             email, | ||||
| 		EmailVerified:     emailVerified, | ||||
| 		Groups:            groups, | ||||
| 		ConnectorData:     connData, | ||||
| 	} | ||||
|  | ||||
| 	if c.userIDKey != "" { | ||||
| @@ -335,18 +384,5 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I | ||||
| 		identity.UserID = userID | ||||
| 	} | ||||
|  | ||||
| 	if c.insecureEnableGroups { | ||||
| 		vs, ok := claims["groups"].([]interface{}) | ||||
| 		if ok { | ||||
| 			for _, v := range vs { | ||||
| 				if s, ok := v.(string); ok { | ||||
| 					identity.Groups = append(identity.Groups, s) | ||||
| 				} else { | ||||
| 					return identity, errors.New("malformed \"groups\" claim") | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return identity, nil | ||||
| } | ||||
|   | ||||
| @@ -49,10 +49,15 @@ func TestHandleCallback(t *testing.T) { | ||||
| 		name                      string | ||||
| 		userIDKey                 string | ||||
| 		userNameKey               string | ||||
| 		preferredUsernameKey      string | ||||
| 		emailKey                  string | ||||
| 		groupsKey                 string | ||||
| 		insecureSkipEmailVerified bool | ||||
| 		scopes                    []string | ||||
| 		expectUserID              string | ||||
| 		expectUserName            string | ||||
| 		expectGroups              []string | ||||
| 		expectPreferredUsername   string | ||||
| 		expectedEmailField        string | ||||
| 		token                     map[string]interface{} | ||||
| 	}{ | ||||
| @@ -62,14 +67,31 @@ func TestHandleCallback(t *testing.T) { | ||||
| 			userNameKey:        "", // not configured | ||||
| 			expectUserID:       "subvalue", | ||||
| 			expectUserName:     "namevalue", | ||||
| 			expectGroups:       []string{"group1", "group2"}, | ||||
| 			expectedEmailField: "emailvalue", | ||||
| 			token: map[string]interface{}{ | ||||
| 				"sub":            "subvalue", | ||||
| 				"name":           "namevalue", | ||||
| 				"groups":         []string{"group1", "group2"}, | ||||
| 				"email":          "emailvalue", | ||||
| 				"email_verified": true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:               "customEmailClaim", | ||||
| 			userIDKey:          "", // not configured | ||||
| 			userNameKey:        "", // not configured | ||||
| 			emailKey:           "mail", | ||||
| 			expectUserID:       "subvalue", | ||||
| 			expectUserName:     "namevalue", | ||||
| 			expectedEmailField: "emailvalue", | ||||
| 			token: map[string]interface{}{ | ||||
| 				"sub":            "subvalue", | ||||
| 				"name":           "namevalue", | ||||
| 				"mail":           "emailvalue", | ||||
| 				"email_verified": true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                      "email_verified not in claims, configured to be skipped", | ||||
| 			insecureSkipEmailVerified: true, | ||||
| @@ -108,6 +130,48 @@ func TestHandleCallback(t *testing.T) { | ||||
| 				"email_verified": true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "withPreferredUsernameKey", | ||||
| 			preferredUsernameKey:    "username_key", | ||||
| 			expectUserID:            "subvalue", | ||||
| 			expectUserName:          "namevalue", | ||||
| 			expectPreferredUsername: "username_value", | ||||
| 			expectedEmailField:      "emailvalue", | ||||
| 			token: map[string]interface{}{ | ||||
| 				"sub":            "subvalue", | ||||
| 				"name":           "namevalue", | ||||
| 				"username_key":   "username_value", | ||||
| 				"email":          "emailvalue", | ||||
| 				"email_verified": true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "withoutPreferredUsernameKeyAndBackendReturns", | ||||
| 			expectUserID:            "subvalue", | ||||
| 			expectUserName:          "namevalue", | ||||
| 			expectPreferredUsername: "preferredusernamevalue", | ||||
| 			expectedEmailField:      "emailvalue", | ||||
| 			token: map[string]interface{}{ | ||||
| 				"sub":                "subvalue", | ||||
| 				"name":               "namevalue", | ||||
| 				"preferred_username": "preferredusernamevalue", | ||||
| 				"email":              "emailvalue", | ||||
| 				"email_verified":     true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                    "withoutPreferredUsernameKeyAndBackendNotReturn", | ||||
| 			expectUserID:            "subvalue", | ||||
| 			expectUserName:          "namevalue", | ||||
| 			expectPreferredUsername: "", | ||||
| 			expectedEmailField:      "emailvalue", | ||||
| 			token: map[string]interface{}{ | ||||
| 				"sub":            "subvalue", | ||||
| 				"name":           "namevalue", | ||||
| 				"email":          "emailvalue", | ||||
| 				"email_verified": true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                      "emptyEmailScope", | ||||
| 			expectUserID:              "subvalue", | ||||
| @@ -135,6 +199,41 @@ func TestHandleCallback(t *testing.T) { | ||||
| 				"email":     "emailvalue", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                      "customGroupsKey", | ||||
| 			groupsKey:                 "cognito:groups", | ||||
| 			expectUserID:              "subvalue", | ||||
| 			expectUserName:            "namevalue", | ||||
| 			expectedEmailField:        "emailvalue", | ||||
| 			expectGroups:              []string{"group3", "group4"}, | ||||
| 			scopes:                    []string{"groups"}, | ||||
| 			insecureSkipEmailVerified: true, | ||||
| 			token: map[string]interface{}{ | ||||
| 				"sub":            "subvalue", | ||||
| 				"name":           "namevalue", | ||||
| 				"user_name":      "username", | ||||
| 				"email":          "emailvalue", | ||||
| 				"cognito:groups": []string{"group3", "group4"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:                      "customGroupsKeyButGroupsProvided", | ||||
| 			groupsKey:                 "cognito:groups", | ||||
| 			expectUserID:              "subvalue", | ||||
| 			expectUserName:            "namevalue", | ||||
| 			expectedEmailField:        "emailvalue", | ||||
| 			expectGroups:              []string{"group1", "group2"}, | ||||
| 			scopes:                    []string{"groups"}, | ||||
| 			insecureSkipEmailVerified: true, | ||||
| 			token: map[string]interface{}{ | ||||
| 				"sub":            "subvalue", | ||||
| 				"name":           "namevalue", | ||||
| 				"user_name":      "username", | ||||
| 				"email":          "emailvalue", | ||||
| 				"groups":         []string{"group1", "group2"}, | ||||
| 				"cognito:groups": []string{"group3", "group4"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range tests { | ||||
| @@ -162,8 +261,12 @@ func TestHandleCallback(t *testing.T) { | ||||
| 				UserIDKey:                 tc.userIDKey, | ||||
| 				UserNameKey:               tc.userNameKey, | ||||
| 				InsecureSkipEmailVerified: tc.insecureSkipEmailVerified, | ||||
| 				InsecureEnableGroups:      true, | ||||
| 				BasicAuthUnsupported:      &basicAuth, | ||||
| 			} | ||||
| 			config.ClaimMapping.PreferredUsernameKey = tc.preferredUsernameKey | ||||
| 			config.ClaimMapping.EmailKey = tc.emailKey | ||||
| 			config.ClaimMapping.GroupsKey = tc.groupsKey | ||||
|  | ||||
| 			conn, err := newConnector(config) | ||||
| 			if err != nil { | ||||
| @@ -182,8 +285,10 @@ func TestHandleCallback(t *testing.T) { | ||||
|  | ||||
| 			expectEquals(t, identity.UserID, tc.expectUserID) | ||||
| 			expectEquals(t, identity.Username, tc.expectUserName) | ||||
| 			expectEquals(t, identity.PreferredUsername, tc.expectPreferredUsername) | ||||
| 			expectEquals(t, identity.Email, tc.expectedEmailField) | ||||
| 			expectEquals(t, identity.EmailVerified, true) | ||||
| 			expectEquals(t, identity.Groups, tc.expectGroups) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user