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:
commit
ca0a9e821e
@ -8,8 +8,6 @@ Prominent examples of OpenID Connect providers include Google Accounts, Salesfor
|
|||||||
|
|
||||||
## Caveats
|
## 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].
|
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
|
## Configuration
|
||||||
@ -75,9 +73,8 @@ connectors:
|
|||||||
# getUserInfo: true
|
# getUserInfo: true
|
||||||
|
|
||||||
# The set claim is used as user id.
|
# The set claim is used as user id.
|
||||||
# Default: sub
|
|
||||||
# Claims list at https://openid.net/specs/openid-connect-core-1_0.html#Claims
|
# Claims list at https://openid.net/specs/openid-connect-core-1_0.html#Claims
|
||||||
#
|
# Default: sub
|
||||||
# userIDKey: nickname
|
# userIDKey: nickname
|
||||||
|
|
||||||
# The set claim is used as user name.
|
# The set claim is used as user name.
|
||||||
@ -88,9 +85,25 @@ connectors:
|
|||||||
# However this is not supported by all OIDC providers, some of them support different
|
# However this is not supported by all OIDC providers, some of them support different
|
||||||
# value for prompt, like "prompt=login" or "prompt=none"
|
# value for prompt, like "prompt=login" or "prompt=none"
|
||||||
# promptType: consent
|
# 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
|
[oidc-doc]: openid-connect.md
|
||||||
[issue-863]: https://github.com/dexidp/dex/issues/863
|
[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
|
[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 | |
|
| [GitHub](Documentation/connectors/github.md) | yes | yes | yes | stable | |
|
||||||
| [SAML 2.0](Documentation/connectors/saml.md) | no | yes | no | stable |
|
| [SAML 2.0](Documentation/connectors/saml.md) | no | yes | no | stable |
|
||||||
| [GitLab](Documentation/connectors/gitlab.md) | yes | yes | yes | beta | |
|
| [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 | |
|
| [Google](Documentation/connectors/google.md) | yes | yes | yes | alpha | |
|
||||||
| [LinkedIn](Documentation/connectors/linkedin.md) | yes | no | no | beta | |
|
| [LinkedIn](Documentation/connectors/linkedin.md) | yes | no | no | beta | |
|
||||||
| [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | |
|
| [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | |
|
||||||
|
@ -49,14 +49,23 @@ type Config struct {
|
|||||||
// id tokens
|
// id tokens
|
||||||
GetUserInfo bool `json:"getUserInfo"`
|
GetUserInfo bool `json:"getUserInfo"`
|
||||||
|
|
||||||
// Configurable key which contains the user id claim
|
|
||||||
UserIDKey string `json:"userIDKey"`
|
UserIDKey string `json:"userIDKey"`
|
||||||
|
|
||||||
// Configurable key which contains the user name claim
|
|
||||||
UserNameKey string `json:"userNameKey"`
|
UserNameKey string `json:"userNameKey"`
|
||||||
|
|
||||||
// PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent)
|
// PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent)
|
||||||
PromptType string `json:"promptType"`
|
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
|
// 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,
|
insecureSkipEmailVerified: c.InsecureSkipEmailVerified,
|
||||||
insecureEnableGroups: c.InsecureEnableGroups,
|
insecureEnableGroups: c.InsecureEnableGroups,
|
||||||
getUserInfo: c.GetUserInfo,
|
getUserInfo: c.GetUserInfo,
|
||||||
|
promptType: c.PromptType,
|
||||||
userIDKey: c.UserIDKey,
|
userIDKey: c.UserIDKey,
|
||||||
userNameKey: c.UserNameKey,
|
userNameKey: c.UserNameKey,
|
||||||
promptType: c.PromptType,
|
preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey,
|
||||||
|
emailKey: c.ClaimMapping.EmailKey,
|
||||||
|
groupsKey: c.ClaimMapping.GroupsKey,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,9 +175,12 @@ type oidcConnector struct {
|
|||||||
insecureSkipEmailVerified bool
|
insecureSkipEmailVerified bool
|
||||||
insecureEnableGroups bool
|
insecureEnableGroups bool
|
||||||
getUserInfo bool
|
getUserInfo bool
|
||||||
|
promptType string
|
||||||
userIDKey string
|
userIDKey string
|
||||||
userNameKey string
|
userNameKey string
|
||||||
promptType string
|
preferredUsernameKey string
|
||||||
|
emailKey string
|
||||||
|
groupsKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *oidcConnector) Close() error {
|
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)
|
return identity, fmt.Errorf("missing \"%s\" claim", userNameKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preferredUsername, found := claims["preferred_username"].(string)
|
||||||
|
if !found {
|
||||||
|
preferredUsername, _ = claims[c.preferredUsernameKey].(string)
|
||||||
|
}
|
||||||
|
|
||||||
hasEmailScope := false
|
hasEmailScope := false
|
||||||
for _, s := range c.oauth2Config.Scopes {
|
for _, s := range c.oauth2Config.Scopes {
|
||||||
if s == "email" {
|
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 {
|
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)
|
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")
|
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 {
|
if len(c.hostedDomains) > 0 {
|
||||||
found := false
|
found := false
|
||||||
for _, domain := range c.hostedDomains {
|
for _, domain := range c.hostedDomains {
|
||||||
@ -322,8 +369,10 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
|
|||||||
identity = connector.Identity{
|
identity = connector.Identity{
|
||||||
UserID: idToken.Subject,
|
UserID: idToken.Subject,
|
||||||
Username: name,
|
Username: name,
|
||||||
|
PreferredUsername: preferredUsername,
|
||||||
Email: email,
|
Email: email,
|
||||||
EmailVerified: emailVerified,
|
EmailVerified: emailVerified,
|
||||||
|
Groups: groups,
|
||||||
ConnectorData: connData,
|
ConnectorData: connData,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,18 +384,5 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
|
|||||||
identity.UserID = userID
|
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
|
return identity, nil
|
||||||
}
|
}
|
||||||
|
@ -49,10 +49,15 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
userIDKey string
|
userIDKey string
|
||||||
userNameKey string
|
userNameKey string
|
||||||
|
preferredUsernameKey string
|
||||||
|
emailKey string
|
||||||
|
groupsKey string
|
||||||
insecureSkipEmailVerified bool
|
insecureSkipEmailVerified bool
|
||||||
scopes []string
|
scopes []string
|
||||||
expectUserID string
|
expectUserID string
|
||||||
expectUserName string
|
expectUserName string
|
||||||
|
expectGroups []string
|
||||||
|
expectPreferredUsername string
|
||||||
expectedEmailField string
|
expectedEmailField string
|
||||||
token map[string]interface{}
|
token map[string]interface{}
|
||||||
}{
|
}{
|
||||||
@ -62,14 +67,31 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
userNameKey: "", // not configured
|
userNameKey: "", // not configured
|
||||||
expectUserID: "subvalue",
|
expectUserID: "subvalue",
|
||||||
expectUserName: "namevalue",
|
expectUserName: "namevalue",
|
||||||
|
expectGroups: []string{"group1", "group2"},
|
||||||
expectedEmailField: "emailvalue",
|
expectedEmailField: "emailvalue",
|
||||||
token: map[string]interface{}{
|
token: map[string]interface{}{
|
||||||
"sub": "subvalue",
|
"sub": "subvalue",
|
||||||
"name": "namevalue",
|
"name": "namevalue",
|
||||||
|
"groups": []string{"group1", "group2"},
|
||||||
"email": "emailvalue",
|
"email": "emailvalue",
|
||||||
"email_verified": true,
|
"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",
|
name: "email_verified not in claims, configured to be skipped",
|
||||||
insecureSkipEmailVerified: true,
|
insecureSkipEmailVerified: true,
|
||||||
@ -108,6 +130,48 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
"email_verified": true,
|
"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",
|
name: "emptyEmailScope",
|
||||||
expectUserID: "subvalue",
|
expectUserID: "subvalue",
|
||||||
@ -135,6 +199,41 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
"email": "emailvalue",
|
"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 {
|
for _, tc := range tests {
|
||||||
@ -162,8 +261,12 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
UserIDKey: tc.userIDKey,
|
UserIDKey: tc.userIDKey,
|
||||||
UserNameKey: tc.userNameKey,
|
UserNameKey: tc.userNameKey,
|
||||||
InsecureSkipEmailVerified: tc.insecureSkipEmailVerified,
|
InsecureSkipEmailVerified: tc.insecureSkipEmailVerified,
|
||||||
|
InsecureEnableGroups: true,
|
||||||
BasicAuthUnsupported: &basicAuth,
|
BasicAuthUnsupported: &basicAuth,
|
||||||
}
|
}
|
||||||
|
config.ClaimMapping.PreferredUsernameKey = tc.preferredUsernameKey
|
||||||
|
config.ClaimMapping.EmailKey = tc.emailKey
|
||||||
|
config.ClaimMapping.GroupsKey = tc.groupsKey
|
||||||
|
|
||||||
conn, err := newConnector(config)
|
conn, err := newConnector(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -182,8 +285,10 @@ func TestHandleCallback(t *testing.T) {
|
|||||||
|
|
||||||
expectEquals(t, identity.UserID, tc.expectUserID)
|
expectEquals(t, identity.UserID, tc.expectUserID)
|
||||||
expectEquals(t, identity.Username, tc.expectUserName)
|
expectEquals(t, identity.Username, tc.expectUserName)
|
||||||
|
expectEquals(t, identity.PreferredUsername, tc.expectPreferredUsername)
|
||||||
expectEquals(t, identity.Email, tc.expectedEmailField)
|
expectEquals(t, identity.Email, tc.expectedEmailField)
|
||||||
expectEquals(t, identity.EmailVerified, true)
|
expectEquals(t, identity.EmailVerified, true)
|
||||||
|
expectEquals(t, identity.Groups, tc.expectGroups)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user