Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support username, email and groups claim in OIDC connector #1634

Merged
merged 9 commits into from
Sep 8, 2020
31 changes: 18 additions & 13 deletions Documentation/connectors/oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,23 +72,30 @@ connectors:
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
# 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
#
# userIDKey: nickname

# The set claim is used as user name.
# Default: name
# userNameKey: nickname

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be added back as these are valid options, but not claims.

# For offline_access, the prompt parameter is set by default to "prompt=consent".
# 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
Expand Down
108 changes: 82 additions & 26 deletions connector/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,31 @@ type Config struct {
// id tokens
GetUserInfo bool `json:"getUserInfo"`

// Configurable key which contains the user id claim
// Deprecated: use UserIDKey in claimMapping instead
UserIDKey string `json:"userIDKey"`

// Configurable key which contains the user name claim
// Deprecated: use UserNameKey in claimMapping instead
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These shouldn't be deprecated.

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 user id claim
UserIDKey string `json:"user_id"` // defaults to "sub"

// Configurable key which contains the username claim
UserNameKey string `json:"user_name"` // defaults to "name"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess these shouldn't be here either.


// 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
Expand Down Expand Up @@ -121,6 +138,18 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
c.PromptType = "consent"
}

// Backward compatibility
userIDKey := c.ClaimMapping.UserIDKey
if userIDKey == "" {
userIDKey = c.UserIDKey
}

// Backward compatibility
userNameKey := c.ClaimMapping.UserNameKey
if userNameKey == "" {
userNameKey = c.UserNameKey
}

clientID := c.ClientID
return &oidcConnector{
provider: provider,
Expand All @@ -141,9 +170,12 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
insecureSkipEmailVerified: c.InsecureSkipEmailVerified,
insecureEnableGroups: c.InsecureEnableGroups,
getUserInfo: c.GetUserInfo,
userIDKey: c.UserIDKey,
userNameKey: c.UserNameKey,
promptType: c.PromptType,
userIDKey: userIDKey,
userNameKey: userNameKey,
preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey,
emailKey: c.ClaimMapping.EmailKey,
groupsKey: c.ClaimMapping.GroupsKey,
}, nil
}

Expand All @@ -163,9 +195,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 {
Expand Down Expand Up @@ -273,6 +308,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" {
Expand All @@ -281,9 +321,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)
Expand All @@ -294,8 +341,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 {
Expand All @@ -320,11 +387,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 != "" {
Expand All @@ -335,18 +404,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
}
Loading