@@ -0,0 +1,414 @@
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"
23+ idToken = "eyJfoobar123.eyJbaz987.IDToken"
24+ accessToken = "eyJfoobar123.eyJbaz987.AccessToken"
25+ refreshToken = "eyJfoobar123.eyJbaz987.RefreshToken"
27+ oidcIssuer = "https://issuer.example.com"
28+ oidcClientID = "https://test.myapp.com"
29+ oidcSecret = "SuperSecret123456789"
31+ failureTokenID = "this-id-fails-verification"
38+ standardClaims = jwt.StandardClaims{
39+ Audience: oidcClientID,
40+ ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
42+ IssuedAt: time.Now().Unix(),
45+ Subject: "123456789",
48+ defaultIDToken = idTokenClaims{
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"},
56+ StandardClaims: standardClaims,
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{}{
66+ "groupId": "Admin Group Id",
67+ "roles": []string{"Admin"},
70+ Roles: []string{"test:simple", "test:roles"},
72+ StandardClaims: standardClaims,
75+ unverifiedIDToken = idTokenClaims{
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,
86+ minimalIDToken = idTokenClaims{
87+ StandardClaims: standardClaims,
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"`
102+type mockJWKS struct{}
104+func (mockJWKS) VerifySignature(_ context.Context, jwt string) ([]byte, error) {
105+ decoded, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
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)
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)
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),
134+func TestProviderData_verifyIDToken(t *testing.T) {
135+ failureIDToken := defaultIDToken
136+ failureIDToken.Id = failureTokenID
138+ testCases := map[string]struct {
139+ IDToken *idTokenClaims
144+ IDToken: &defaultIDToken,
145+ ExpectIDToken: true,
148+ "Invalid ID Token": {
149+ IDToken: &failureIDToken,
150+ ExpectIDToken: false,
151+ ExpectedError: errors.New("failed to verify signature: the validation failed for subject [123456789]"),
153+ "Missing ID Token": {
155+ ExpectIDToken: false,
160+ for testName, tc := range testCases {
161+ t.Run(testName, func(t *testing.T) {
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,
173+ provider := &ProviderData{
174+ Verifier: oidc.NewVerifier(
177+ &oidc.Config{ClientID: oidcClientID},
180+ verified, err := provider.verifyIDToken(context.Background(), token)
182+ g.Expect(err).To(Equal(tc.ExpectedError))
185+ if tc.ExpectIDToken {
186+ g.Expect(verified).ToNot(BeNil())
187+ g.Expect(*verified).To(BeAssignableToTypeOf(oidc.IDToken{}))
189+ g.Expect(verified).To(BeNil())
195+func TestProviderData_buildSessionFromClaims(t *testing.T) {
196+ testCases := map[string]struct {
197+ IDToken idTokenClaims
198+ AllowUnverified bool
202+ ExpectedSession *sessions.SessionState
205+ IDToken: defaultIDToken,
206+ AllowUnverified: false,
207+ EmailClaim: "email",
208+ GroupsClaim: "groups",
209+ ExpectedSession: &sessions.SessionState{
211+ Email: "janed@me.com",
212+ Groups: []string{"test:a", "test:b"},
213+ PreferredUsername: "Jane Dobbs",
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"),
223+ "Unverified Allowed": {
224+ IDToken: unverifiedIDToken,
225+ AllowUnverified: true,
226+ EmailClaim: "email",
227+ GroupsClaim: "groups",
228+ ExpectedSession: &sessions.SessionState{
230+ Email: "unverified@email.com",
231+ Groups: []string{"test:a", "test:b"},
232+ PreferredUsername: "Mystery Man",
236+ IDToken: complexGroupsIDToken,
237+ AllowUnverified: true,
238+ EmailClaim: "email",
239+ GroupsClaim: "groups",
240+ ExpectedSession: &sessions.SessionState{
242+ Email: "complex@claims.com",
243+ Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
244+ PreferredUsername: "Complex Claim",
247+ "Email Claim Switched": {
248+ IDToken: unverifiedIDToken,
249+ AllowUnverified: true,
250+ EmailClaim: "phone_number",
251+ GroupsClaim: "groups",
252+ ExpectedSession: &sessions.SessionState{
254+ Email: "+4025205729",
255+ Groups: []string{"test:a", "test:b"},
256+ PreferredUsername: "Mystery Man",
259+ "Email Claim Switched to Non String": {
260+ IDToken: unverifiedIDToken,
261+ AllowUnverified: true,
262+ EmailClaim: "roles",
263+ GroupsClaim: "groups",
264+ ExpectedSession: &sessions.SessionState{
266+ Email: "[test:c test:d]",
267+ Groups: []string{"test:a", "test:b"},
268+ PreferredUsername: "Mystery Man",
271+ "Email Claim Non Existent": {
272+ IDToken: unverifiedIDToken,
273+ AllowUnverified: true,
274+ EmailClaim: "aksjdfhjksadh",
275+ GroupsClaim: "groups",
276+ ExpectedSession: &sessions.SessionState{
279+ Groups: []string{"test:a", "test:b"},
280+ PreferredUsername: "Mystery Man",
283+ "Groups Claim Switched": {
284+ IDToken: defaultIDToken,
285+ AllowUnverified: false,
286+ EmailClaim: "email",
287+ GroupsClaim: "roles",
288+ ExpectedSession: &sessions.SessionState{
290+ Email: "janed@me.com",
291+ Groups: []string{"test:c", "test:d"},
292+ PreferredUsername: "Jane Dobbs",
295+ "Groups Claim Non Existent": {
296+ IDToken: defaultIDToken,
297+ AllowUnverified: false,
298+ EmailClaim: "email",
299+ GroupsClaim: "alskdjfsalkdjf",
300+ ExpectedSession: &sessions.SessionState{
302+ Email: "janed@me.com",
304+ PreferredUsername: "Jane Dobbs",
308+ for testName, tc := range testCases {
309+ t.Run(testName, func(t *testing.T) {
312+ provider := &ProviderData{
313+ Verifier: oidc.NewVerifier(
316+ &oidc.Config{ClientID: oidcClientID},
319+ provider.AllowUnverifiedEmail = tc.AllowUnverified
320+ provider.EmailClaim = tc.EmailClaim
321+ provider.GroupsClaim = tc.GroupsClaim
323+ rawIDToken, err := newSignedTestIDToken(tc.IDToken)
324+ g.Expect(err).ToNot(HaveOccurred())
326+ idToken, err := provider.Verifier.Verify(context.Background(), rawIDToken)
327+ g.Expect(err).ToNot(HaveOccurred())
329+ ss, err := provider.buildSessionFromClaims(idToken)
331+ g.Expect(err).To(Equal(tc.ExpectedError))
334+ g.Expect(ss).To(Equal(tc.ExpectedSession))
340+func TestProviderData_extractGroups(t *testing.T) {
341+ testCases := map[string]struct {
342+ Claims map[string]interface{}
344+ ExpectedGroups []string
346+ "Standard String Groups": {
347+ Claims: map[string]interface{}{
348+ "email": "this@does.not.matter.com",
349+ "groups": []interface{}{"three", "string", "groups"},
351+ GroupsClaim: "groups",
352+ ExpectedGroups: []string{"three", "string", "groups"},
354+ "Different Claim Name": {
355+ Claims: map[string]interface{}{
356+ "email": "this@does.not.matter.com",
357+ "roles": []interface{}{"three", "string", "roles"},
359+ GroupsClaim: "roles",
360+ ExpectedGroups: []string{"three", "string", "roles"},
363+ Claims: map[string]interface{}{
364+ "email": "this@does.not.matter.com",
365+ "groups": []interface{}{1, 2, 3},
367+ GroupsClaim: "groups",
368+ ExpectedGroups: []string{"1", "2", "3"},
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"},
382+ GroupsClaim: "groups",
383+ ExpectedGroups: []string{
384+ "{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
390+ Claims: map[string]interface{}{
391+ "email": "this@does.not.matter.com",
393+ GroupsClaim: "groups",
394+ ExpectedGroups: []string{},
397+ for testName, tc := range testCases {
398+ t.Run(testName, func(t *testing.T) {
401+ provider := &ProviderData{
402+ Verifier: oidc.NewVerifier(
405+ &oidc.Config{ClientID: oidcClientID},
408+ provider.GroupsClaim = tc.GroupsClaim
410+ groups := provider.extractGroups(tc.Claims)
411+ g.Expect(groups).To(Equal(tc.ExpectedGroups))