@@ -12,12 +12,13 @@ import (
1212
1313// CustomOAuthProvider implements OAuthProvider for custom OAuth2 providers
1414type CustomOAuthProvider struct {
15- config * oauth2.Config
16- userinfoURL string
17- pkceEnabled bool
18- acceptableClientIDs []string
19- attributeMapping map [string ]interface {}
20- authorizationParams map [string ]interface {}
15+ config * oauth2.Config
16+ userinfoURL string
17+ pkceEnabled bool
18+ acceptableClientIDs []string
19+ attributeMapping map [string ]interface {}
20+ authorizationParams map [string ]interface {}
21+ customClaimsAllowlist []string
2122}
2223
2324// NewCustomOAuthProvider creates a new custom OAuth provider
@@ -27,6 +28,7 @@ func NewCustomOAuthProvider(
2728 pkceEnabled bool ,
2829 acceptableClientIDs []string ,
2930 attributeMapping , authorizationParams map [string ]interface {},
31+ customClaimsAllowlist []string ,
3032) * CustomOAuthProvider {
3133 config := & oauth2.Config {
3234 ClientID : clientID ,
@@ -40,12 +42,13 @@ func NewCustomOAuthProvider(
4042 }
4143
4244 return & CustomOAuthProvider {
43- config : config ,
44- userinfoURL : userinfoURL ,
45- pkceEnabled : pkceEnabled ,
46- acceptableClientIDs : acceptableClientIDs ,
47- attributeMapping : attributeMapping ,
48- authorizationParams : authorizationParams ,
45+ config : config ,
46+ userinfoURL : userinfoURL ,
47+ pkceEnabled : pkceEnabled ,
48+ acceptableClientIDs : acceptableClientIDs ,
49+ attributeMapping : attributeMapping ,
50+ authorizationParams : authorizationParams ,
51+ customClaimsAllowlist : customClaimsAllowlist ,
4952 }
5053}
5154
@@ -68,11 +71,14 @@ func (p *CustomOAuthProvider) GetOAuthToken(ctx context.Context, code string, op
6871
6972// GetUserData fetches user data from the provider's userinfo endpoint
7073func (p * CustomOAuthProvider ) GetUserData (ctx context.Context , tok * oauth2.Token ) (* UserProvidedData , error ) {
71- var claims Claims
72- if err := makeRequest ( ctx , tok , p . config , p . userinfoURL , & claims ); err != nil {
74+ claims , raw , err := fetchUserinfoClaims ( ctx , tok , p . config , p . userinfoURL )
75+ if err != nil {
7376 return nil , err
7477 }
7578
79+ // Capture allowlisted custom claims before attribute mapping
80+ captureAllowedClaims (raw , p .customClaimsAllowlist , & claims )
81+
7682 // Apply attribute mapping if configured
7783 if len (p .attributeMapping ) > 0 {
7884 claims = applyAttributeMapping (claims , p .attributeMapping )
@@ -101,13 +107,14 @@ func (p *CustomOAuthProvider) RequiresPKCE() bool {
101107
102108// CustomOIDCProvider implements OAuthProvider for custom OIDC providers
103109type CustomOIDCProvider struct {
104- config * oauth2.Config
105- oidcProvider * oidc.Provider
106- userinfoEndpoint string
107- pkceEnabled bool
108- acceptableClientIDs []string
109- attributeMapping map [string ]interface {}
110- authorizationParams map [string ]interface {}
110+ config * oauth2.Config
111+ oidcProvider * oidc.Provider
112+ userinfoEndpoint string
113+ pkceEnabled bool
114+ acceptableClientIDs []string
115+ attributeMapping map [string ]interface {}
116+ authorizationParams map [string ]interface {}
117+ customClaimsAllowlist []string
111118}
112119
113120// NewCustomOIDCProvider creates a new custom OIDC provider.
@@ -123,6 +130,7 @@ func NewCustomOIDCProvider(
123130 pkceEnabled bool ,
124131 acceptableClientIDs []string ,
125132 attributeMapping , authorizationParams map [string ]interface {},
133+ customClaimsAllowlist []string ,
126134 cache * OIDCProviderCache ,
127135) (* CustomOIDCProvider , error ) {
128136 // Ensure 'openid' scope is always present for OIDC
@@ -155,13 +163,14 @@ func NewCustomOIDCProvider(
155163 }
156164
157165 return & CustomOIDCProvider {
158- config : config ,
159- oidcProvider : oidcProvider ,
160- userinfoEndpoint : userinfoEndpoint ,
161- pkceEnabled : pkceEnabled ,
162- acceptableClientIDs : acceptableClientIDs ,
163- attributeMapping : attributeMapping ,
164- authorizationParams : authorizationParams ,
166+ config : config ,
167+ oidcProvider : oidcProvider ,
168+ userinfoEndpoint : userinfoEndpoint ,
169+ pkceEnabled : pkceEnabled ,
170+ acceptableClientIDs : acceptableClientIDs ,
171+ attributeMapping : attributeMapping ,
172+ authorizationParams : authorizationParams ,
173+ customClaimsAllowlist : customClaimsAllowlist ,
165174 }, nil
166175}
167176
@@ -201,6 +210,17 @@ func (p *CustomOIDCProvider) GetUserData(ctx context.Context, tok *oauth2.Token)
201210 return nil , err
202211 }
203212
213+ // Capture allowlisted custom claims from the raw ID token before
214+ // attribute mapping. Because we only copy explicitly listed keys, there
215+ // is no risk of re-adding keys a parser intentionally stripped (e.g. Azure).
216+ if len (p .customClaimsAllowlist ) > 0 && userData .Metadata != nil {
217+ var raw map [string ]interface {}
218+ if err := idTokenObj .Claims (& raw ); err != nil {
219+ return nil , fmt .Errorf ("failed to read ID token claims: %w" , err )
220+ }
221+ captureAllowedClaims (raw , p .customClaimsAllowlist , userData .Metadata )
222+ }
223+
204224 // Apply attribute mapping to the metadata from ID token
205225 if len (p .attributeMapping ) > 0 && userData .Metadata != nil {
206226 * userData .Metadata = applyAttributeMapping (* userData .Metadata , p .attributeMapping )
@@ -211,11 +231,14 @@ func (p *CustomOIDCProvider) GetUserData(ctx context.Context, tok *oauth2.Token)
211231
212232 // No ID token, use userinfo endpoint
213233 if p .userinfoEndpoint != "" {
214- var claims Claims
215- if err := makeRequest ( ctx , tok , p . config , p . userinfoEndpoint , & claims ); err != nil {
234+ claims , raw , err := fetchUserinfoClaims ( ctx , tok , p . config , p . userinfoEndpoint )
235+ if err != nil {
216236 return nil , err
217237 }
218238
239+ // Capture allowlisted custom claims before attribute mapping
240+ captureAllowedClaims (raw , p .customClaimsAllowlist , & claims )
241+
219242 // Apply attribute mapping
220243 if len (p .attributeMapping ) > 0 {
221244 claims = applyAttributeMapping (claims , p .attributeMapping )
@@ -268,6 +291,44 @@ func (p *CustomOIDCProvider) validateAudience(audiences []string) error {
268291 return fmt .Errorf ("token audience %v does not match any acceptable client ID" , audiences )
269292}
270293
294+ // fetchUserinfoClaims fetches the userinfo response once and returns both the
295+ // typed Claims and the raw claim map. The raw map is needed so that arbitrary
296+ // allowlisted keys (which have no typed field) can be copied verbatim.
297+ func fetchUserinfoClaims (ctx context.Context , tok * oauth2.Token , config * oauth2.Config , url string ) (Claims , map [string ]interface {}, error ) {
298+ var raw map [string ]interface {}
299+ if err := makeRequest (ctx , tok , config , url , & raw ); err != nil {
300+ return Claims {}, nil , err
301+ }
302+
303+ var claims Claims
304+ b , err := json .Marshal (raw )
305+ if err != nil {
306+ return Claims {}, nil , err
307+ }
308+ if err := json .Unmarshal (b , & claims ); err != nil {
309+ return Claims {}, nil , err
310+ }
311+
312+ return claims , raw , nil
313+ }
314+
315+ // captureAllowedClaims copies each allowlisted key present in raw into
316+ // c.CustomClaims verbatim. An empty allowlist captures nothing (D1), and keys
317+ // absent from raw are silently skipped (no nil entry is created). Because only
318+ // explicitly listed keys are copied, protocol/registered claims never leak.
319+ func captureAllowedClaims (raw map [string ]interface {}, allowlist []string , c * Claims ) {
320+ for _ , key := range allowlist {
321+ value , ok := raw [key ]
322+ if ! ok {
323+ continue
324+ }
325+ if c .CustomClaims == nil {
326+ c .CustomClaims = make (map [string ]interface {})
327+ }
328+ c .CustomClaims [key ] = value
329+ }
330+ }
331+
271332// applyAttributeMapping applies custom attribute mapping to claims
272333func applyAttributeMapping (claims Claims , mapping map [string ]interface {}) Claims {
273334 // Create a map representation of claims for easier manipulation
0 commit comments