@@ -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
@@ -119,6 +126,7 @@ func NewCustomOIDCProvider(
119126 pkceEnabled bool ,
120127 acceptableClientIDs []string ,
121128 attributeMapping , authorizationParams map [string ]interface {},
129+ customClaimsAllowlist []string ,
122130 cache * OIDCProviderCache ,
123131) (* CustomOIDCProvider , error ) {
124132 // Ensure 'openid' scope is always present for OIDC
@@ -152,13 +160,14 @@ func NewCustomOIDCProvider(
152160 }
153161
154162 return & CustomOIDCProvider {
155- config : config ,
156- oidcProvider : oidcProvider ,
157- userinfoEndpoint : userinfoEndpoint ,
158- pkceEnabled : pkceEnabled ,
159- acceptableClientIDs : acceptableClientIDs ,
160- attributeMapping : attributeMapping ,
161- authorizationParams : authorizationParams ,
163+ config : config ,
164+ oidcProvider : oidcProvider ,
165+ userinfoEndpoint : userinfoEndpoint ,
166+ pkceEnabled : pkceEnabled ,
167+ acceptableClientIDs : acceptableClientIDs ,
168+ attributeMapping : attributeMapping ,
169+ authorizationParams : authorizationParams ,
170+ customClaimsAllowlist : customClaimsAllowlist ,
162171 }, nil
163172}
164173
@@ -198,6 +207,17 @@ func (p *CustomOIDCProvider) GetUserData(ctx context.Context, tok *oauth2.Token)
198207 return nil , err
199208 }
200209
210+ // Capture allowlisted custom claims from the raw ID token before
211+ // attribute mapping. Because we only copy explicitly listed keys, there
212+ // is no risk of re-adding keys a parser intentionally stripped (e.g. Azure).
213+ if len (p .customClaimsAllowlist ) > 0 && userData .Metadata != nil {
214+ var raw map [string ]interface {}
215+ if err := idTokenObj .Claims (& raw ); err != nil {
216+ return nil , fmt .Errorf ("failed to read ID token claims: %w" , err )
217+ }
218+ captureAllowedClaims (raw , p .customClaimsAllowlist , userData .Metadata )
219+ }
220+
201221 // Apply attribute mapping to the metadata from ID token
202222 if len (p .attributeMapping ) > 0 && userData .Metadata != nil {
203223 * userData .Metadata = applyAttributeMapping (* userData .Metadata , p .attributeMapping )
@@ -208,11 +228,14 @@ func (p *CustomOIDCProvider) GetUserData(ctx context.Context, tok *oauth2.Token)
208228
209229 // No ID token, use userinfo endpoint
210230 if p .userinfoEndpoint != "" {
211- var claims Claims
212- if err := makeRequest ( ctx , tok , p . config , p . userinfoEndpoint , & claims ); err != nil {
231+ claims , raw , err := fetchUserinfoClaims ( ctx , tok , p . config , p . userinfoEndpoint )
232+ if err != nil {
213233 return nil , err
214234 }
215235
236+ // Capture allowlisted custom claims before attribute mapping
237+ captureAllowedClaims (raw , p .customClaimsAllowlist , & claims )
238+
216239 // Apply attribute mapping
217240 if len (p .attributeMapping ) > 0 {
218241 claims = applyAttributeMapping (claims , p .attributeMapping )
@@ -265,6 +288,44 @@ func (p *CustomOIDCProvider) validateAudience(audiences []string) error {
265288 return fmt .Errorf ("token audience %v does not match any acceptable client ID" , audiences )
266289}
267290
291+ // fetchUserinfoClaims fetches the userinfo response once and returns both the
292+ // typed Claims and the raw claim map. The raw map is needed so that arbitrary
293+ // allowlisted keys (which have no typed field) can be copied verbatim.
294+ func fetchUserinfoClaims (ctx context.Context , tok * oauth2.Token , config * oauth2.Config , url string ) (Claims , map [string ]interface {}, error ) {
295+ var raw map [string ]interface {}
296+ if err := makeRequest (ctx , tok , config , url , & raw ); err != nil {
297+ return Claims {}, nil , err
298+ }
299+
300+ var claims Claims
301+ b , err := json .Marshal (raw )
302+ if err != nil {
303+ return Claims {}, nil , err
304+ }
305+ if err := json .Unmarshal (b , & claims ); err != nil {
306+ return Claims {}, nil , err
307+ }
308+
309+ return claims , raw , nil
310+ }
311+
312+ // captureAllowedClaims copies each allowlisted key present in raw into
313+ // c.CustomClaims verbatim. An empty allowlist captures nothing (D1), and keys
314+ // absent from raw are silently skipped (no nil entry is created). Because only
315+ // explicitly listed keys are copied, protocol/registered claims never leak.
316+ func captureAllowedClaims (raw map [string ]interface {}, allowlist []string , c * Claims ) {
317+ for _ , key := range allowlist {
318+ value , ok := raw [key ]
319+ if ! ok {
320+ continue
321+ }
322+ if c .CustomClaims == nil {
323+ c .CustomClaims = make (map [string ]interface {})
324+ }
325+ c .CustomClaims [key ] = value
326+ }
327+ }
328+
268329// applyAttributeMapping applies custom attribute mapping to claims
269330func applyAttributeMapping (claims Claims , mapping map [string ]interface {}) Claims {
270331 // Create a map representation of claims for easier manipulation
0 commit comments