git.iko.soy/typeable/oauth2-proxy
on master help sign in
Integrate claim extractor into providers
commit967051314e5d84cb389aa33ca8e0ff624b43c7eb authorJoel Speed <joel.speed@hotmail.co.uk> dateSat 26 Jun 2021, 11:49 (5 years ago) parent537e596
15 files changed+213−734
providers/adfs.go+7−6 providers/adfs_test.go+2−4 providers/auth_test.go+12−1 providers/azure.go+10−9 providers/azure_test.go+24−15 providers/digitalocean.go+2 providers/facebook.go+1 providers/linkedin.go+2 providers/nextcloud.go+6−23 providers/nextcloud_test.go−72 providers/oidc.go+9−56 providers/oidc_test.go+10−333 providers/provider_data.go+55−87 providers/provider_data_test.go+73−112 providers/util.go−16
providers/adfs.go+7−6
@@ -103,16 +103,17 @@ func (p *ADFSProvider) RefreshSession(ctx context.Context, s *sessions.SessionSt
103103}
104104
105105func (p *ADFSProvider) fallbackUPN(ctx context.Context, s *sessions.SessionState) error {
106− idToken, err := p.Verifier.Verify(ctx, s.IDToken)
106+ claims, err := p.getClaimExtractor(s.IDToken, s.AccessToken)
107107 if err != nil {
108− return err
108+ return fmt.Errorf("could not extract claims: %v", err)
109109 }
110− claims, err := p.getClaims(idToken)
110+
111+ upn, found, err := claims.GetClaim(adfsUPNClaim)
111112 if err != nil {
112− return fmt.Errorf("couldn't extract claims from id_token (%v)", err)
113+ return fmt.Errorf("could not extract %s claim: %v", adfsUPNClaim, err)
113114 }
114− upn := claims.raw[adfsUPNClaim]
115− if upn != nil {
115+
116+ if found && fmt.Sprint(upn) != "" {
116117 s.Email = fmt.Sprint(upn)
117118 }
118119 return nil
providers/adfs_test.go+2−4
@@ -79,7 +79,7 @@ func testADFSBackend() *httptest.Server {
7979 {
8080 "access_token": "my_access_token",
8181 "id_token": "my_id_token",
82− "refresh_token": "my_refresh_token"
82+ "refresh_token": "my_refresh_token"
8383 }
8484 `
8585 userInfo := `
@@ -150,9 +150,7 @@ var _ = Describe("ADFS Provider Tests", func() {
150150 Context("with valid token", func() {
151151 It("should not throw an error", func() {
152152 rawIDToken, _ := newSignedTestIDToken(defaultIDToken)
153− idToken, err := p.Verifier.Verify(context.Background(), rawIDToken)
154− Expect(err).To(BeNil())
155− session, err := p.buildSessionFromClaims(idToken)
153+ session, err := p.buildSessionFromClaims(rawIDToken, "")
156154 Expect(err).To(BeNil())
157155 session.IDToken = rawIDToken
158156 err = p.EnrichSession(context.Background(), session)
providers/auth_test.go+12−1
@@ -15,9 +15,20 @@ func CreateAuthorizedSession() *sessions.SessionState {
1515}
1616
1717func IsAuthorizedInHeader(reqHeader http.Header) bool {
18− return reqHeader.Get("Authorization") == fmt.Sprintf("Bearer %s", authorizedAccessToken)
18+ return IsAuthorizedInHeaderWithToken(reqHeader, authorizedAccessToken)
19+}
20+
21+func IsAuthorizedInHeaderWithToken(reqHeader http.Header, token string) bool {
22+ return reqHeader.Get("Authorization") == fmt.Sprintf("Bearer %s", token)
1923}
2024
2125func IsAuthorizedInURL(reqURL *url.URL) bool {
2226 return reqURL.Query().Get("access_token") == authorizedAccessToken
2327}
28+
29+func isAuthorizedRefreshInURLWithToken(reqURL *url.URL, token string) bool {
30+ if token == "" {
31+ return false
32+ }
33+ return reqURL.Query().Get("refresh_token") == token
34+}
providers/azure.go+10−9
@@ -78,6 +78,7 @@ func NewAzureProvider(p *ProviderData) *AzureProvider {
7878 if p.ValidateURL == nil || p.ValidateURL.String() == "" {
7979 p.ValidateURL = p.ProfileURL
8080 }
81+ p.getAuthorizationHeaderFunc = makeAzureHeader
8182
8283 return &AzureProvider{
8384 ProviderData: p,
@@ -150,7 +151,7 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (*
150151 session.CreatedAtNow()
151152 session.SetExpiresOn(time.Unix(jsonResponse.ExpiresOn, 0))
152153
153− email, err := p.verifyTokenAndExtractEmail(ctx, session.IDToken)
154+ email, err := p.verifyTokenAndExtractEmail(ctx, session.IDToken, session.AccessToken)
154155
155156 // https://github.com/oauth2-proxy/oauth2-proxy/pull/914#issuecomment-782285814
156157 // https://github.com/AzureAD/azure-activedirectory-library-for-java/issues/117
@@ -163,7 +164,7 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (*
163164 }
164165
165166 if session.Email == "" {
166− email, err = p.verifyTokenAndExtractEmail(ctx, session.AccessToken)
167+ email, err = p.verifyTokenAndExtractEmail(ctx, session.AccessToken, session.AccessToken)
167168 if err == nil && email != "" {
168169 session.Email = email
169170 } else {
@@ -215,16 +216,16 @@ func (p *AzureProvider) prepareRedeem(redirectURL, code string) (url.Values, err
215216
216217// verifyTokenAndExtractEmail tries to extract email claim from either id_token or access token
217218// when oidc verifier is configured
218−func (p *AzureProvider) verifyTokenAndExtractEmail(ctx context.Context, token string) (string, error) {
219+func (p *AzureProvider) verifyTokenAndExtractEmail(ctx context.Context, rawIDToken string, accessToken string) (string, error) {
219220 email := ""
220221
221− if token != "" && p.Verifier != nil {
222− token, err := p.Verifier.Verify(ctx, token)
222+ if rawIDToken != "" && p.Verifier != nil {
223+ _, err := p.Verifier.Verify(ctx, rawIDToken)
223224 // due to issues mentioned above, id_token may not be signed by AAD
224225 if err == nil {
225− claims, err := p.getClaims(token)
226+ s, err := p.buildSessionFromClaims(rawIDToken, accessToken)
226227 if err == nil {
227− email = claims.Email
228+ email = s.Email
228229 } else {
229230 logger.Printf("unable to get claims from token: %v", err)
230231 }
@@ -287,7 +288,7 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
287288 s.CreatedAtNow()
288289 s.SetExpiresOn(time.Unix(jsonResponse.ExpiresOn, 0))
289290
290− email, err := p.verifyTokenAndExtractEmail(ctx, s.IDToken)
291+ email, err := p.verifyTokenAndExtractEmail(ctx, s.IDToken, s.AccessToken)
291292
292293 // https://github.com/oauth2-proxy/oauth2-proxy/pull/914#issuecomment-782285814
293294 // https://github.com/AzureAD/azure-activedirectory-library-for-java/issues/117
@@ -300,7 +301,7 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
300301 }
301302
302303 if s.Email == "" {
303− email, err = p.verifyTokenAndExtractEmail(ctx, s.AccessToken)
304+ email, err = p.verifyTokenAndExtractEmail(ctx, s.AccessToken, s.AccessToken)
304305 if err == nil && email != "" {
305306 s.Email = email
306307 } else {
providers/azure_test.go+24−15
@@ -13,9 +13,8 @@ import (
1313 "testing"
1414 "time"
1515
16+ "github.com/coreos/go-oidc/v3/oidc"
1617 "github.com/golang-jwt/jwt"
17−
18− oidc "github.com/coreos/go-oidc/v3/oidc"
1918 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
2019 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
2120
@@ -145,11 +144,11 @@ func TestAzureSetTenant(t *testing.T) {
145144 assert.Equal(t, "openid", p.Data().Scope)
146145}
147146
148−func testAzureBackend(payload string) *httptest.Server {
149− return testAzureBackendWithError(payload, false)
147+func testAzureBackend(payload string, accessToken, refreshToken string) *httptest.Server {
148+ return testAzureBackendWithError(payload, accessToken, refreshToken, false)
150149}
151150
152−func testAzureBackendWithError(payload string, injectError bool) *httptest.Server {
151+func testAzureBackendWithError(payload string, accessToken, refreshToken string, injectError bool) *httptest.Server {
153152 path := "/v1.0/me"
154153
155154 return httptest.NewServer(http.HandlerFunc(
@@ -163,7 +162,8 @@ func testAzureBackendWithError(payload string, injectError bool) *httptest.Serve
163162 w.WriteHeader(200)
164163 }
165164 w.Write([]byte(payload))
166− } else if !IsAuthorizedInHeader(r.Header) {
165+ } else if !IsAuthorizedInHeaderWithToken(r.Header, accessToken) &&
166+ !isAuthorizedRefreshInURLWithToken(r.URL, refreshToken) {
167167 w.WriteHeader(403)
168168 } else {
169169 w.WriteHeader(200)
@@ -224,7 +224,7 @@ func TestAzureProviderEnrichSession(t *testing.T) {
224224 host string
225225 )
226226 if testCase.PayloadFromAzureBackend != "" {
227− b = testAzureBackend(testCase.PayloadFromAzureBackend)
227+ b = testAzureBackend(testCase.PayloadFromAzureBackend, authorizedAccessToken, "")
228228 defer b.Close()
229229
230230 bURL, _ := url.Parse(b.URL)
@@ -319,7 +319,7 @@ func TestAzureProviderRedeem(t *testing.T) {
319319 payloadBytes, err := json.Marshal(payload)
320320 assert.NoError(t, err)
321321
322− b := testAzureBackendWithError(string(payloadBytes), testCase.InjectRedeemURLError)
322+ b := testAzureBackendWithError(string(payloadBytes), accessTokenString, testCase.RefreshToken, testCase.InjectRedeemURLError)
323323 defer b.Close()
324324
325325 bURL, _ := url.Parse(b.URL)
@@ -353,35 +353,44 @@ func TestAzureProviderProtectedResourceConfigured(t *testing.T) {
353353
354354func TestAzureProviderRefresh(t *testing.T) {
355355 email := "foo@example.com"
356+ subject := "foo"
356357 idToken := idTokenClaims{
357− StandardClaims: jwt.StandardClaims{Audience: "cd6d4fae-f6a6-4a34-8454-2c6b598e9532"},
358− Email: email}
358+ Email: email,
359+ StandardClaims: jwt.StandardClaims{
360+ Audience: "cd6d4fae-f6a6-4a34-8454-2c6b598e9532",
361+ Subject: subject,
362+ },
363+ }
359364 idTokenString, err := newSignedTestIDToken(idToken)
360365 assert.NoError(t, err)
366+
361367 timestamp, err := time.Parse(time.RFC3339, "3006-01-02T22:04:05Z")
362368 assert.NoError(t, err)
369+
370+ newAccessToken := "new_some_access_token"
363371 payload := azureOAuthPayload{
364372 IDToken: idTokenString,
365373 RefreshToken: "new_some_refresh_token",
366− AccessToken: "new_some_access_token",
374+ AccessToken: newAccessToken,
367375 ExpiresOn: timestamp.Unix(),
368376 }
369−
370377 payloadBytes, err := json.Marshal(payload)
371378 assert.NoError(t, err)
372− b := testAzureBackend(string(payloadBytes))
379+
380+ refreshToken := "some_refresh_token"
381+ b := testAzureBackend(string(payloadBytes), newAccessToken, refreshToken)
373382 defer b.Close()
374383 bURL, _ := url.Parse(b.URL)
375384 p := testAzureProvider(bURL.Host)
376385
377386 expires := time.Now().Add(time.Duration(-1) * time.Hour)
378− session := &sessions.SessionState{AccessToken: "some_access_token", RefreshToken: "some_refresh_token", IDToken: "some_id_token", ExpiresOn: &expires}
387+ session := &sessions.SessionState{AccessToken: "some_access_token", RefreshToken: refreshToken, IDToken: "some_id_token", ExpiresOn: &expires}
379388
380389 refreshed, err := p.RefreshSession(context.Background(), session)
381390 assert.Equal(t, nil, err)
382391 assert.True(t, refreshed)
383392 assert.NotEqual(t, session, nil)
384− assert.Equal(t, "new_some_access_token", session.AccessToken)
393+ assert.Equal(t, newAccessToken, session.AccessToken)
385394 assert.Equal(t, "new_some_refresh_token", session.RefreshToken)
386395 assert.Equal(t, idTokenString, session.IDToken)
387396 assert.Equal(t, email, session.Email)
providers/digitalocean.go+2
@@ -57,6 +57,8 @@ func NewDigitalOceanProvider(p *ProviderData) *DigitalOceanProvider {
5757 validateURL: digitalOceanDefaultProfileURL,
5858 scope: digitalOceanDefaultScope,
5959 })
60+ p.getAuthorizationHeaderFunc = makeOIDCHeader
61+
6062 return &DigitalOceanProvider{ProviderData: p}
6163}
6264
providers/facebook.go+1
@@ -58,6 +58,7 @@ func NewFacebookProvider(p *ProviderData) *FacebookProvider {
5858 validateURL: facebookDefaultProfileURL,
5959 scope: facebookDefaultScope,
6060 })
61+ p.getAuthorizationHeaderFunc = makeOIDCHeader
6162 return &FacebookProvider{ProviderData: p}
6263}
6364
providers/linkedin.go+2
@@ -65,6 +65,8 @@ func NewLinkedInProvider(p *ProviderData) *LinkedInProvider {
6565 validateURL: linkedinDefaultValidateURL,
6666 scope: linkedinDefaultScope,
6767 })
68+ p.getAuthorizationHeaderFunc = makeLinkedInHeader
69+
6870 return &LinkedInProvider{ProviderData: p}
6971}
7072
providers/nextcloud.go+6−23
@@ -1,13 +1,5 @@
11package providers
22
3−import (
4− "context"
5− "fmt"
6−
7− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
8− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
9−)
10−
113// NextcloudProvider represents an Nextcloud based Identity Provider
124type NextcloudProvider struct {
135 *ProviderData
@@ -20,20 +12,11 @@ const nextCloudProviderName = "Nextcloud"
2012// NewNextcloudProvider initiates a new NextcloudProvider
2113func NewNextcloudProvider(p *ProviderData) *NextcloudProvider {
2214 p.ProviderName = nextCloudProviderName
23− return &NextcloudProvider{ProviderData: p}
24−}
25−
26−// GetEmailAddress returns the Account email address
27−func (p *NextcloudProvider) GetEmailAddress(ctx context.Context, s *sessions.SessionState) (string, error) {
28− json, err := requests.New(p.ValidateURL.String()).
29− WithContext(ctx).
30− WithHeaders(makeOIDCHeader(s.AccessToken)).
31− Do().
32− UnmarshalJSON()
33− if err != nil {
34− return "", fmt.Errorf("error making request: %v", err)
15+ p.getAuthorizationHeaderFunc = makeOIDCHeader
16+ if p.EmailClaim == OIDCEmailClaim {
17+ // This implies the email claim has not been overridden, we should set a default
18+ // for this provider
19+ p.EmailClaim = "ocs.data.email"
3520 }
36−
37− email, err := json.Get("ocs").Get("data").Get("email").String()
38− return email, err
21+ return &NextcloudProvider{ProviderData: p}
3922}
providers/nextcloud_test.go−72
@@ -1,18 +1,13 @@
11package providers
22
33import (
4− "context"
5− "net/http"
6− "net/http/httptest"
74 "net/url"
85 "testing"
96
10− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
117 "github.com/stretchr/testify/assert"
128)
139
1410const formatJSON = "format=json"
15−const userPath = "/ocs/v2.php/cloud/user"
1611
1712func testNextcloudProvider(hostname string) *NextcloudProvider {
1813 p := NewNextcloudProvider(
@@ -32,23 +27,6 @@ func testNextcloudProvider(hostname string) *NextcloudProvider {
3227 return p
3328}
3429
35−func testNextcloudBackend(payload string) *httptest.Server {
36− path := userPath
37− query := formatJSON
38−
39− return httptest.NewServer(http.HandlerFunc(
40− func(w http.ResponseWriter, r *http.Request) {
41− if r.URL.Path != path || r.URL.RawQuery != query {
42− w.WriteHeader(404)
43− } else if !IsAuthorizedInHeader(r.Header) {
44− w.WriteHeader(403)
45− } else {
46− w.WriteHeader(200)
47− w.Write([]byte(payload))
48− }
49− }))
50−}
51−
5230func TestNextcloudProviderDefaults(t *testing.T) {
5331 p := testNextcloudProvider("")
5432 assert.NotEqual(t, nil, p)
@@ -87,53 +65,3 @@ func TestNextcloudProviderOverrides(t *testing.T) {
8765 assert.Equal(t, "https://example.com/test/ocs/v2.php/cloud/user?"+formatJSON,
8866 p.Data().ValidateURL.String())
8967}
90−
91−func TestNextcloudProviderGetEmailAddress(t *testing.T) {
92− b := testNextcloudBackend("{\"ocs\": {\"data\": { \"email\": \"michael.bland@gsa.gov\"}}}")
93− defer b.Close()
94−
95− bURL, _ := url.Parse(b.URL)
96− p := testNextcloudProvider(bURL.Host)
97− p.ValidateURL.Path = userPath
98− p.ValidateURL.RawQuery = formatJSON
99−
100− session := CreateAuthorizedSession()
101− email, err := p.GetEmailAddress(context.Background(), session)
102− assert.Equal(t, nil, err)
103− assert.Equal(t, "michael.bland@gsa.gov", email)
104−}
105−
106−// Note that trying to trigger the "failed building request" case is not
107−// practical, since the only way it can fail is if the URL fails to parse.
108−func TestNextcloudProviderGetEmailAddressFailedRequest(t *testing.T) {
109− b := testNextcloudBackend("unused payload")
110− defer b.Close()
111−
112− bURL, _ := url.Parse(b.URL)
113− p := testNextcloudProvider(bURL.Host)
114− p.ValidateURL.Path = userPath
115− p.ValidateURL.RawQuery = formatJSON
116−
117− // We'll trigger a request failure by using an unexpected access
118− // token. Alternatively, we could allow the parsing of the payload as
119− // JSON to fail.
120− session := &sessions.SessionState{AccessToken: "unexpected_access_token"}
121− email, err := p.GetEmailAddress(context.Background(), session)
122− assert.NotEqual(t, nil, err)
123− assert.Equal(t, "", email)
124−}
125−
126−func TestNextcloudProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
127− b := testNextcloudBackend("{\"foo\": \"bar\"}")
128− defer b.Close()
129−
130− bURL, _ := url.Parse(b.URL)
131− p := testNextcloudProvider(bURL.Host)
132− p.ValidateURL.Path = userPath
133− p.ValidateURL.RawQuery = formatJSON
134−
135− session := CreateAuthorizedSession()
136− email, err := p.GetEmailAddress(context.Background(), session)
137− assert.NotEqual(t, nil, err)
138− assert.Equal(t, "", email)
139−}
providers/oidc.go+9−56
@@ -5,12 +5,10 @@ import (
55 "errors"
66 "fmt"
77 "net/url"
8− "reflect"
98 "time"
109
1110 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1211 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
13− "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
1412 "golang.org/x/oauth2"
1513)
1614
@@ -24,6 +22,8 @@ type OIDCProvider struct {
2422// NewOIDCProvider initiates a new OIDCProvider
2523func NewOIDCProvider(p *ProviderData) *OIDCProvider {
2624 p.ProviderName = "OpenID Connect"
25+ p.getAuthorizationHeaderFunc = makeOIDCHeader
26+
2727 return &OIDCProvider{
2828 ProviderData: p,
2929 SkipNonce: true,
@@ -68,21 +68,6 @@ func (p *OIDCProvider) Redeem(ctx context.Context, redirectURL, code string) (*s
6868// EnrichSession is called after Redeem to allow providers to enrich session fields
6969// such as User, Email, Groups with provider specific API calls.
7070func (p *OIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
71− if p.ProfileURL.String() == "" {
72− if s.Email == "" {
73− return errors.New("id_token did not contain an email and profileURL is not defined")
74− }
75− return nil
76− }
77−
78− // Try to get missing emails or groups from a profileURL
79− if s.Email == "" || s.Groups == nil {
80− err := p.enrichFromProfileURL(ctx, s)
81− if err != nil {
82− logger.Errorf("Warning: Profile URL request failed: %v", err)
83− }
84− }
85−
8671 // If a mandatory email wasn't set, error at this point.
8772 if s.Email == "" {
8873 return errors.New("neither the id_token nor the profileURL set an email")
@@ -90,42 +75,9 @@ func (p *OIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionSta
9075 return nil
9176}
9277
93−// enrichFromProfileURL enriches a session's Email & Groups via the JSON response of
94−// an OIDC profile URL
95−func (p *OIDCProvider) enrichFromProfileURL(ctx context.Context, s *sessions.SessionState) error {
96− respJSON, err := requests.New(p.ProfileURL.String()).
97− WithContext(ctx).
98− WithHeaders(makeOIDCHeader(s.AccessToken)).
99− Do().
100− UnmarshalJSON()
101− if err != nil {
102− return err
103− }
104−
105− email, err := respJSON.Get(p.EmailClaim).String()
106− if err == nil && s.Email == "" {
107− s.Email = email
108− }
109−
110− if len(s.Groups) > 0 {
111− return nil
112− }
113− for _, group := range coerceArray(respJSON, p.GroupsClaim) {
114− formatted, err := formatGroup(group)
115− if err != nil {
116− logger.Errorf("Warning: unable to format group of type %s with error %s",
117− reflect.TypeOf(group), err)
118− continue
119− }
120− s.Groups = append(s.Groups, formatted)
121− }
122−
123− return nil
124−}
125−
12678// ValidateSession checks that the session's IDToken is still valid
12779func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
128− idToken, err := p.Verifier.Verify(ctx, s.IDToken)
80+ _, err := p.Verifier.Verify(ctx, s.IDToken)
12981 if err != nil {
13082 logger.Errorf("id_token verification failed: %v", err)
13183 return false
@@ -134,7 +86,7 @@ func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionS
13486 if p.SkipNonce {
13587 return true
13688 }
137− err = p.checkNonce(s, idToken)
89+ err = p.checkNonce(s)
13890 if err != nil {
13991 logger.Errorf("nonce verification failed: %v", err)
14092 return false
@@ -212,7 +164,7 @@ func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string)
212164 return nil, err
213165 }
214166
215− ss, err := p.buildSessionFromClaims(idToken)
167+ ss, err := p.buildSessionFromClaims(token, "")
216168 if err != nil {
217169 return nil, err
218170 }
@@ -235,7 +187,7 @@ func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string)
235187// createSession takes an oauth2.Token and creates a SessionState from it.
236188// It alters behavior if called from Redeem vs Refresh
237189func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, refresh bool) (*sessions.SessionState, error) {
238− idToken, err := p.verifyIDToken(ctx, token)
190+ _, err := p.verifyIDToken(ctx, token)
239191 if err != nil {
240192 switch err {
241193 case ErrMissingIDToken:
@@ -248,14 +200,15 @@ func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, r
248200 }
249201 }
250202
251− ss, err := p.buildSessionFromClaims(idToken)
203+ rawIDToken := getIDToken(token)
204+ ss, err := p.buildSessionFromClaims(rawIDToken, token.AccessToken)
252205 if err != nil {
253206 return nil, err
254207 }
255208
256209 ss.AccessToken = token.AccessToken
257210 ss.RefreshToken = token.RefreshToken
258− ss.IDToken = getIDToken(token)
211+ ss.IDToken = rawIDToken
259212
260213 ss.CreatedAtNow()
261214 ss.SetExpiresOn(token.Expiry)
providers/oidc_test.go+10−333
@@ -4,7 +4,6 @@ import (
44 "context"
55 "encoding/base64"
66 "encoding/json"
7− "errors"
87 "fmt"
98 "net/http"
109 "net/http/httptest"
@@ -54,6 +53,7 @@ func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
5453 Scope: "openid profile offline_access",
5554 EmailClaim: "email",
5655 GroupsClaim: "groups",
56+ UserClaim: "sub",
5757 Verifier: internaloidc.NewVerifier(oidc.NewVerifier(
5858 oidcIssuer,
5959 mockJWKS{},
@@ -142,333 +142,6 @@ func TestOIDCProviderRedeem_custom_userid(t *testing.T) {
142142 assert.Equal(t, defaultIDToken.Phone, session.Email)
143143}
144144
145−func TestOIDCProvider_EnrichSession(t *testing.T) {
146− testCases := map[string]struct {
147− ExistingSession *sessions.SessionState
148− EmailClaim string
149− GroupsClaim string
150− ProfileJSON map[string]interface{}
151− ExpectedError error
152− ExpectedSession *sessions.SessionState
153− }{
154− "Already Populated": {
155− ExistingSession: &sessions.SessionState{
156− User: "already",
157− Email: "already@populated.com",
158− Groups: []string{"already", "populated"},
159− IDToken: idToken,
160− AccessToken: accessToken,
161− RefreshToken: refreshToken,
162− },
163− EmailClaim: "email",
164− GroupsClaim: "groups",
165− ProfileJSON: map[string]interface{}{
166− "email": "new@thing.com",
167− "groups": []string{"new", "thing"},
168− },
169− ExpectedError: nil,
170− ExpectedSession: &sessions.SessionState{
171− User: "already",
172− Email: "already@populated.com",
173− Groups: []string{"already", "populated"},
174− IDToken: idToken,
175− AccessToken: accessToken,
176− RefreshToken: refreshToken,
177− },
178− },
179− "Missing Email": {
180− ExistingSession: &sessions.SessionState{
181− User: "missing.email",
182− Groups: []string{"already", "populated"},
183− IDToken: idToken,
184− AccessToken: accessToken,
185− RefreshToken: refreshToken,
186− },
187− EmailClaim: "email",
188− GroupsClaim: "groups",
189− ProfileJSON: map[string]interface{}{
190− "email": "found@email.com",
191− "groups": []string{"new", "thing"},
192− },
193− ExpectedError: nil,
194− ExpectedSession: &sessions.SessionState{
195− User: "missing.email",
196− Email: "found@email.com",
197− Groups: []string{"already", "populated"},
198− IDToken: idToken,
199− AccessToken: accessToken,
200− RefreshToken: refreshToken,
201− },
202− },
203−
204− "Missing Email Only in Profile URL": {
205− ExistingSession: &sessions.SessionState{
206− User: "missing.email",
207− IDToken: idToken,
208− AccessToken: accessToken,
209− RefreshToken: refreshToken,
210− },
211− EmailClaim: "email",
212− GroupsClaim: "groups",
213− ProfileJSON: map[string]interface{}{
214− "email": "found@email.com",
215− },
216− ExpectedError: nil,
217− ExpectedSession: &sessions.SessionState{
218− User: "missing.email",
219− Email: "found@email.com",
220− IDToken: idToken,
221− AccessToken: accessToken,
222− RefreshToken: refreshToken,
223− },
224− },
225− "Missing Email with Custom Claim": {
226− ExistingSession: &sessions.SessionState{
227− User: "missing.email",
228− Groups: []string{"already", "populated"},
229− IDToken: idToken,
230− AccessToken: accessToken,
231− RefreshToken: refreshToken,
232− },
233− EmailClaim: "weird",
234− GroupsClaim: "groups",
235− ProfileJSON: map[string]interface{}{
236− "weird": "weird@claim.com",
237− "groups": []string{"new", "thing"},
238− },
239− ExpectedError: nil,
240− ExpectedSession: &sessions.SessionState{
241− User: "missing.email",
242− Email: "weird@claim.com",
243− Groups: []string{"already", "populated"},
244− IDToken: idToken,
245− AccessToken: accessToken,
246− RefreshToken: refreshToken,
247− },
248− },
249− "Missing Email not in Profile URL": {
250− ExistingSession: &sessions.SessionState{
251− User: "missing.email",
252− Groups: []string{"already", "populated"},
253− IDToken: idToken,
254− AccessToken: accessToken,
255− RefreshToken: refreshToken,
256− },
257− EmailClaim: "email",
258− GroupsClaim: "groups",
259− ProfileJSON: map[string]interface{}{
260− "groups": []string{"new", "thing"},
261− },
262− ExpectedError: errors.New("neither the id_token nor the profileURL set an email"),
263− ExpectedSession: &sessions.SessionState{
264− User: "missing.email",
265− Groups: []string{"already", "populated"},
266− IDToken: idToken,
267− AccessToken: accessToken,
268− RefreshToken: refreshToken,
269− },
270− },
271− "Missing Groups": {
272− ExistingSession: &sessions.SessionState{
273− User: "already",
274− Email: "already@populated.com",
275− Groups: nil,
276− IDToken: idToken,
277− AccessToken: accessToken,
278− RefreshToken: refreshToken,
279− },
280− EmailClaim: "email",
281− GroupsClaim: "groups",
282− ProfileJSON: map[string]interface{}{
283− "email": "new@thing.com",
284− "groups": []string{"new", "thing"},
285− },
286− ExpectedError: nil,
287− ExpectedSession: &sessions.SessionState{
288− User: "already",
289− Email: "already@populated.com",
290− Groups: []string{"new", "thing"},
291− IDToken: idToken,
292− AccessToken: accessToken,
293− RefreshToken: refreshToken,
294− },
295− },
296− "Missing Groups with Complex Groups in Profile URL": {
297− ExistingSession: &sessions.SessionState{
298− User: "already",
299− Email: "already@populated.com",
300− Groups: nil,
301− IDToken: idToken,
302− AccessToken: accessToken,
303− RefreshToken: refreshToken,
304− },
305− EmailClaim: "email",
306− GroupsClaim: "groups",
307− ProfileJSON: map[string]interface{}{
308− "email": "new@thing.com",
309− "groups": []map[string]interface{}{
310− {
311− "groupId": "Admin Group Id",
312− "roles": []string{"Admin"},
313− },
314− },
315− },
316− ExpectedError: nil,
317− ExpectedSession: &sessions.SessionState{
318− User: "already",
319− Email: "already@populated.com",
320− Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
321− IDToken: idToken,
322− AccessToken: accessToken,
323− RefreshToken: refreshToken,
324− },
325− },
326− "Missing Groups with Singleton Complex Group in Profile URL": {
327− ExistingSession: &sessions.SessionState{
328− User: "already",
329− Email: "already@populated.com",
330− Groups: nil,
331− IDToken: idToken,
332− AccessToken: accessToken,
333− RefreshToken: refreshToken,
334− },
335− EmailClaim: "email",
336− GroupsClaim: "groups",
337− ProfileJSON: map[string]interface{}{
338− "email": "new@thing.com",
339− "groups": map[string]interface{}{
340− "groupId": "Admin Group Id",
341− "roles": []string{"Admin"},
342− },
343− },
344− ExpectedError: nil,
345− ExpectedSession: &sessions.SessionState{
346− User: "already",
347− Email: "already@populated.com",
348− Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
349− IDToken: idToken,
350− AccessToken: accessToken,
351− RefreshToken: refreshToken,
352− },
353− },
354− "Empty Groups Claims": {
355− ExistingSession: &sessions.SessionState{
356− User: "already",
357− Email: "already@populated.com",
358− Groups: []string{},
359− IDToken: idToken,
360− AccessToken: accessToken,
361− RefreshToken: refreshToken,
362− },
363− EmailClaim: "email",
364− GroupsClaim: "groups",
365− ProfileJSON: map[string]interface{}{
366− "email": "new@thing.com",
367− "groups": []string{"new", "thing"},
368− },
369− ExpectedError: nil,
370− ExpectedSession: &sessions.SessionState{
371− User: "already",
372− Email: "already@populated.com",
373− Groups: []string{},
374− IDToken: idToken,
375− AccessToken: accessToken,
376− RefreshToken: refreshToken,
377− },
378− },
379− "Missing Groups with Custom Claim": {
380− ExistingSession: &sessions.SessionState{
381− User: "already",
382− Email: "already@populated.com",
383− Groups: nil,
384− IDToken: idToken,
385− AccessToken: accessToken,
386− RefreshToken: refreshToken,
387− },
388− EmailClaim: "email",
389− GroupsClaim: "roles",
390− ProfileJSON: map[string]interface{}{
391− "email": "new@thing.com",
392− "roles": []string{"new", "thing", "roles"},
393− },
394− ExpectedError: nil,
395− ExpectedSession: &sessions.SessionState{
396− User: "already",
397− Email: "already@populated.com",
398− Groups: []string{"new", "thing", "roles"},
399− IDToken: idToken,
400− AccessToken: accessToken,
401− RefreshToken: refreshToken,
402− },
403− },
404− "Missing Groups String Profile URL Response": {
405− ExistingSession: &sessions.SessionState{
406− User: "already",
407− Email: "already@populated.com",
408− Groups: nil,
409− IDToken: idToken,
410− AccessToken: accessToken,
411− RefreshToken: refreshToken,
412− },
413− EmailClaim: "email",
414− GroupsClaim: "groups",
415− ProfileJSON: map[string]interface{}{
416− "email": "new@thing.com",
417− "groups": "singleton",
418− },
419− ExpectedError: nil,
420− ExpectedSession: &sessions.SessionState{
421− User: "already",
422− Email: "already@populated.com",
423− Groups: []string{"singleton"},
424− IDToken: idToken,
425− AccessToken: accessToken,
426− RefreshToken: refreshToken,
427− },
428− },
429− "Missing Groups in both Claims and Profile URL": {
430− ExistingSession: &sessions.SessionState{
431− User: "already",
432− Email: "already@populated.com",
433− IDToken: idToken,
434− AccessToken: accessToken,
435− RefreshToken: refreshToken,
436− },
437− EmailClaim: "email",
438− GroupsClaim: "groups",
439− ProfileJSON: map[string]interface{}{
440− "email": "new@thing.com",
441− },
442− ExpectedError: nil,
443− ExpectedSession: &sessions.SessionState{
444− User: "already",
445− Email: "already@populated.com",
446− IDToken: idToken,
447− AccessToken: accessToken,
448− RefreshToken: refreshToken,
449− },
450− },
451− }
452− for testName, tc := range testCases {
453− t.Run(testName, func(t *testing.T) {
454− jsonResp, err := json.Marshal(tc.ProfileJSON)
455− assert.NoError(t, err)
456−
457− server, provider := newTestOIDCSetup(jsonResp)
458− provider.ProfileURL, err = url.Parse(server.URL)
459− assert.NoError(t, err)
460−
461− provider.EmailClaim = tc.EmailClaim
462− provider.GroupsClaim = tc.GroupsClaim
463− defer server.Close()
464−
465− err = provider.EnrichSession(context.Background(), tc.ExistingSession)
466− assert.Equal(t, tc.ExpectedError, err)
467− assert.Equal(t, *tc.ExpectedSession, *tc.ExistingSession)
468− })
469− }
470−}
471−
472145func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) {
473146
474147 idToken, _ := newSignedTestIDToken(defaultIDToken)
@@ -565,11 +238,15 @@ func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
565238 ExpectedGroups: []string{"test:c", "test:d"},
566239 },
567240 "Complex Groups Claim": {
568− IDToken: complexGroupsIDToken,
569− GroupsClaim: "groups",
570− ExpectedUser: "123456789",
571− ExpectedEmail: "complex@claims.com",
572− ExpectedGroups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
241+ IDToken: complexGroupsIDToken,
242+ GroupsClaim: "groups",
243+ ExpectedUser: "123456789",
244+ ExpectedEmail: "complex@claims.com",
245+ ExpectedGroups: []string{
246+ "{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
247+ "12345",
248+ "Just::A::String",
249+ },
573250 },
574251 }
575252 for testName, tc := range testCases {
providers/provider_data.go+55−87
@@ -5,20 +5,23 @@ import (
55 "errors"
66 "fmt"
77 "io/ioutil"
8+ "net/http"
89 "net/url"
9− "reflect"
1010 "strings"
1111
1212 "github.com/coreos/go-oidc/v3/oidc"
1313 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
1414 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
1515 internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/oidc"
16+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/util"
1617 "golang.org/x/oauth2"
1718)
1819
1920const (
2021 OIDCEmailClaim = "email"
2122 OIDCGroupsClaim = "groups"
23+ // This is not exported as it's not currently user configurable
24+ oidcUserClaim = "sub"
2225)
2326
2427var OIDCAudienceClaims = []string{"aud"}
@@ -52,6 +55,8 @@ type ProviderData struct {
5255 // Universal Group authorization data structure
5356 // any provider can set to consume
5457 AllowedGroups map[string]struct{}
58+
59+ getAuthorizationHeaderFunc func(string) http.Header
5560}
5661
5762// Data returns the ProviderData
@@ -99,6 +104,10 @@ func (p *ProviderData) setProviderDefaults(defaults providerDefaults) {
99104 if p.Scope == "" {
100105 p.Scope = defaults.scope
101106 }
107+
108+ if p.UserClaim == "" {
109+ p.UserClaim = oidcUserClaim
110+ }
102111}
103112
104113// defaultURL will set return a default value if the given value is not set.
@@ -120,17 +129,6 @@ func defaultURL(u *url.URL, d *url.URL) *url.URL {
120129// OIDC compliant
121130// ****************************************************************************
122131
123−// OIDCClaims is a struct to unmarshal the OIDC claims from an ID Token payload
124−type OIDCClaims struct {
125− Subject string `json:"sub"`
126− Email string `json:"-"`
127− Groups []string `json:"-"`
128− Verified *bool `json:"email_verified"`
129− Nonce string `json:"nonce"`
130−
131− raw map[string]interface{}
132−}
133−
134132func (p *ProviderData) verifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
135133 rawIDToken := getIDToken(token)
136134 if strings.TrimSpace(rawIDToken) == "" {
@@ -144,110 +142,80 @@ func (p *ProviderData) verifyIDToken(ctx context.Context, token *oauth2.Token) (
144142
145143// buildSessionFromClaims uses IDToken claims to populate a fresh SessionState
146144// with non-Token related fields.
147−func (p *ProviderData) buildSessionFromClaims(idToken *oidc.IDToken) (*sessions.SessionState, error) {
145+func (p *ProviderData) buildSessionFromClaims(rawIDToken, accessToken string) (*sessions.SessionState, error) {
148146 ss := &sessions.SessionState{}
149147
150− if idToken == nil {
148+ if rawIDToken == "" {
151149 return ss, nil
152150 }
153151
154− claims, err := p.getClaims(idToken)
152+ extractor, err := p.getClaimExtractor(rawIDToken, accessToken)
155153 if err != nil {
156− return nil, fmt.Errorf("couldn't extract claims from id_token (%v)", err)
157− }
158−
159− ss.User = claims.Subject
160− ss.Email = claims.Email
161− ss.Groups = claims.Groups
162−
163− // Allow specialized providers that embed OIDCProvider to control the User
164− // claim. Not exposed as a configuration flag to generic OIDC provider
165− // users (yet).
166− if p.UserClaim != "" {
167− user, ok := claims.raw[p.UserClaim].(string)
168− if !ok {
169− return nil, fmt.Errorf("unable to extract custom UserClaim (%s)", p.UserClaim)
154+ return nil, err
155+ }
156+
157+ // Use a slice of a struct (vs map) here in case the same claim is used twice
158+ for _, c := range []struct {
159+ claim string
160+ dst interface{}
161+ }{
162+ {p.UserClaim, &ss.User},
163+ {p.EmailClaim, &ss.Email},
164+ {p.GroupsClaim, &ss.Groups},
165+ // TODO (@NickMeves) Deprecate for dynamic claim to session mapping
166+ {"preferred_username", &ss.PreferredUsername},
167+ } {
168+ if _, err := extractor.GetClaimInto(c.claim, c.dst); err != nil {
169+ return nil, err
170170 }
171− ss.User = user
172− }
173−
174− // TODO (@NickMeves) Deprecate for dynamic claim to session mapping
175− if pref, ok := claims.raw["preferred_username"].(string); ok {
176− ss.PreferredUsername = pref
177171 }
178172
179173 // `email_verified` must be present and explicitly set to `false` to be
180174 // considered unverified.
181175 verifyEmail := (p.EmailClaim == OIDCEmailClaim) && !p.AllowUnverifiedEmail
182− if verifyEmail && claims.Verified != nil && !*claims.Verified {
183− return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
176+
177+ var verified bool
178+ exists, err := extractor.GetClaimInto("email_verified", &verified)
179+ if err != nil {
180+ return nil, err
181+ }
182+
183+ if verifyEmail && exists && !verified {
184+ return nil, fmt.Errorf("email in id_token (%s) isn't verified", ss.Email)
184185 }
185186
186187 return ss, nil
187188}
188189
189−// getClaims extracts IDToken claims into an OIDCClaims
190−func (p *ProviderData) getClaims(idToken *oidc.IDToken) (*OIDCClaims, error) {
191− claims := &OIDCClaims{}
192−
193− // Extract default claims.
194− if err := idToken.Claims(&claims); err != nil {
195− return nil, fmt.Errorf("failed to parse default id_token claims: %v", err)
196− }
197− // Extract custom claims.
198− if err := idToken.Claims(&claims.raw); err != nil {
199− return nil, fmt.Errorf("failed to parse all id_token claims: %v", err)
200− }
201−
202− email := claims.raw[p.EmailClaim]
203− if email != nil {
204− claims.Email = fmt.Sprint(email)
190+func (p *ProviderData) getClaimExtractor(rawIDToken, accessToken string) (util.ClaimExtractor, error) {
191+ extractor, err := util.NewClaimExtractor(context.TODO(), rawIDToken, p.ProfileURL, p.getAuthorizationHeader(accessToken))
192+ if err != nil {
193+ return nil, fmt.Errorf("could not initialise claim extractor: %v", err)
205194 }
206− claims.Groups = p.extractGroups(claims.raw)
207195
208− return claims, nil
196+ return extractor, nil
209197}
210198
211199// checkNonce compares the session's nonce with the IDToken's nonce claim
212−func (p *ProviderData) checkNonce(s *sessions.SessionState, idToken *oidc.IDToken) error {
213− claims, err := p.getClaims(idToken)
200+func (p *ProviderData) checkNonce(s *sessions.SessionState) error {
201+ extractor, err := p.getClaimExtractor(s.IDToken, "")
214202 if err != nil {
215203 return fmt.Errorf("id_token claims extraction failed: %v", err)
216204 }
217− if !s.CheckNonce(claims.Nonce) {
205+ var nonce string
206+ if _, err := extractor.GetClaimInto("nonce", &nonce); err != nil {
207+ return fmt.Errorf("could not extract nonce from ID Token: %v", err)
208+ }
209+
210+ if !s.CheckNonce(nonce) {
218211 return errors.New("id_token nonce claim does not match the session nonce")
219212 }
220213 return nil
221214}
222215
223−// extractGroups extracts groups from a claim to a list in a type safe manner.
224−// If the claim isn't present, `nil` is returned. If the groups claim is
225−// present but empty, `[]string{}` is returned.
226−func (p *ProviderData) extractGroups(claims map[string]interface{}) []string {
227− rawClaim, ok := claims[p.GroupsClaim]
228− if !ok {
229− return nil
230− }
231−
232− // Handle traditional list-based groups as well as non-standard singleton
233− // based groups. Both variants support complex objects if needed.
234− var claimGroups []interface{}
235− switch raw := rawClaim.(type) {
236− case []interface{}:
237− claimGroups = raw
238− case interface{}:
239− claimGroups = []interface{}{raw}
240− }
241−
242− groups := []string{}
243− for _, rawGroup := range claimGroups {
244− formattedGroup, err := formatGroup(rawGroup)
245− if err != nil {
246− logger.Errorf("Warning: unable to format group of type %s with error %s",
247− reflect.TypeOf(rawGroup), err)
248− continue
249− }
250− groups = append(groups, formattedGroup)
216+func (p *ProviderData) getAuthorizationHeader(accessToken string) http.Header {
217+ if p.getAuthorizationHeaderFunc != nil && accessToken != "" {
218+ return p.getAuthorizationHeaderFunc(accessToken)
251219 }
252− return groups
220+ return nil
253221}
providers/provider_data_test.go+73−112
@@ -60,16 +60,30 @@ var (
6060 StandardClaims: standardClaims,
6161 }
6262
63+ numericGroupsIDToken = idTokenClaims{
64+ Name: "Jane Dobbs",
65+ Email: "janed@me.com",
66+ Phone: "+4798765432",
67+ Picture: "http://mugbook.com/janed/me.jpg",
68+ Groups: []interface{}{1, 2, 3},
69+ Roles: []string{"test:c", "test:d"},
70+ Verified: &verified,
71+ Nonce: encryption.HashNonce([]byte(oidcNonce)),
72+ StandardClaims: standardClaims,
73+ }
74+
6375 complexGroupsIDToken = idTokenClaims{
6476 Name: "Complex Claim",
6577 Email: "complex@claims.com",
6678 Phone: "+5439871234",
6779 Picture: "http://mugbook.com/complex/claims.jpg",
68− Groups: []map[string]interface{}{
69− {
80+ Groups: []interface{}{
81+ map[string]interface{}{
7082 "groupId": "Admin Group Id",
7183 "roles": []string{"Admin"},
7284 },
85+ 12345,
86+ "Just::A::String",
7387 },
7488 Roles: []string{"test:simple", "test:roles"},
7589 Verified: &verified,
@@ -228,6 +242,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
228242 AllowUnverified: false,
229243 EmailClaim: "email",
230244 GroupsClaim: "groups",
245+ UserClaim: "sub",
231246 ExpectedSession: &sessions.SessionState{
232247 User: "123456789",
233248 Email: "janed@me.com",
@@ -247,6 +262,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
247262 AllowUnverified: true,
248263 EmailClaim: "email",
249264 GroupsClaim: "groups",
265+ UserClaim: "sub",
250266 ExpectedSession: &sessions.SessionState{
251267 User: "123456789",
252268 Email: "unverified@email.com",
@@ -259,10 +275,15 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
259275 AllowUnverified: true,
260276 EmailClaim: "email",
261277 GroupsClaim: "groups",
278+ UserClaim: "sub",
262279 ExpectedSession: &sessions.SessionState{
263− User: "123456789",
264− Email: "complex@claims.com",
265− Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
280+ User: "123456789",
281+ Email: "complex@claims.com",
282+ Groups: []string{
283+ "{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
284+ "12345",
285+ "Just::A::String",
286+ },
266287 PreferredUsername: "Complex Claim",
267288 },
268289 },
@@ -279,19 +300,25 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
279300 PreferredUsername: "Jane Dobbs",
280301 },
281302 },
282− "User Claim Invalid": {
303+ "User Claim switched to non string": {
283304 IDToken: defaultIDToken,
284305 AllowUnverified: true,
285− UserClaim: "groups",
306+ UserClaim: "roles",
286307 EmailClaim: "email",
287308 GroupsClaim: "groups",
288− ExpectedError: errors.New("unable to extract custom UserClaim (groups)"),
309+ ExpectedSession: &sessions.SessionState{
310+ User: "[\"test:c\",\"test:d\"]",
311+ Email: "janed@me.com",
312+ Groups: []string{"test:a", "test:b"},
313+ PreferredUsername: "Jane Dobbs",
314+ },
289315 },
290316 "Email Claim Switched": {
291317 IDToken: unverifiedIDToken,
292318 AllowUnverified: true,
293319 EmailClaim: "phone_number",
294320 GroupsClaim: "groups",
321+ UserClaim: "sub",
295322 ExpectedSession: &sessions.SessionState{
296323 User: "123456789",
297324 Email: "+4025205729",
@@ -304,9 +331,10 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
304331 AllowUnverified: true,
305332 EmailClaim: "roles",
306333 GroupsClaim: "groups",
334+ UserClaim: "sub",
307335 ExpectedSession: &sessions.SessionState{
308336 User: "123456789",
309− Email: "[test:c test:d]",
337+ Email: "[\"test:c\",\"test:d\"]",
310338 Groups: []string{"test:a", "test:b"},
311339 PreferredUsername: "Mystery Man",
312340 },
@@ -316,6 +344,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
316344 AllowUnverified: true,
317345 EmailClaim: "aksjdfhjksadh",
318346 GroupsClaim: "groups",
347+ UserClaim: "sub",
319348 ExpectedSession: &sessions.SessionState{
320349 User: "123456789",
321350 Email: "",
@@ -328,6 +357,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
328357 AllowUnverified: false,
329358 EmailClaim: "email",
330359 GroupsClaim: "roles",
360+ UserClaim: "sub",
331361 ExpectedSession: &sessions.SessionState{
332362 User: "123456789",
333363 Email: "janed@me.com",
@@ -340,6 +370,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
340370 AllowUnverified: false,
341371 EmailClaim: "email",
342372 GroupsClaim: "alskdjfsalkdjf",
373+ UserClaim: "sub",
343374 ExpectedSession: &sessions.SessionState{
344375 User: "123456789",
345376 Email: "janed@me.com",
@@ -347,6 +378,32 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
347378 PreferredUsername: "Jane Dobbs",
348379 },
349380 },
381+ "Groups Claim Numeric values": {
382+ IDToken: numericGroupsIDToken,
383+ AllowUnverified: false,
384+ EmailClaim: "email",
385+ GroupsClaim: "groups",
386+ UserClaim: "sub",
387+ ExpectedSession: &sessions.SessionState{
388+ User: "123456789",
389+ Email: "janed@me.com",
390+ Groups: []string{"1", "2", "3"},
391+ PreferredUsername: "Jane Dobbs",
392+ },
393+ },
394+ "Groups Claim string values": {
395+ IDToken: defaultIDToken,
396+ AllowUnverified: false,
397+ EmailClaim: "email",
398+ GroupsClaim: "email",
399+ UserClaim: "sub",
400+ ExpectedSession: &sessions.SessionState{
401+ User: "123456789",
402+ Email: "janed@me.com",
403+ Groups: []string{"janed@me.com"},
404+ PreferredUsername: "Jane Dobbs",
405+ },
406+ },
350407 }
351408 for testName, tc := range testCases {
352409 t.Run(testName, func(t *testing.T) {
@@ -371,10 +428,7 @@ func TestProviderData_buildSessionFromClaims(t *testing.T) {
371428 rawIDToken, err := newSignedTestIDToken(tc.IDToken)
372429 g.Expect(err).ToNot(HaveOccurred())
373430
374− idToken, err := provider.Verifier.Verify(context.Background(), rawIDToken)
375− g.Expect(err).ToNot(HaveOccurred())
376−
377− ss, err := provider.buildSessionFromClaims(idToken)
431+ ss, err := provider.buildSessionFromClaims(rawIDToken, "")
378432 if err != nil {
379433 g.Expect(err).To(Equal(tc.ExpectedError))
380434 }
@@ -418,102 +472,11 @@ func TestProviderData_checkNonce(t *testing.T) {
418472 t.Run(testName, func(t *testing.T) {
419473 g := NewWithT(t)
420474
421− verificationOptions := &internaloidc.IDTokenVerificationOptions{
422− AudienceClaims: []string{"aud"},
423− ClientID: oidcClientID,
424− }
425− provider := &ProviderData{
426− Verifier: internaloidc.NewVerifier(oidc.NewVerifier(
427− oidcIssuer,
428− mockJWKS{},
429− &oidc.Config{ClientID: oidcClientID},
430− ), verificationOptions),
431− }
432−
475+ // Ensure that the ID token in the session is valid (signed and contains a nonce)
476+ // as the nonce claim is extracted to compare with the session nonce
433477 rawIDToken, err := newSignedTestIDToken(tc.IDToken)
434478 g.Expect(err).ToNot(HaveOccurred())
435−
436− idToken, err := provider.Verifier.Verify(context.Background(), rawIDToken)
437− g.Expect(err).ToNot(HaveOccurred())
438−
439− err = provider.checkNonce(tc.Session, idToken)
440− if err != nil {
441− g.Expect(err).To(Equal(tc.ExpectedError))
442− } else {
443− g.Expect(err).ToNot(HaveOccurred())
444− }
445− })
446− }
447−}
448−
449−func TestProviderData_extractGroups(t *testing.T) {
450− testCases := map[string]struct {
451− Claims map[string]interface{}
452− GroupsClaim string
453− ExpectedGroups []string
454− }{
455− "Standard String Groups": {
456− Claims: map[string]interface{}{
457− "email": "this@does.not.matter.com",
458− "groups": []interface{}{"three", "string", "groups"},
459− },
460− GroupsClaim: "groups",
461− ExpectedGroups: []string{"three", "string", "groups"},
462− },
463− "Different Claim Name": {
464− Claims: map[string]interface{}{
465− "email": "this@does.not.matter.com",
466− "roles": []interface{}{"three", "string", "roles"},
467− },
468− GroupsClaim: "roles",
469− ExpectedGroups: []string{"three", "string", "roles"},
470− },
471− "Numeric Groups": {
472− Claims: map[string]interface{}{
473− "email": "this@does.not.matter.com",
474− "groups": []interface{}{1, 2, 3},
475− },
476− GroupsClaim: "groups",
477− ExpectedGroups: []string{"1", "2", "3"},
478− },
479− "Complex Groups": {
480− Claims: map[string]interface{}{
481− "email": "this@does.not.matter.com",
482− "groups": []interface{}{
483− map[string]interface{}{
484− "groupId": "Admin Group Id",
485− "roles": []string{"Admin"},
486− },
487− 12345,
488− "Just::A::String",
489− },
490− },
491− GroupsClaim: "groups",
492− ExpectedGroups: []string{
493− "{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
494− "12345",
495− "Just::A::String",
496− },
497− },
498− "Missing Groups Claim Returns Nil": {
499− Claims: map[string]interface{}{
500− "email": "this@does.not.matter.com",
501− },
502− GroupsClaim: "groups",
503− ExpectedGroups: nil,
504− },
505− "Non List Groups": {
506− Claims: map[string]interface{}{
507− "email": "this@does.not.matter.com",
508− "groups": "singleton",
509− },
510− GroupsClaim: "groups",
511− ExpectedGroups: []string{"singleton"},
512− },
513− }
514− for testName, tc := range testCases {
515− t.Run(testName, func(t *testing.T) {
516− g := NewWithT(t)
479+ tc.Session.IDToken = rawIDToken
517480
518481 verificationOptions := &internaloidc.IDTokenVerificationOptions{
519482 AudienceClaims: []string{"aud"},
@@ -526,13 +489,11 @@ func TestProviderData_extractGroups(t *testing.T) {
526489 &oidc.Config{ClientID: oidcClientID},
527490 ), verificationOptions),
528491 }
529− provider.GroupsClaim = tc.GroupsClaim
530492
531− groups := provider.extractGroups(tc.Claims)
532− if tc.ExpectedGroups != nil {
533− g.Expect(groups).To(Equal(tc.ExpectedGroups))
493+ if err := provider.checkNonce(tc.Session); err != nil {
494+ g.Expect(err).To(Equal(tc.ExpectedError))
534495 } else {
535− g.Expect(groups).To(BeNil())
496+ g.Expect(err).ToNot(HaveOccurred())
536497 }
537498 })
538499 }
providers/util.go−16
@@ -6,7 +6,6 @@ import (
66 "net/http"
77 "net/url"
88
9− "github.com/bitly/go-simplejson"
109 "golang.org/x/oauth2"
1110)
1211
@@ -83,18 +82,3 @@ func formatGroup(rawGroup interface{}) (string, error) {
8382 }
8483 return string(jsonGroup), nil
8584}
86−
87−// coerceArray extracts a field from simplejson.Json that might be a
88−// singleton or a list and coerces it into a list.
89−func coerceArray(sj *simplejson.Json, key string) []interface{} {
90− array, err := sj.Get(key).Array()
91− if err == nil {
92− return array
93− }
94−
95− single := sj.Get(key).Interface()
96− if single == nil {
97− return nil
98− }
99− return []interface{}{single}
100−}