Skip to content

Commit 41207ba

Browse files
author
Rui Yang
committed
Combine #1691 and #1776 to unify OIDC provider claim mapping
add tests for groups key mapping Signed-off-by: Rui Yang <[email protected]>
1 parent a783667 commit 41207ba

File tree

4 files changed

+145
-76
lines changed

4 files changed

+145
-76
lines changed

Documentation/connectors/oidc.md

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ Prominent examples of OpenID Connect providers include Google Accounts, Salesfor
88

99
## Caveats
1010

11-
This connector does not support the "groups" claim. Progress for this is tracked in [issue #1065][issue-1065].
12-
1311
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].
1412

1513
## Configuration
@@ -56,11 +54,6 @@ connectors:
5654
# - email
5755
# - groups
5856

59-
# Some providers return no standard email claim key (ex: 'mail')
60-
# Override email claim key
61-
# Default is "email"
62-
# emailClaim: email
63-
6457
# Some providers return claims without "email_verified", when they had no usage of emails verification in enrollment process
6558
# or if they are acting as a proxy for another IDP etc AWS Cognito with an upstream SAML IDP
6659
# This can be overridden with the below option
@@ -73,33 +66,43 @@ connectors:
7366
# This can be overridden with the below option
7467
# insecureEnableGroups: true
7568

76-
# If an OIDC provider uses a different claim name than the standard "groups" claim to provide group information
77-
# the claim to use can be specified
78-
# groupsClaimMapping: "cognito:groups"
79-
8069
# When enabled, the OpenID Connector will query the UserInfo endpoint for additional claims. UserInfo claims
8170
# take priority over claims returned by the IDToken. This option should be used when the IDToken doesn't contain
8271
# all the claims requested.
8372
# https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
8473
# getUserInfo: true
8574

