@@ -46,6 +46,14 @@ import (
46
46
"github.com/coder/coder/v2/cryptorand"
47
47
)
48
48
49
+ type MergedClaimsSource string
50
+
51
+ var (
52
+ MergedClaimsSourceNone MergedClaimsSource = "none"
53
+ MergedClaimsSourceUserInfo MergedClaimsSource = "user_info"
54
+ MergedClaimsSourceAccessToken MergedClaimsSource = "access_token"
55
+ )
56
+
49
57
const (
50
58
userAuthLoggerName = "userauth"
51
59
OAuthConvertCookieValue = "coder_oauth_convert_jwt"
@@ -1116,11 +1124,13 @@ type OIDCConfig struct {
1116
1124
// AuthURLParams are additional parameters to be passed to the OIDC provider
1117
1125
// when requesting an access token.
1118
1126
AuthURLParams map [string ]string
1119
- // IgnoreUserInfo causes Coder to only use claims from the ID token to
1120
- // process OIDC logins. This is useful if the OIDC provider does not
1121
- // support the userinfo endpoint, or if the userinfo endpoint causes
1122
- // undesirable behavior.
1123
- IgnoreUserInfo bool
1127
+ // SecondaryClaims indicates where to source additional claim information from.
1128
+ // The standard is either 'MergedClaimsSourceNone' or 'MergedClaimsSourceUserInfo'.
1129
+ //
1130
+ // The OIDC compliant way is to use the userinfo endpoint. This option
1131
+ // is useful when the userinfo endpoint does not exist or causes undesirable
1132
+ // behavior.
1133
+ SecondaryClaims MergedClaimsSource
1124
1134
// SignInText is the text to display on the OIDC login button
1125
1135
SignInText string
1126
1136
// IconURL points to the URL of an icon to display on the OIDC login button
@@ -1216,50 +1226,39 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1216
1226
// Some providers (e.g. ADFS) do not support custom OIDC claims in the
1217
1227
// UserInfo endpoint, so we allow users to disable it and only rely on the
1218
1228
// ID token.
1219
- userInfoClaims := make ( map [ string ] interface {})
1229
+ //
1220
1230
// If user info is skipped, the idtokenClaims are the claims.
1221
1231
mergedClaims := idtokenClaims
1222
- if ! api .OIDCConfig .IgnoreUserInfo {
1223
- userInfo , err := api .OIDCConfig .Provider .UserInfo (ctx , oauth2 .StaticTokenSource (state .Token ))
1224
- if err == nil {
1225
- err = userInfo .Claims (& userInfoClaims )
1226
- if err != nil {
1227
- logger .Error (ctx , "oauth2: unable to unmarshal user info claims" , slog .Error (err ))
1228
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1229
- Message : "Failed to unmarshal user info claims." ,
1230
- Detail : err .Error (),
1231
- })
1232
- return
1233
- }
1234
- logger .Debug (ctx , "got oidc claims" ,
1235
- slog .F ("source" , "userinfo" ),
1236
- slog .F ("claim_fields" , claimFields (userInfoClaims )),
1237
- slog .F ("blank" , blankFields (userInfoClaims )),
1238
- )
1239
-
1240
- // Merge the claims from the ID token and the UserInfo endpoint.
1241
- // Information from UserInfo takes precedence.
1242
- mergedClaims = mergeClaims (idtokenClaims , userInfoClaims )
1232
+ supplementaryClaims := make (map [string ]interface {})
1233
+ switch api .OIDCConfig .SecondaryClaims {
1234
+ case MergedClaimsSourceUserInfo :
1235
+ supplementaryClaims , ok = api .userInfoClaims (ctx , rw , state , logger )
1236
+ if ! ok {
1237
+ return
1238
+ }
1243
1239
1244
- // Log all of the field names after merging.
1245
- logger .Debug (ctx , "got oidc claims" ,
1246
- slog .F ("source" , "merged" ),
1247
- slog .F ("claim_fields" , claimFields (mergedClaims )),
1248
- slog .F ("blank" , blankFields (mergedClaims )),
1249
- )
1250
- } else if ! strings .Contains (err .Error (), "user info endpoint is not supported by this provider" ) {
1251
- logger .Error (ctx , "oauth2: unable to obtain user information claims" , slog .Error (err ))
1252
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1253
- Message : "Failed to obtain user information claims." ,
1254
- Detail : "The attempt to fetch claims via the UserInfo endpoint failed: " + err .Error (),
1255
- })
1240
+ // The precedence ordering is userInfoClaims > idTokenClaims.
1241
+ // Note: Unsure why exactly this is the case. idTokenClaims feels more
1242
+ // important?
1243
+ mergedClaims = mergeClaims (idtokenClaims , supplementaryClaims )
1244
+ case MergedClaimsSourceAccessToken :
1245
+ supplementaryClaims , ok = api .accessTokenClaims (ctx , rw , state , logger )
1246
+ if ! ok {
1256
1247
return
1257
- } else {
1258
- // The OIDC provider does not support the UserInfo endpoint.
1259
- // This is not an error, but we should log it as it may mean
1260
- // that some claims are missing.
1261
- logger .Warn (ctx , "OIDC provider does not support the user info endpoint, ensure that all required claims are present in the id_token" )
1262
1248
}
1249
+ // idTokenClaims take priority over accessTokenClaims. The order should
1250
+ // not matter. It is just safer to assume idTokenClaims is the truth,
1251
+ // and accessTokenClaims are supplemental.
1252
+ mergedClaims = mergeClaims (supplementaryClaims , idtokenClaims )
1253
+ case MergedClaimsSourceNone :
1254
+ // noop, keep the userInfoClaims empty
1255
+ default :
1256
+ // This should never happen and is a developer error
1257
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1258
+ Message : "Invalid source for secondary user claims." ,
1259
+ Detail : fmt .Sprintf ("invalid source: %q" , api .OIDCConfig .SecondaryClaims ),
1260
+ })
1261
+ return // Invalid MergedClaimsSource
1263
1262
}
1264
1263
1265
1264
usernameRaw , ok := mergedClaims [api .OIDCConfig .UsernameField ]
@@ -1413,7 +1412,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1413
1412
RoleSync : roleSync ,
1414
1413
UserClaims : database.UserLinkClaims {
1415
1414
IDTokenClaims : idtokenClaims ,
1416
- UserInfoClaims : userInfoClaims ,
1415
+ UserInfoClaims : supplementaryClaims ,
1417
1416
MergedClaims : mergedClaims ,
1418
1417
},
1419
1418
}).SetInitAuditRequest (func (params * audit.RequestParams ) (* audit.Request [database.User ], func ()) {
@@ -1447,6 +1446,68 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1447
1446
http .Redirect (rw , r , redirect , http .StatusTemporaryRedirect )
1448
1447
}
1449
1448
1449
+ func (api * API ) accessTokenClaims (ctx context.Context , rw http.ResponseWriter , state httpmw.OAuth2State , logger slog.Logger ) (accessTokenClaims map [string ]interface {}, ok bool ) {
1450
+ // Assume the access token is a jwt, and signed by the provider.
1451
+ accessToken , err := api .OIDCConfig .Verifier .Verify (ctx , state .Token .AccessToken )
1452
+ if err != nil {
1453
+ logger .Error (ctx , "oauth2: unable to verify access token as secondary claims source" , slog .Error (err ))
1454
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1455
+ Message : "Failed to verify access token." ,
1456
+ Detail : fmt .Sprintf ("sourcing secondary claims from access token: %s" , err .Error ()),
1457
+ })
1458
+ return nil , false
1459
+ }
1460
+
1461
+ rawClaims := make (map [string ]any )
1462
+ err = accessToken .Claims (& rawClaims )
1463
+ if err != nil {
1464
+ logger .Error (ctx , "oauth2: unable to unmarshal access token claims" , slog .Error (err ))
1465
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1466
+ Message : "Failed to unmarshal access token claims." ,
1467
+ Detail : err .Error (),
1468
+ })
1469
+ return nil , false
1470
+ }
1471
+
1472
+ return rawClaims , true
1473
+ }
1474
+
1475
+ func (api * API ) userInfoClaims (ctx context.Context , rw http.ResponseWriter , state httpmw.OAuth2State , logger slog.Logger ) (userInfoClaims map [string ]interface {}, ok bool ) {
1476
+ userInfoClaims = make (map [string ]interface {})
1477
+ userInfo , err := api .OIDCConfig .Provider .UserInfo (ctx , oauth2 .StaticTokenSource (state .Token ))
1478
+ if err == nil {
1479
+ err = userInfo .Claims (& userInfoClaims )
1480
+ if err != nil {
1481
+ logger .Error (ctx , "oauth2: unable to unmarshal user info claims" , slog .Error (err ))
1482
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1483
+ Message : "Failed to unmarshal user info claims." ,
1484
+ Detail : err .Error (),
1485
+ })
1486
+ return nil , false
1487
+ }
1488
+ logger .Debug (ctx , "got oidc claims" ,
1489
+ slog .F ("source" , "userinfo" ),
1490
+ slog .F ("claim_fields" , claimFields (userInfoClaims )),
1491
+ slog .F ("blank" , blankFields (userInfoClaims )),
1492
+ )
1493
+ } else if ! strings .Contains (err .Error (), "user info endpoint is not supported by this provider" ) {
1494
+ logger .Error (ctx , "oauth2: unable to obtain user information claims" , slog .Error (err ))
1495
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1496
+ Message : "Failed to obtain user information claims." ,
1497
+ Detail : "The attempt to fetch claims via the UserInfo endpoint failed: " + err .Error (),
1498
+ })
1499
+ return nil , false
1500
+ } else {
1501
+ // The OIDC provider does not support the UserInfo endpoint.
1502
+ // This is not an error, but we should log it as it may mean
1503
+ // that some claims are missing.
1504
+ logger .Warn (ctx , "OIDC provider does not support the user info endpoint, ensure that all required claims are present in the id_token" ,
1505
+ slog .Error (err ),
1506
+ )
1507
+ }
1508
+ return userInfoClaims , true
1509
+ }
1510
+
1450
1511
// claimFields returns the sorted list of fields in the claims map.
1451
1512
func claimFields (claims map [string ]interface {}) []string {
1452
1513
fields := []string {}
0 commit comments