@@ -0,0 +1,414 @@
1+package providers
2+
3+import (
4+ "context"
5+ "crypto/rand"
6+ "crypto/rsa"
7+ "encoding/base64"
8+ "encoding/json"
9+ "errors"
10+ "fmt"
11+ "strings"
12+ "testing"
13+ "time"
14+
15+ "github.com/coreos/go-oidc"
16+ "github.com/dgrijalva/jwt-go"
17+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
18+ . "github.com/onsi/gomega"
19+ "golang.org/x/oauth2"
20+)
21+
22+const (
23+ idToken = "eyJfoobar123.eyJbaz987.IDToken"
24+ accessToken = "eyJfoobar123.eyJbaz987.AccessToken"
25+ refreshToken = "eyJfoobar123.eyJbaz987.RefreshToken"
26+
27+ oidcIssuer = "https://issuer.example.com"
28+ oidcClientID = "https://test.myapp.com"
29+ oidcSecret = "SuperSecret123456789"
30+
31+ failureTokenID = "this-id-fails-verification"
32+)
33+
34+var (
35+ verified = true
36+ unverified = false
37+
38+ standardClaims = jwt.StandardClaims{
39+ Audience: oidcClientID,
40+ ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
41+ Id: "id-some-id",
42+ IssuedAt: time.Now().Unix(),
43+ Issuer: oidcIssuer,
44+ NotBefore: 0,
45+ Subject: "123456789",
46+ }
47+
48+ defaultIDToken = idTokenClaims{
49+ Name: "Jane Dobbs",
50+ Email: "janed@me.com",
51+ Phone: "+4798765432",
52+ Picture: "http://mugbook.com/janed/me.jpg",
53+ Groups: []string{"test:a", "test:b"},
54+ Roles: []string{"test:c", "test:d"},
55+ Verified: &verified,
56+ StandardClaims: standardClaims,
57+ }
58+
59+ complexGroupsIDToken = idTokenClaims{
60+ Name: "Complex Claim",
61+ Email: "complex@claims.com",
62+ Phone: "+5439871234",
63+ Picture: "http://mugbook.com/complex/claims.jpg",
64+ Groups: []map[string]interface{}{
65+ {
66+ "groupId": "Admin Group Id",
67+ "roles": []string{"Admin"},
68+ },
69+ },
70+ Roles: []string{"test:simple", "test:roles"},
71+ Verified: &verified,
72+ StandardClaims: standardClaims,
73+ }
74+
75+ unverifiedIDToken = idTokenClaims{
76+ Name: "Mystery Man",
77+ Email: "unverified@email.com",
78+ Phone: "+4025205729",
79+ Picture: "http://mugbook.com/unverified/email.jpg",
80+ Groups: []string{"test:a", "test:b"},
81+ Roles: []string{"test:c", "test:d"},
82+ Verified: &unverified,
83+ StandardClaims: standardClaims,
84+ }
85+
86+ minimalIDToken = idTokenClaims{
87+ StandardClaims: standardClaims,
88+ }
89+)
90+
91+type idTokenClaims struct {
92+ Name string `json:"preferred_username,omitempty"`
93+ Email string `json:"email,omitempty"`
94+ Phone string `json:"phone_number,omitempty"`
95+ Picture string `json:"picture,omitempty"`
96+ Groups interface{} `json:"groups,omitempty"`
97+ Roles interface{} `json:"roles,omitempty"`
98+ Verified *bool `json:"email_verified,omitempty"`
99+ jwt.StandardClaims
100+}
101+
102+type mockJWKS struct{}
103+
104+func (mockJWKS) VerifySignature(_ context.Context, jwt string) ([]byte, error) {
105+ decoded, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
106+ if err != nil {
107+ return nil, err
108+ }
109+
110+ tokenClaims := &idTokenClaims{}
111+ err = json.Unmarshal(decoded, tokenClaims)
112+ if err != nil || tokenClaims.Id == failureTokenID {
113+ return nil, fmt.Errorf("the validation failed for subject [%v]", tokenClaims.Subject)
114+ }
115+
116+ return decoded, nil
117+}
118+
119+func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
120+ key, _ := rsa.GenerateKey(rand.Reader, 2048)
121+ standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
122+ return standardClaims.SignedString(key)
123+}
124+
125+func newTestOauth2Token() *oauth2.Token {
126+ return &oauth2.Token{
127+ AccessToken: accessToken,
128+ TokenType: "Bearer",
129+ RefreshToken: refreshToken,
130+ Expiry: time.Time{}.Add(time.Duration(5) * time.Second),
131+ }
132+}
133+
134+func TestProviderData_verifyIDToken(t *testing.T) {
135+ failureIDToken := defaultIDToken
136+ failureIDToken.Id = failureTokenID
137+
138+ testCases := map[string]struct {
139+ IDToken *idTokenClaims
140+ ExpectIDToken bool
141+ ExpectedError error
142+ }{
143+ "Valid ID Token": {
144+ IDToken: &defaultIDToken,
145+ ExpectIDToken: true,
146+ ExpectedError: nil,
147+ },
148+ "Invalid ID Token": {
149+ IDToken: &failureIDToken,
150+ ExpectIDToken: false,
151+ ExpectedError: errors.New("failed to verify signature: the validation failed for subject [123456789]"),
152+ },
153+ "Missing ID Token": {
154+ IDToken: nil,
155+ ExpectIDToken: false,
156+ ExpectedError: nil,
157+ },
158+ }
159+
160+ for testName, tc := range testCases {
161+ t.Run(testName, func(t *testing.T) {
162+ g := NewWithT(t)
163+
164+ token := newTestOauth2Token()
165+ if tc.IDToken != nil {
166+ idToken, err := newSignedTestIDToken(*tc.IDToken)
167+ g.Expect(err).ToNot(HaveOccurred())
168+ token = token.WithExtra(map[string]interface{}{
169+ "id_token": idToken,
170+ })
171+ }
172+
173+ provider := &ProviderData{
174+ Verifier: oidc.NewVerifier(
175+ oidcIssuer,
176+ mockJWKS{},
177+ &oidc.Config{ClientID: oidcClientID},
178+ ),
179+ }
180+ verified, err := provider.verifyIDToken(context.Background(), token)
181+ if err != nil {
182+ g.Expect(err).To(Equal(tc.ExpectedError))
183+ }
184+
185+ if tc.ExpectIDToken {
186+ g.Expect(verified).ToNot(BeNil())
187+ g.Expect(*verified).To(BeAssignableToTypeOf(oidc.IDToken{}))
188+ } else {
189+ g.Expect(verified).To(BeNil())
190+ }
191+ })
192+ }
193+}
194+
195+func TestProviderData_buildSessionFromClaims(t *testing.T) {
196+ testCases := map[string]struct {
197+ IDToken idTokenClaims
198+ AllowUnverified bool
199+ EmailClaim string
200+ GroupsClaim string
201+ ExpectedError error
202+ ExpectedSession *sessions.SessionState
203+ }{
204+ "Standard": {
205+ IDToken: defaultIDToken,
206+ AllowUnverified: false,
207+ EmailClaim: "email",
208+ GroupsClaim: "groups",
209+ ExpectedSession: &sessions.SessionState{
210+ User: "123456789",
211+ Email: "janed@me.com",
212+ Groups: []string{"test:a", "test:b"},
213+ PreferredUsername: "Jane Dobbs",
214+ },
215+ },
216+ "Unverified Denied": {
217+ IDToken: unverifiedIDToken,
218+ AllowUnverified: false,
219+ EmailClaim: "email",
220+ GroupsClaim: "groups",
221+ ExpectedError: errors.New("email in id_token (unverified@email.com) isn't verified"),
222+ },
223+ "Unverified Allowed": {
224+ IDToken: unverifiedIDToken,
225+ AllowUnverified: true,
226+ EmailClaim: "email",
227+ GroupsClaim: "groups",
228+ ExpectedSession: &sessions.SessionState{
229+ User: "123456789",
230+ Email: "unverified@email.com",
231+ Groups: []string{"test:a", "test:b"},
232+ PreferredUsername: "Mystery Man",
233+ },
234+ },
235+ "Complex Groups": {
236+ IDToken: complexGroupsIDToken,
237+ AllowUnverified: true,
238+ EmailClaim: "email",
239+ GroupsClaim: "groups",
240+ ExpectedSession: &sessions.SessionState{
241+ User: "123456789",
242+ Email: "complex@claims.com",
243+ Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
244+ PreferredUsername: "Complex Claim",
245+ },
246+ },
247+ "Email Claim Switched": {
248+ IDToken: unverifiedIDToken,
249+ AllowUnverified: true,
250+ EmailClaim: "phone_number",
251+ GroupsClaim: "groups",
252+ ExpectedSession: &sessions.SessionState{
253+ User: "123456789",
254+ Email: "+4025205729",
255+ Groups: []string{"test:a", "test:b"},
256+ PreferredUsername: "Mystery Man",
257+ },
258+ },
259+ "Email Claim Switched to Non String": {
260+ IDToken: unverifiedIDToken,
261+ AllowUnverified: true,
262+ EmailClaim: "roles",
263+ GroupsClaim: "groups",
264+ ExpectedSession: &sessions.SessionState{
265+ User: "123456789",
266+ Email: "[test:c test:d]",
267+ Groups: []string{"test:a", "test:b"},
268+ PreferredUsername: "Mystery Man",
269+ },
270+ },
271+ "Email Claim Non Existent": {
272+ IDToken: unverifiedIDToken,
273+ AllowUnverified: true,
274+ EmailClaim: "aksjdfhjksadh",
275+ GroupsClaim: "groups",
276+ ExpectedSession: &sessions.SessionState{
277+ User: "123456789",
278+ Email: "",
279+ Groups: []string{"test:a", "test:b"},
280+ PreferredUsername: "Mystery Man",
281+ },
282+ },
283+ "Groups Claim Switched": {
284+ IDToken: defaultIDToken,
285+ AllowUnverified: false,
286+ EmailClaim: "email",
287+ GroupsClaim: "roles",
288+ ExpectedSession: &sessions.SessionState{
289+ User: "123456789",
290+ Email: "janed@me.com",
291+ Groups: []string{"test:c", "test:d"},
292+ PreferredUsername: "Jane Dobbs",
293+ },
294+ },
295+ "Groups Claim Non Existent": {
296+ IDToken: defaultIDToken,
297+ AllowUnverified: false,
298+ EmailClaim: "email",
299+ GroupsClaim: "alskdjfsalkdjf",
300+ ExpectedSession: &sessions.SessionState{
301+ User: "123456789",
302+ Email: "janed@me.com",
303+ Groups: []string{},
304+ PreferredUsername: "Jane Dobbs",
305+ },
306+ },
307+ }
308+ for testName, tc := range testCases {
309+ t.Run(testName, func(t *testing.T) {
310+ g := NewWithT(t)
311+
312+ provider := &ProviderData{
313+ Verifier: oidc.NewVerifier(
314+ oidcIssuer,
315+ mockJWKS{},
316+ &oidc.Config{ClientID: oidcClientID},
317+ ),
318+ }
319+ provider.AllowUnverifiedEmail = tc.AllowUnverified
320+ provider.EmailClaim = tc.EmailClaim
321+ provider.GroupsClaim = tc.GroupsClaim
322+
323+ rawIDToken, err := newSignedTestIDToken(tc.IDToken)
324+ g.Expect(err).ToNot(HaveOccurred())
325+
326+ idToken, err := provider.Verifier.Verify(context.Background(), rawIDToken)
327+ g.Expect(err).ToNot(HaveOccurred())
328+
329+ ss, err := provider.buildSessionFromClaims(idToken)
330+ if err != nil {
331+ g.Expect(err).To(Equal(tc.ExpectedError))
332+ }
333+ if ss != nil {
334+ g.Expect(ss).To(Equal(tc.ExpectedSession))
335+ }
336+ })
337+ }
338+}
339+
340+func TestProviderData_extractGroups(t *testing.T) {
341+ testCases := map[string]struct {
342+ Claims map[string]interface{}
343+ GroupsClaim string
344+ ExpectedGroups []string
345+ }{
346+ "Standard String Groups": {
347+ Claims: map[string]interface{}{
348+ "email": "this@does.not.matter.com",
349+ "groups": []interface{}{"three", "string", "groups"},
350+ },
351+ GroupsClaim: "groups",
352+ ExpectedGroups: []string{"three", "string", "groups"},
353+ },
354+ "Different Claim Name": {
355+ Claims: map[string]interface{}{
356+ "email": "this@does.not.matter.com",
357+ "roles": []interface{}{"three", "string", "roles"},
358+ },
359+ GroupsClaim: "roles",
360+ ExpectedGroups: []string{"three", "string", "roles"},
361+ },
362+ "Numeric Groups": {
363+ Claims: map[string]interface{}{
364+ "email": "this@does.not.matter.com",
365+ "groups": []interface{}{1, 2, 3},
366+ },
367+ GroupsClaim: "groups",
368+ ExpectedGroups: []string{"1", "2", "3"},
369+ },
370+ "Complex Groups": {
371+ Claims: map[string]interface{}{
372+ "email": "this@does.not.matter.com",
373+ "groups": []interface{}{
374+ map[string]interface{}{
375+ "groupId": "Admin Group Id",
376+ "roles": []string{"Admin"},
377+ },
378+ 12345,
379+ "Just::A::String",
380+ },
381+ },
382+ GroupsClaim: "groups",
383+ ExpectedGroups: []string{
384+ "{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
385+ "12345",
386+ "Just::A::String",
387+ },
388+ },
389+ "Missing Groups": {
390+ Claims: map[string]interface{}{
391+ "email": "this@does.not.matter.com",
392+ },
393+ GroupsClaim: "groups",
394+ ExpectedGroups: []string{},
395+ },
396+ }
397+ for testName, tc := range testCases {
398+ t.Run(testName, func(t *testing.T) {
399+ g := NewWithT(t)
400+
401+ provider := &ProviderData{
402+ Verifier: oidc.NewVerifier(
403+ oidcIssuer,
404+ mockJWKS{},
405+ &oidc.Config{ClientID: oidcClientID},
406+ ),
407+ }
408+ provider.GroupsClaim = tc.GroupsClaim
409+
410+ groups := provider.extractGroups(tc.Claims)
411+ g.Expect(groups).To(Equal(tc.ExpectedGroups))
412+ })
413+ }
414+}