Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc522c7b

Browse files
Emyrkaslilac
authored andcommitted
feat: add sourcing secondary claims from access_token (#16517)
Niche edge case, assumes access_token is jwt. Some `access_token`s are JWT's with potential useful claims.These claims would be nearly equivalent to `user_info` claims.This is not apart of the oauth spec, so this feature should not beloudly advertised. If using this feature, alternate solutions are preferred.
1 parent7ad2366 commitc522c7b

File tree

12 files changed

+281
-99
lines changed

12 files changed

+281
-99
lines changed

‎cli/server.go‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,17 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De
172172
groupAllowList[group]=true
173173
}
174174

175+
secondaryClaimsSrc:=coderd.MergedClaimsSourceUserInfo
176+
if!vals.OIDC.IgnoreUserInfo&&vals.OIDC.UserInfoFromAccessToken {
177+
returnnil,xerrors.Errorf("to use 'oidc-access-token-claims', 'oidc-ignore-userinfo' must be set to 'false'")
178+
}
179+
ifvals.OIDC.IgnoreUserInfo {
180+
secondaryClaimsSrc=coderd.MergedClaimsSourceNone
181+
}
182+
ifvals.OIDC.UserInfoFromAccessToken {
183+
secondaryClaimsSrc=coderd.MergedClaimsSourceAccessToken
184+
}
185+
175186
return&coderd.OIDCConfig{
176187
OAuth2Config:useCfg,
177188
Provider:oidcProvider,
@@ -187,7 +198,7 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De
187198
NameField:vals.OIDC.NameField.String(),
188199
EmailField:vals.OIDC.EmailField.String(),
189200
AuthURLParams:vals.OIDC.AuthURLParams.Value,
190-
IgnoreUserInfo:vals.OIDC.IgnoreUserInfo.Value(),
201+
SecondaryClaims:secondaryClaimsSrc,
191202
SignInText:vals.OIDC.SignInText.String(),
192203
SignupsDisabledText:vals.OIDC.SignupsDisabledText.String(),
193204
IconURL:vals.OIDC.IconURL.String(),

‎cli/testdata/server-config.yaml.golden‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,12 @@ oidc:
329329
# Ignore the userinfo endpoint and only use the ID token for user information.
330330
# (default: false, type: bool)
331331
ignoreUserInfo: false
332+
# Source supplemental user claims from the 'access_token'. This assumes the token
333+
# is a jwt signed by the same issuer as the id_token. Using this requires setting
334+
# 'oidc-ignore-userinfo' to true. This setting is not compliant with the OIDC
335+
# specification and is not recommended. Use at your own risk.
336+
# (default: false, type: bool)
337+
accessTokenClaims: false
332338
# This field must be set if using the organization sync feature. Set to the claim
333339
# to be used for organizations.
334340
# (default: <unset>, type: string)

‎coderd/apidoc/docs.go‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/apidoc/swagger.json‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/coderdtest/oidctest/idp.go‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ type FakeIDP struct {
105105
// "Authorized Redirect URLs". This can be used to emulate that.
106106
hookValidRedirectURLfunc(redirectURLstring)error
107107
hookUserInfofunc(emailstring) (jwt.MapClaims,error)
108+
hookAccessTokenJWTfunc(emailstring,exp time.Time) jwt.MapClaims
108109
// defaultIDClaims is if a new client connects and we didn't preset
109110
// some claims.
110111
defaultIDClaims jwt.MapClaims
@@ -154,6 +155,12 @@ func WithMiddlewares(mws ...func(http.Handler) http.Handler) func(*FakeIDP) {
154155
}
155156
}
156157

158+
funcWithAccessTokenJWTHook(hookfunc(emailstring,exp time.Time) jwt.MapClaims)func(*FakeIDP) {
159+
returnfunc(f*FakeIDP) {
160+
f.hookAccessTokenJWT=hook
161+
}
162+
}
163+
157164
funcWithHookWellKnown(hookfunc(r*http.Request,j*ProviderJSON)error)func(*FakeIDP) {
158165
returnfunc(f*FakeIDP) {
159166
f.hookWellKnown=hook
@@ -316,8 +323,7 @@ const (
316323
funcNewFakeIDP(t testing.TB,opts...FakeIDPOpt)*FakeIDP {
317324
t.Helper()
318325

319-
block,_:=pem.Decode([]byte(testRSAPrivateKey))
320-
pkey,err:=x509.ParsePKCS1PrivateKey(block.Bytes)
326+
pkey,err:=FakeIDPKey()
321327
require.NoError(t,err)
322328

323329
idp:=&FakeIDP{
@@ -676,8 +682,13 @@ func (f *FakeIDP) newCode(state string) string {
676682

677683
// newToken enforces the access token exchanged is actually a valid access token
678684
// created by the IDP.
679-
func (f*FakeIDP)newToken(emailstring,expires time.Time)string {
685+
func (f*FakeIDP)newToken(t testing.TB,emailstring,expires time.Time)string {
680686
accessToken:=uuid.NewString()
687+
iff.hookAccessTokenJWT!=nil {
688+
claims:=f.hookAccessTokenJWT(email,expires)
689+
accessToken=f.encodeClaims(t,claims)
690+
}
691+
681692
f.accessTokens.Store(accessToken,token{
682693
issued:time.Now(),
683694
email:email,
@@ -963,7 +974,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
963974
email:=getEmail(claims)
964975
refreshToken:=f.newRefreshTokens(email)
965976
token:=map[string]interface{}{
966-
"access_token":f.newToken(email,exp),
977+
"access_token":f.newToken(t,email,exp),
967978
"refresh_token":refreshToken,
968979
"token_type":"Bearer",
969980
"expires_in":int64((f.defaultExpire).Seconds()),
@@ -1465,9 +1476,10 @@ func (f *FakeIDP) internalOIDCConfig(ctx context.Context, t testing.TB, scopes [
14651476
Verifier:oidc.NewVerifier(f.provider.Issuer,&oidc.StaticKeySet{
14661477
PublicKeys: []crypto.PublicKey{f.key.Public()},
14671478
},verifierConfig),
1468-
UsernameField:"preferred_username",
1469-
EmailField:"email",
1470-
AuthURLParams:map[string]string{"access_type":"offline"},
1479+
UsernameField:"preferred_username",
1480+
EmailField:"email",
1481+
AuthURLParams:map[string]string{"access_type":"offline"},
1482+
SecondaryClaims:coderd.MergedClaimsSourceUserInfo,
14711483
}
14721484

14731485
for_,opt:=rangeopts {
@@ -1552,3 +1564,8 @@ d8h4Ht09E+f3nhTEc87mODkl7WJZpHL6V2sORfeq/eIkds+H6CJ4hy5w/bSw8tjf
15521564
sz9Di8sGIaUbLZI2rd0CQQCzlVwEtRtoNCyMJTTrkgUuNufLP19RZ5FpyXxBO5/u
15531565
QastnN77KfUwdj3SJt44U/uh1jAIv4oSLBr8HYUkbnI8
15541566
-----END RSA PRIVATE KEY-----`
1567+
1568+
funcFakeIDPKey() (*rsa.PrivateKey,error) {
1569+
block,_:=pem.Decode([]byte(testRSAPrivateKey))
1570+
returnx509.ParsePKCS1PrivateKey(block.Bytes)
1571+
}

‎coderd/userauth.go‎

Lines changed: 106 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ import (
4646
"github.com/coder/coder/v2/cryptorand"
4747
)
4848

49+
typeMergedClaimsSourcestring
50+
51+
var (
52+
MergedClaimsSourceNoneMergedClaimsSource="none"
53+
MergedClaimsSourceUserInfoMergedClaimsSource="user_info"
54+
MergedClaimsSourceAccessTokenMergedClaimsSource="access_token"
55+
)
56+
4957
const (
5058
userAuthLoggerName="userauth"
5159
OAuthConvertCookieValue="coder_oauth_convert_jwt"
@@ -1116,11 +1124,13 @@ type OIDCConfig struct {
11161124
// AuthURLParams are additional parameters to be passed to the OIDC provider
11171125
// when requesting an access token.
11181126
AuthURLParamsmap[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-
IgnoreUserInfobool
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+
SecondaryClaimsMergedClaimsSource
11241134
// SignInText is the text to display on the OIDC login button
11251135
SignInTextstring
11261136
// 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) {
12161226
// Some providers (e.g. ADFS) do not support custom OIDC claims in the
12171227
// UserInfo endpoint, so we allow users to disable it and only rely on the
12181228
// ID token.
1219-
userInfoClaims:=make(map[string]interface{})
1229+
//
12201230
// If user info is skipped, the idtokenClaims are the claims.
12211231
mergedClaims:=idtokenClaims
1222-
if!api.OIDCConfig.IgnoreUserInfo {
1223-
userInfo,err:=api.OIDCConfig.Provider.UserInfo(ctx,oauth2.StaticTokenSource(state.Token))
1224-
iferr==nil {
1225-
err=userInfo.Claims(&userInfoClaims)
1226-
iferr!=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+
switchapi.OIDCConfig.SecondaryClaims {
1234+
caseMergedClaimsSourceUserInfo:
1235+
supplementaryClaims,ok=api.userInfoClaims(ctx,rw,state,logger)
1236+
if!ok {
1237+
return
1238+
}
12431239

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-
}elseif!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+
caseMergedClaimsSourceAccessToken:
1245+
supplementaryClaims,ok=api.accessTokenClaims(ctx,rw,state,logger)
1246+
if!ok {
12561247
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")
12621248
}
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+
caseMergedClaimsSourceNone:
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
12631262
}
12641263

12651264
usernameRaw,ok:=mergedClaims[api.OIDCConfig.UsernameField]
@@ -1413,7 +1412,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
14131412
RoleSync:roleSync,
14141413
UserClaims: database.UserLinkClaims{
14151414
IDTokenClaims:idtokenClaims,
1416-
UserInfoClaims:userInfoClaims,
1415+
UserInfoClaims:supplementaryClaims,
14171416
MergedClaims:mergedClaims,
14181417
},
14191418
}).SetInitAuditRequest(func(params*audit.RequestParams) (*audit.Request[database.User],func()) {
@@ -1447,6 +1446,68 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
14471446
http.Redirect(rw,r,redirect,http.StatusTemporaryRedirect)
14481447
}
14491448

1449+
func (api*API)accessTokenClaims(ctx context.Context,rw http.ResponseWriter,state httpmw.OAuth2State,logger slog.Logger) (accessTokenClaimsmap[string]interface{},okbool) {
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+
iferr!=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+
returnnil,false
1459+
}
1460+
1461+
rawClaims:=make(map[string]any)
1462+
err=accessToken.Claims(&rawClaims)
1463+
iferr!=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+
returnnil,false
1470+
}
1471+
1472+
returnrawClaims,true
1473+
}
1474+
1475+
func (api*API)userInfoClaims(ctx context.Context,rw http.ResponseWriter,state httpmw.OAuth2State,logger slog.Logger) (userInfoClaimsmap[string]interface{},okbool) {
1476+
userInfoClaims=make(map[string]interface{})
1477+
userInfo,err:=api.OIDCConfig.Provider.UserInfo(ctx,oauth2.StaticTokenSource(state.Token))
1478+
iferr==nil {
1479+
err=userInfo.Claims(&userInfoClaims)
1480+
iferr!=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+
returnnil,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+
}elseif!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+
returnnil,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+
returnuserInfoClaims,true
1509+
}
1510+
14501511
// claimFields returns the sorted list of fields in the claims map.
14511512
funcclaimFields(claimsmap[string]interface{}) []string {
14521513
fields:= []string{}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp