Skip to content

Commit bbc1a9a

Browse files
authored
fix: use UserInfo endpoint with OIDC (#5735)
This resolves a user issue surfaced in Discord: https://discord.com/channels/747933592273027093/1064566338875576361/1064566338875576361 Both methods of obtaining claims need to be used according to the OIDC specification.
1 parent 592ce3b commit bbc1a9a

File tree

4 files changed

+89
-24
lines changed

4 files changed

+89
-24
lines changed

cli/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
543543
Endpoint: oidcProvider.Endpoint(),
544544
Scopes: cfg.OIDC.Scopes.Value,
545545
},
546+
Provider: oidcProvider,
546547
Verifier: oidcProvider.Verifier(&oidc.Config{
547548
ClientID: cfg.OIDC.ClientID.Value,
548549
}),

coderd/coderdtest/coderdtest.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,14 +887,31 @@ func (o *OIDCConfig) EncodeClaims(t *testing.T, claims jwt.MapClaims) string {
887887
return base64.StdEncoding.EncodeToString([]byte(signed))
888888
}
889889

890-
func (o *OIDCConfig) OIDCConfig() *coderd.OIDCConfig {
890+
func (o *OIDCConfig) OIDCConfig(t *testing.T, userInfoClaims jwt.MapClaims) *coderd.OIDCConfig {
891+
// By default, the provider can be empty.
892+
// This means it won't support any endpoints!
893+
provider := &oidc.Provider{}
894+
if userInfoClaims != nil {
895+
resp, err := json.Marshal(userInfoClaims)
896+
require.NoError(t, err)
897+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
898+
w.WriteHeader(http.StatusOK)
899+
_, _ = w.Write(resp)
900+
}))
901+
t.Cleanup(srv.Close)
902+
cfg := &oidc.ProviderConfig{
903+
UserInfoURL: srv.URL,
904+
}
905+
provider = cfg.NewProvider(context.Background())
906+
}
891907
return &coderd.OIDCConfig{
892908
OAuth2Config: o,
893909
Verifier: oidc.NewVerifier(o.issuer, &oidc.StaticKeySet{
894910
PublicKeys: []crypto.PublicKey{o.key.Public()},
895911
}, &oidc.Config{
896912
SkipClientIDCheck: true,
897913
}),
914+
Provider: provider,
898915
UsernameField: "preferred_username",
899916
}
900917
}