86-
# The set claim is used as user id.
87-
# Default: sub
88-
# Claims list at https://openid.net/specs/openid-connect-core-1_0.html#Claims
89-
#
90-
# userIDKey: nickname
91-
92-
# The set claim is used as user name.
93-
# Default: name
94-
# userNameKey: nickname
95-
9675
# For offline_access, the prompt parameter is set by default to "prompt=consent".
9776
# However this is not supported by all OIDC providers, some of them support different
9877
# value for prompt, like "prompt=login" or "prompt=none"
9978
# promptType: consent
79+
80+
81+
# Some providers return no standard claim that is different to
82+
# claims list at https://openid.net/specs/openid-connect-core-1_0.html#Claims
83+
# Use claimMapping to specify custom claim names
84+
claimMapping:
85+
# The set claim is used as user id.
86+
# Default: sub
87+
# user_id: nickname
88+
89+
# The set claim is used as user name.
90+
# Default: name
91+
# user_name: nickname
92+
93+
# The set claim is used as preferred username.
94+
# Default: preferred_username
95+
# preferred_username: other_user_name
96+
97+
# The set claim is used as email.
98+
# Default: "email"
99+
# email: mail
100+
101+
# The set claim is used as groups.
102+
# Default: "groups"
103+
# groups: "cognito:groups"
100104
```
101105

102106
[oidc-doc]: openid-connect.md
103107
[issue-863]: https://github.com/dexidp/dex/issues/863
104-
[issue-1065]: https://github.com/dexidp/dex/issues/1065
105108
[azure-ad-v1]: https://github.com/coreos/go-oidc/issues/133

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Dex implements the following connectors:
6969
| [GitHub](Documentation/connectors/github.md) | yes | yes | yes | stable | |
7070
| [SAML 2.0](Documentation/connectors/saml.md) | no | yes | no | stable |
7171
| [GitLab](Documentation/connectors/gitlab.md) | yes | yes | yes | beta | |
72-
| [OpenID Connect](Documentation/connectors/oidc.md) | yes | no ([#1065][issue-1065]) | no | beta | Includes Salesforce, Azure, etc. |
72+
| [OpenID Connect](Documentation/connectors/oidc.md) | yes | yes | yes | beta | Includes Salesforce, Azure, etc. |
7373
| [Google](Documentation/connectors/google.md) | yes | yes | yes | alpha | |
7474
| [LinkedIn](Documentation/connectors/linkedin.md) | yes | no | no | beta | |
7575
| [Microsoft](Documentation/connectors/microsoft.md) | yes | yes | no | beta | |

connector/oidc/oidc.go

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -44,28 +44,36 @@ type Config struct {
4444
// InsecureEnableGroups enables groups claims. This is disabled by default until https://github.com/dexidp/dex/issues/1065 is resolved
4545
InsecureEnableGroups bool `json:"insecureEnableGroups"`
4646

47-
// GroupsClaimMapping sets the name of the claim which contains the users groups. InsecureEnableGroups must be enabled to use this setting
48-
GroupsClaimMapping string `json:"groupsClaimMapping"` // defaults to "groups"
49-
5047
// GetUserInfo uses the userinfo endpoint to get additional claims for
5148
// the token. This is especially useful where upstreams return "thin"
5249
// id tokens
5350
GetUserInfo bool `json:"getUserInfo"`
5451

55-
// Configurable key which contains the user id claim
52+
// Deprecated: use UserIDKey in claimMapping instead
5653
UserIDKey string `json:"userIDKey"`
5754

58-
// Configurable key which contains the user name claim
55+
// Deprecated: use UserNameKey in claimMapping instead
5956
UserNameKey string `json:"userNameKey"`
6057

61-
// Configurable key which contains the preferred username claims
62-
PreferredUsernameKey string `json:"preferredUsernameKey"`
63-
64-
// EmailClaim override email claim key. Defaults to "email"
65-
EmailClaim string `json:"emailClaim"`
66-
6758
// PromptType will be used fot the prompt parameter (when offline_access, by default prompt=consent)
6859
PromptType string `json:"promptType"`
60+
61+
ClaimMapping struct {
62+
// Configurable key which contains the user id claim
63+
UserIDKey string `json:"user_id"` // defaults to "sub"
64+
65+
// Configurable key which contains the username claim
66+
UserNameKey string `json:"user_name"` // defaults to "name"
67+
68+
// Configurable key which contains the preferred username claims
69+
PreferredUsernameKey string `json:"preferred_username"` // defaults to "preferred_username"
70+
71+
// Configurable key which contains the email claims
72+
EmailKey string `json:"email"` // defaults to "email"
73+
74+
// Configurable key which contains the groups claims
75+
GroupsKey string `json:"groups"` // defaults to "groups"
76+
} `json:"claimMapping"`
6977
}
7078

7179
// Domains that don't support basic auth. golang.org/x/oauth2 has an internal
@@ -118,11 +126,6 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
118126
endpoint.AuthStyle = oauth2.AuthStyleInParams
119127
}
120128

121-
emailClaim := "email"
122-
if len(c.EmailClaim) > 0 {
123-
emailClaim = c.EmailClaim
124-
}
125-
126129
scopes := []string{oidc.ScopeOpenID}
127130
if len(c.Scopes) > 0 {
128131
scopes = append(scopes, c.Scopes...)
@@ -135,9 +138,16 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
135138
c.PromptType = "consent"
136139
}
137140

138-
// GroupsClaimMapping should be "groups" by default, if not set
139-
if c.GroupsClaimMapping == "" {
140-
c.GroupsClaimMapping = "groups"
141+
// Backward compatibility
142+
userIDKey := c.ClaimMapping.UserIDKey
143+
if userIDKey == "" {
144+
userIDKey = c.UserIDKey
145+
}
146+
147+
// Backward compatibility
148+
userNameKey := c.ClaimMapping.UserNameKey
149+
if userNameKey == "" {
150+
userNameKey = c.UserNameKey
141151
}
142152

143153
clientID := c.ClientID
@@ -159,13 +169,13 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
159169
hostedDomains: c.HostedDomains,
160170
insecureSkipEmailVerified: c.InsecureSkipEmailVerified,
161171
insecureEnableGroups: c.InsecureEnableGroups,
162-
groupsClaimMapping: c.GroupsClaimMapping,
163172
getUserInfo: c.GetUserInfo,
164-
userIDKey: c.UserIDKey,
165-
userNameKey: c.UserNameKey,
166-
preferredUsernameKey: c.PreferredUsernameKey,
167-
emailClaim: emailClaim,
168173
promptType: c.PromptType,
174+
userIDKey: userIDKey,
175+
userNameKey: userNameKey,
176+
preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey,
177+
emailKey: c.ClaimMapping.EmailKey,
178+
groupsKey: c.ClaimMapping.GroupsKey,
169179
}, nil
170180
}
171181

@@ -184,13 +194,13 @@ type oidcConnector struct {
184194
hostedDomains []string
185195
insecureSkipEmailVerified bool
186196
insecureEnableGroups bool
187-
groupsClaimMapping string
188197
getUserInfo bool
198+
promptType string
189199
userIDKey string
190200
userNameKey string
191201
preferredUsernameKey string
192-
emailClaim string
193-
promptType string
202+
emailKey string
203+
groupsKey string
194204
}
195205

196206
func (c *oidcConnector) Close() error {
@@ -298,6 +308,11 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
298308
return identity, fmt.Errorf("missing \"%s\" claim", userNameKey)
299309
}
300310

311+
preferredUsername, found := claims["preferred_username"].(string)
312+
if !found {
313+
preferredUsername, _ = claims[c.preferredUsernameKey].(string)
314+
}
315+
301316
hasEmailScope := false
302317
for _, s := range c.oauth2Config.Scopes {
303318
if s == "email" {
@@ -306,9 +321,16 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
306321
}
307322
}
308323

309-
email, found := claims[c.emailClaim].(string)
324+
var email string
325+
emailKey := "email"
326+
email, found = claims[emailKey].(string)
327+
if !found && c.emailKey != "" {
328+
emailKey = c.emailKey
329+
email, found = claims[emailKey].(string)
330+
}
331+
310332
if !found && hasEmailScope {
311-
return identity, fmt.Errorf("missing \"%s\" claim", c.emailClaim)
333+
return identity, fmt.Errorf("missing \"%s\" claim", emailKey)
312334
}
313335

314336
emailVerified, found := claims["email_verified"].(bool)
@@ -319,13 +341,28 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
319341
return identity, errors.New("missing \"email_verified\" claim")
320342
}
321343
}
322-
hostedDomain, _ := claims["hd"].(string)
323344

324-
preferredUsername, found := claims["preferred_username"].(string)
325-
if !found {
326-
preferredUsername, _ = claims[c.preferredUsernameKey].(string)
345+
var groups []string
346+
if c.insecureEnableGroups {
347+
groupsKey := "groups"
348+
vs, found := claims[groupsKey].([]interface{})
349+
if !found {
350+
groupsKey = c.groupsKey
351+
vs, found = claims[groupsKey].([]interface{})
352+
}
353+
354+
if found {
355+
for _, v := range vs {
356+
if s, ok := v.(string); ok {
357+
groups = append(groups, s)
358+
} else {
359+
return identity, fmt.Errorf("malformed \"%v\" claim", groupsKey)
360+
}
361+
}
362+
}
327363
}
328364

365+
hostedDomain, _ := claims["hd"].(string)
329366
if len(c.hostedDomains) > 0 {
330367
found := false
331368
for _, domain := range c.hostedDomains {
@@ -355,6 +392,7 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
355392
PreferredUsername: preferredUsername,
356393
Email: email,
357394
EmailVerified: emailVerified,
395+
Groups: groups,
358396
ConnectorData: connData,
359397
}
360398

@@ -366,19 +404,5 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
366404
identity.UserID = userID
367405
}
368406

369-
if c.insecureEnableGroups {
370-
371-
vs, ok := claims[c.groupsClaimMapping].([]interface{})
372-
if ok {
373-
for _, v := range vs {
374-
if s, ok := v.(string); ok {
375-
identity.Groups = append(identity.Groups, s)
376-
} else {
377-
return identity, fmt.Errorf("malformed \"%v\" claim", c.groupsClaimMapping)
378-
}
379-
}
380-
}
381-
}
382-
383407
return identity, nil
384408
}

0 commit comments

Comments
 (0)