@@ -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"
@@ -1042,11 +1050,13 @@ type OIDCConfig struct {
1042
1050
// AuthURLParams are additional parameters to be passed to the OIDC provider
1043
1051
// when requesting an access token.
1044
1052
AuthURLParams map [string ]string
1045
- // IgnoreUserInfo causes Coder to only use claims from the ID token to
1046
- // process OIDC logins. This is useful if the OIDC provider does not
1047
- // support the userinfo endpoint, or if the userinfo endpoint causes
1048
- // undesirable behavior.
1049
- IgnoreUserInfo bool
1053
+ // SecondaryClaims indicates where to source additional claim information from.
1054
+ // The standard is either 'MergedClaimsSourceNone' or 'MergedClaimsSourceUserInfo'.
1055
+ //
1056
+ // The OIDC compliant way is to use the userinfo endpoint. This option
1057
+ // is useful when the userinfo endpoint does not exist or causes undesirable
1058
+ // behavior.
1059
+ SecondaryClaims MergedClaimsSource
1050
1060
// SignInText is the text to display on the OIDC login button
1051
1061
SignInText string
1052
1062
// IconURL points to the URL of an icon to display on the OIDC login button
@@ -1112,20 +1122,6 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1112
1122
return
1113
1123
}
1114
1124
1115
- if idToken .Subject == "" {
1116
- logger .Error (ctx , "oauth2: missing 'sub' claim field in OIDC token" ,
1117
- slog .F ("source" , "id_token" ),
1118
- slog .F ("claim_fields" , claimFields (idtokenClaims )),
1119
- slog .F ("blank" , blankFields (idtokenClaims )),
1120
- )
1121
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1122
- Message : "OIDC token missing 'sub' claim field or 'sub' claim field is empty." ,
1123
- Detail : "'sub' claim field is required to be unique for all users by a given issue, " +
1124
- "an empty field is invalid and this authentication attempt is rejected." ,
1125
- })
1126
- return
1127
- }
1128
-
1129
1125
logger .Debug (ctx , "got oidc claims" ,
1130
1126
slog .F ("source" , "id_token" ),
1131
1127
slog .F ("claim_fields" , claimFields (idtokenClaims )),
@@ -1142,50 +1138,39 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1142
1138
// Some providers (e.g. ADFS) do not support custom OIDC claims in the
1143
1139
// UserInfo endpoint, so we allow users to disable it and only rely on the
1144
1140
// ID token.
1145
- userInfoClaims := make ( map [ string ] interface {})
1141
+ //
1146
1142
// If user info is skipped, the idtokenClaims are the claims.
1147
1143
mergedClaims := idtokenClaims
1148
- if ! api .OIDCConfig .IgnoreUserInfo {
1149
- userInfo , err := api .OIDCConfig .Provider .UserInfo (ctx , oauth2 .StaticTokenSource (state .Token ))
1150
- if err == nil {
1151
- err = userInfo .Claims (& userInfoClaims )
1152
- if err != nil {
1153
- logger .Error (ctx , "oauth2: unable to unmarshal user info claims" , slog .Error (err ))
1154
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1155
- Message : "Failed to unmarshal user info claims." ,
1156
- Detail : err .Error (),
1157
- })
1158
- return
1159
- }
1160
- logger .Debug (ctx , "got oidc claims" ,
1161
- slog .F ("source" , "userinfo" ),
1162
- slog .F ("claim_fields" , claimFields (userInfoClaims )),
1163
- slog .F ("blank" , blankFields (userInfoClaims )),
1164
- )
1165
-
1166
- // Merge the claims from the ID token and the UserInfo endpoint.
1167
- // Information from UserInfo takes precedence.
1168
- mergedClaims = mergeClaims (idtokenClaims , userInfoClaims )
1144
+ supplementaryClaims := make (map [string ]interface {})
1145
+ switch api .OIDCConfig .SecondaryClaims {
1146
+ case MergedClaimsSourceUserInfo :
1147
+ supplementaryClaims , ok = api .userInfoClaims (ctx , rw , state , logger )
1148
+ if ! ok {
1149
+ return
1150
+ }
1169
1151
1170
- // Log all of the field names after merging.
1171
- logger .Debug (ctx , "got oidc claims" ,
1172
- slog .F ("source" , "merged" ),
1173
- slog .F ("claim_fields" , claimFields (mergedClaims )),
1174
- slog .F ("blank" , blankFields (mergedClaims )),
1175
- )
1176
- } else if ! strings .Contains (err .Error (), "user info endpoint is not supported by this provider" ) {
1177
- logger .Error (ctx , "oauth2: unable to obtain user information claims" , slog .Error (err ))
1178
- httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1179
- Message : "Failed to obtain user information claims." ,
1180
- Detail : "The attempt to fetch claims via the UserInfo endpoint failed: " + err .Error (),
1181
- })
1152
+ // The precedence ordering is userInfoClaims > idTokenClaims.
1153
+ // Note: Unsure why exactly this is the case. idTokenClaims feels more
1154
+ // important?
1155
+ mergedClaims = mergeClaims (idtokenClaims , supplementaryClaims )
1156
+ case MergedClaimsSourceAccessToken :
1157
+ supplementaryClaims , ok = api .accessTokenClaims (ctx , rw , state , logger )
1158
+ if ! ok {
1182
1159
return
1183
- } else {
1184
- // The OIDC provider does not support the UserInfo endpoint.
1185
- // This is not an error, but we should log it as it may mean
1186
- // that some claims are missing.
1187
- logger .Warn (ctx , "OIDC provider does not support the user info endpoint, ensure that all required claims are present in the id_token" )
1188
1160
}
1161
+ // idTokenClaims take priority over accessTokenClaims. The order should
1162
+ // not matter. It is just safer to assume idTokenClaims is the truth,
1163
+ // and accessTokenClaims are supplemental.
1164
+ mergedClaims = mergeClaims (supplementaryClaims , idtokenClaims )
1165
+ case MergedClaimsSourceNone :
1166
+ // noop, keep the userInfoClaims empty
1167
+ default :
1168
+ // This should never happen and is a developer error
1169
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1170
+ Message : "Invalid source for secondary user claims." ,
1171
+ Detail : fmt .Sprintf ("invalid source: %q" , api .OIDCConfig .SecondaryClaims ),
1172
+ })
1173
+ return // Invalid MergedClaimsSource
1189
1174
}
1190
1175
1191
1176
usernameRaw , ok := mergedClaims [api .OIDCConfig .UsernameField ]
@@ -1339,7 +1324,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1339
1324
RoleSync : roleSync ,
1340
1325
UserClaims : database.UserLinkClaims {
1341
1326
IDTokenClaims : idtokenClaims ,
1342
- UserInfoClaims : userInfoClaims ,
1327
+ UserInfoClaims : supplementaryClaims ,
1343
1328
MergedClaims : mergedClaims ,
1344
1329
},
1345
1330
}).SetInitAuditRequest (func (params * audit.RequestParams ) (* audit.Request [database.User ], func ()) {
@@ -1373,6 +1358,68 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1373
1358
http .Redirect (rw , r , redirect , http .StatusTemporaryRedirect )
1374
1359
}
1375
1360
1361
+ func (api * API ) accessTokenClaims (ctx context.Context , rw http.ResponseWriter , state httpmw.OAuth2State , logger slog.Logger ) (accessTokenClaims map [string ]interface {}, ok bool ) {
1362
+ // Assume the access token is a jwt, and signed by the provider.
1363
+ accessToken , err := api .OIDCConfig .Verifier .Verify (ctx , state .Token .AccessToken )
1364
+ if err != nil {
1365
+ logger .Error (ctx , "oauth2: unable to verify access token as secondary claims source" , slog .Error (err ))
1366
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1367
+ Message : "Failed to verify access token." ,
1368
+ Detail : fmt .Sprintf ("sourcing secondary claims from access token: %s" , err .Error ()),
1369
+ })
1370
+ return nil , false
1371
+ }
1372
+
1373
+ rawClaims := make (map [string ]any )
1374
+ err = accessToken .Claims (& rawClaims )
1375
+ if err != nil {
1376
+ logger .Error (ctx , "oauth2: unable to unmarshal access token claims" , slog .Error (err ))
1377
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1378
+ Message : "Failed to unmarshal access token claims." ,
1379
+ Detail : err .Error (),
1380
+ })
1381
+ return nil , false
1382
+ }
1383
+
1384
+ return rawClaims , true
1385
+ }
1386
+
1387
+ func (api * API ) userInfoClaims (ctx context.Context , rw http.ResponseWriter , state httpmw.OAuth2State , logger slog.Logger ) (userInfoClaims map [string ]interface {}, ok bool ) {
1388
+ userInfoClaims = make (map [string ]interface {})
1389
+ userInfo , err := api .OIDCConfig .Provider .UserInfo (ctx , oauth2 .StaticTokenSource (state .Token ))
1390
+ if err == nil {
1391
+ err = userInfo .Claims (& userInfoClaims )
1392
+ if err != nil {
1393
+ logger .Error (ctx , "oauth2: unable to unmarshal user info claims" , slog .Error (err ))
1394
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1395
+ Message : "Failed to unmarshal user info claims." ,
1396
+ Detail : err .Error (),
1397
+ })
1398
+ return nil , false
1399
+ }
1400
+ logger .Debug (ctx , "got oidc claims" ,
1401
+ slog .F ("source" , "userinfo" ),
1402
+ slog .F ("claim_fields" , claimFields (userInfoClaims )),
1403
+ slog .F ("blank" , blankFields (userInfoClaims )),
1404
+ )
1405
+ } else if ! strings .Contains (err .Error (), "user info endpoint is not supported by this provider" ) {
1406
+ logger .Error (ctx , "oauth2: unable to obtain user information claims" , slog .Error (err ))
1407
+ httpapi .Write (ctx , rw , http .StatusInternalServerError , codersdk.Response {
1408
+ Message : "Failed to obtain user information claims." ,
1409
+ Detail : "The attempt to fetch claims via the UserInfo endpoint failed: " + err .Error (),
1410
+ })
1411
+ return nil , false
1412
+ } else {
1413
+ // The OIDC provider does not support the UserInfo endpoint.
1414
+ // This is not an error, but we should log it as it may mean
1415
+ // that some claims are missing.
1416
+ logger .Warn (ctx , "OIDC provider does not support the user info endpoint, ensure that all required claims are present in the id_token" ,
1417
+ slog .Error (err ),
1418
+ )
1419
+ }
1420
+ return userInfoClaims , true
1421
+ }
1422
+
1376
1423
// claimFields returns the sorted list of fields in the claims map.
1377
1424
func claimFields (claims map [string ]interface {}) []string {
1378
1425
fields := []string {}
0 commit comments