coderd/userauth.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
204204
type OIDCConfig struct {
205205
httpmw.OAuth2Config
206206

207+
Provider *oidc.Provider
207208
Verifier *oidc.IDTokenVerifier
208209
// EmailDomains are the domains to enforce when a user authenticates.
209210
EmailDomain []string
@@ -258,6 +259,35 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
258259
})
259260
return
260261
}
262+
263+
// Not all claims are necessarily embedded in the `id_token`.
264+
// In GitLab, the username is left empty and must be fetched in UserInfo.
265+
//
266+
// The OIDC specification says claims can be in either place, so we fetch
267+
// user info and merge the two claim sets to be sure we have all of
268+
// the correct data.
269+
userInfo, err := api.OIDCConfig.Provider.UserInfo(ctx, oauth2.StaticTokenSource(state.Token))
270+
if err == nil {
271+
userInfoClaims := map[string]interface{}{}
272+
err = userInfo.Claims(&userInfoClaims)
273+
if err != nil {
274+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
275+
Message: "Failed to unmarshal user info claims.",
276+
Detail: err.Error(),
277+
})
278+
return
279+
}
280+
for k, v := range userInfoClaims {
281+
claims[k] = v
282+
}
283+
} else if !strings.Contains(err.Error(), "user info endpoint is not supported by this provider") {
284+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
285+
Message: "Failed to obtain user information claims.",
286+
Detail: "The OIDC provider returned no claims as part of the `id_token`. The attempt to fetch claims via the UserInfo endpoint failed: " + err.Error(),
287+
})
288+
return
289+
}
290+
261291
usernameRaw, ok := claims[api.OIDCConfig.UsernameField]
262292
var username string
263293
if ok {

coderd/userauth_test.go

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,8 @@ func TestUserOIDC(t *testing.T) {
480480

481481
for _, tc := range []struct {
482482
Name string
483-
Claims jwt.MapClaims
483+
IDTokenClaims jwt.MapClaims
484+
UserInfoClaims jwt.MapClaims
484485
AllowSignups bool
485486
EmailDomain []string
486487
Username string
@@ -489,31 +490,31 @@ func TestUserOIDC(t *testing.T) {
489490
IgnoreEmailVerified bool
490491
}{{
491492
Name: "EmailOnly",
492-
Claims: jwt.MapClaims{
493+
IDTokenClaims: jwt.MapClaims{
493494
"email": "kyle@kwc.io",
494495
},
495496
AllowSignups: true,
496497
StatusCode: http.StatusTemporaryRedirect,
497498
Username: "kyle",
498499
}, {
499500
Name: "EmailNotVerified",
500-
Claims: jwt.MapClaims{
501+
IDTokenClaims: jwt.MapClaims{
501502
"email": "kyle@kwc.io",
502503
"email_verified": false,
503504
},
504505
AllowSignups: true,
505506
StatusCode: http.StatusForbidden,
506507
}, {
507508
Name: "EmailNotAString",
508-
Claims: jwt.MapClaims{
509+
IDTokenClaims: jwt.MapClaims{
509510
"email": 3.14159,
510511
"email_verified": false,
511512
},
512513
AllowSignups: true,
513514
StatusCode: http.StatusBadRequest,
514515
}, {
515516
Name: "EmailNotVerifiedIgnored",
516-
Claims: jwt.MapClaims{
517+
IDTokenClaims: jwt.MapClaims{
517518
"email": "kyle@kwc.io",
518519
"email_verified": false,
519520
},
@@ -523,7 +524,7 @@ func TestUserOIDC(t *testing.T) {
523524
IgnoreEmailVerified: true,
524525
}, {
525526
Name: "NotInRequiredEmailDomain",
526-
Claims: jwt.MapClaims{
527+
IDTokenClaims: jwt.MapClaims{
527528
"email": "kyle@kwc.io",
528529
"email_verified": true,
529530
},
@@ -534,7 +535,7 @@ func TestUserOIDC(t *testing.T) {
534535
StatusCode: http.StatusForbidden,
535536
}, {
536537
Name: "EmailDomainCaseInsensitive",
537-
Claims: jwt.MapClaims{
538+
IDTokenClaims: jwt.MapClaims{
538539
"email": "kyle@KWC.io",
539540
"email_verified": true,
540541
},
@@ -544,20 +545,20 @@ func TestUserOIDC(t *testing.T) {
544545
},
545546
StatusCode: http.StatusTemporaryRedirect,
546547
}, {
547-
Name: "EmptyClaims",
548-
Claims: jwt.MapClaims{},
549-
AllowSignups: true,
550-
StatusCode: http.StatusBadRequest,
548+
Name: "EmptyClaims",
549+
IDTokenClaims: jwt.MapClaims{},
550+
AllowSignups: true,
551+
StatusCode: http.StatusBadRequest,
551552
}, {
552553
Name: "NoSignups",
553-
Claims: jwt.MapClaims{
554+
IDTokenClaims: jwt.MapClaims{
554555
"email": "kyle@kwc.io",
555556
"email_verified": true,
556557
},
557558
StatusCode: http.StatusForbidden,
558559
}, {
559560
Name: "UsernameFromEmail",
560-
Claims: jwt.MapClaims{
561+
IDTokenClaims: jwt.MapClaims{
561562
"email": "kyle@kwc.io",
562563
"email_verified": true,
563564
},
@@ -566,7 +567,7 @@ func TestUserOIDC(t *testing.T) {
566567
StatusCode: http.StatusTemporaryRedirect,
567568
}, {
568569
Name: "UsernameFromClaims",
569-
Claims: jwt.MapClaims{
570+
IDTokenClaims: jwt.MapClaims{
570571
"email": "kyle@kwc.io",
571572
"email_verified": true,
572573
"preferred_username": "hotdog",
@@ -578,7 +579,7 @@ func TestUserOIDC(t *testing.T) {
578579
// Services like Okta return the email as the username:
579580
// https://developer.okta.com/docs/reference/api/oidc/#base-claims-always-present
580581
Name: "UsernameAsEmail",
581-
Claims: jwt.MapClaims{
582+
IDTokenClaims: jwt.MapClaims{
582583
"email": "kyle@kwc.io",
583584
"email_verified": true,
584585
"preferred_username": "kyle@kwc.io",
@@ -589,21 +590,35 @@ func TestUserOIDC(t *testing.T) {
589590
}, {
590591
// See: https://github.com/coder/coder/issues/4472
591592
Name: "UsernameIsEmail",
592-
Claims: jwt.MapClaims{
593+
IDTokenClaims: jwt.MapClaims{
593594
"preferred_username": "kyle@kwc.io",
594595
},
595596
Username: "kyle",
596597
AllowSignups: true,
597598
StatusCode: http.StatusTemporaryRedirect,
598599
}, {
599600
Name: "WithPicture",
600-
Claims: jwt.MapClaims{
601+
IDTokenClaims: jwt.MapClaims{
602+
"email": "kyle@kwc.io",
603+
"email_verified": true,
604+
"preferred_username": "kyle",
605+
"picture": "/example.png",
606+
},
607+
Username: "kyle",
608+
AllowSignups: true,
609+
AvatarURL: "/example.png",
610+
StatusCode: http.StatusTemporaryRedirect,
611+
}, {
612+
Name: "WithUserInfoClaims",
613+
IDTokenClaims: jwt.MapClaims{
601614
"email": "kyle@kwc.io",
602615
"email_verified": true,
603-
"username": "kyle",
604-
"picture": "/example.png",
605616
},
606-
Username: "kyle",
617+
UserInfoClaims: jwt.MapClaims{
618+
"preferred_username": "potato",
619+
"picture": "/example.png",
620+
},
621+
Username: "potato",
607622
AllowSignups: true,
608623
AvatarURL: "/example.png",
609624
StatusCode: http.StatusTemporaryRedirect,
@@ -613,15 +628,15 @@ func TestUserOIDC(t *testing.T) {
613628
t.Parallel()
614629
conf := coderdtest.NewOIDCConfig(t, "")
615630

616-
config := conf.OIDCConfig()
631+
config := conf.OIDCConfig(t, tc.UserInfoClaims)
617632
config.AllowSignups = tc.AllowSignups
618633
config.EmailDomain = tc.EmailDomain
619634
config.IgnoreEmailVerified = tc.IgnoreEmailVerified
620635

621636
client := coderdtest.New(t, &coderdtest.Options{
622637
OIDCConfig: config,
623638
})
624-
resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.Claims))
639+
resp := oidcCallback(t, client, conf.EncodeClaims(t, tc.IDTokenClaims))
625640
assert.Equal(t, tc.StatusCode, resp.StatusCode)
626641

627642
ctx, _ := testutil.Context(t)
@@ -647,7 +662,7 @@ func TestUserOIDC(t *testing.T) {
647662

648663
conf := coderdtest.NewOIDCConfig(t, "")
649664

650-
config := conf.OIDCConfig()
665+
config := conf.OIDCConfig(t, nil)
651666
config.AllowSignups = true
652667

653668
client := coderdtest.New(t, &coderdtest.Options{
@@ -705,6 +720,7 @@ func TestUserOIDC(t *testing.T) {
705720
verifier := oidc.NewVerifier("", &oidc.StaticKeySet{
706721
PublicKeys: []crypto.PublicKey{},
707722
}, &oidc.Config{})
723+
provider := &oidc.Provider{}
708724

709725
client := coderdtest.New(t, &coderdtest.Options{
710726
OIDCConfig: &coderd.OIDCConfig{
@@ -715,6 +731,7 @@ func TestUserOIDC(t *testing.T) {
715731
"id_token": "invalid",
716732
}),
717733
},
734+
Provider: provider,
718735
Verifier: verifier,
719736
},
720737
})

0 commit comments

Comments
 (0)