438 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			438 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
| package providers
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/rsa"
 | |
| 	"encoding/base64"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/coreos/go-oidc"
 | |
| 	"github.com/dgrijalva/jwt-go"
 | |
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
 | |
| 	. "github.com/onsi/gomega"
 | |
| 	"golang.org/x/oauth2"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	idToken      = "eyJfoobar123.eyJbaz987.IDToken"
 | |
| 	accessToken  = "eyJfoobar123.eyJbaz987.AccessToken"
 | |
| 	refreshToken = "eyJfoobar123.eyJbaz987.RefreshToken"
 | |
| 
 | |
| 	oidcIssuer   = "https://issuer.example.com"
 | |
| 	oidcClientID = "https://test.myapp.com"
 | |
| 	oidcSecret   = "SuperSecret123456789"
 | |
| 
 | |
| 	failureTokenID = "this-id-fails-verification"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	verified   = true
 | |
| 	unverified = false
 | |
| 
 | |
| 	standardClaims = jwt.StandardClaims{
 | |
| 		Audience:  oidcClientID,
 | |
| 		ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
 | |
| 		Id:        "id-some-id",
 | |
| 		IssuedAt:  time.Now().Unix(),
 | |
| 		Issuer:    oidcIssuer,
 | |
| 		NotBefore: 0,
 | |
| 		Subject:   "123456789",
 | |
| 	}
 | |
| 
 | |
| 	defaultIDToken = idTokenClaims{
 | |
| 		Name:           "Jane Dobbs",
 | |
| 		Email:          "janed@me.com",
 | |
| 		Phone:          "+4798765432",
 | |
| 		Picture:        "http://mugbook.com/janed/me.jpg",
 | |
| 		Groups:         []string{"test:a", "test:b"},
 | |
| 		Roles:          []string{"test:c", "test:d"},
 | |
| 		Verified:       &verified,
 | |
| 		StandardClaims: standardClaims,
 | |
| 	}
 | |
| 
 | |
| 	complexGroupsIDToken = idTokenClaims{
 | |
| 		Name:    "Complex Claim",
 | |
| 		Email:   "complex@claims.com",
 | |
| 		Phone:   "+5439871234",
 | |
| 		Picture: "http://mugbook.com/complex/claims.jpg",
 | |
| 		Groups: []map[string]interface{}{
 | |
| 			{
 | |
| 				"groupId": "Admin Group Id",
 | |
| 				"roles":   []string{"Admin"},
 | |
| 			},
 | |
| 		},
 | |
| 		Roles:          []string{"test:simple", "test:roles"},
 | |
| 		Verified:       &verified,
 | |
| 		StandardClaims: standardClaims,
 | |
| 	}
 | |
| 
 | |
| 	unverifiedIDToken = idTokenClaims{
 | |
| 		Name:           "Mystery Man",
 | |
| 		Email:          "unverified@email.com",
 | |
| 		Phone:          "+4025205729",
 | |
| 		Picture:        "http://mugbook.com/unverified/email.jpg",
 | |
| 		Groups:         []string{"test:a", "test:b"},
 | |
| 		Roles:          []string{"test:c", "test:d"},
 | |
| 		Verified:       &unverified,
 | |
| 		StandardClaims: standardClaims,
 | |
| 	}
 | |
| 
 | |
| 	minimalIDToken = idTokenClaims{
 | |
| 		StandardClaims: standardClaims,
 | |
| 	}
 | |
| )
 | |
| 
 | |
| type idTokenClaims struct {
 | |
| 	Name     string      `json:"preferred_username,omitempty"`
 | |
| 	Email    string      `json:"email,omitempty"`
 | |
| 	Phone    string      `json:"phone_number,omitempty"`
 | |
| 	Picture  string      `json:"picture,omitempty"`
 | |
| 	Groups   interface{} `json:"groups,omitempty"`
 | |
| 	Roles    interface{} `json:"roles,omitempty"`
 | |
| 	Verified *bool       `json:"email_verified,omitempty"`
 | |
| 	jwt.StandardClaims
 | |
| }
 | |
| 
 | |
| type mockJWKS struct{}
 | |
| 
 | |
| func (mockJWKS) VerifySignature(_ context.Context, jwt string) ([]byte, error) {
 | |
| 	decoded, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	tokenClaims := &idTokenClaims{}
 | |
| 	err = json.Unmarshal(decoded, tokenClaims)
 | |
| 	if err != nil || tokenClaims.Id == failureTokenID {
 | |
| 		return nil, fmt.Errorf("the validation failed for subject [%v]", tokenClaims.Subject)
 | |
| 	}
 | |
| 
 | |
| 	return decoded, nil
 | |
| }
 | |
| 
 | |
| func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
 | |
| 	key, _ := rsa.GenerateKey(rand.Reader, 2048)
 | |
| 	standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
 | |
| 	return standardClaims.SignedString(key)
 | |
| }
 | |
| 
 | |
| func newTestOauth2Token() *oauth2.Token {
 | |
| 	return &oauth2.Token{
 | |
| 		AccessToken:  accessToken,
 | |
| 		TokenType:    "Bearer",
 | |
| 		RefreshToken: refreshToken,
 | |
| 		Expiry:       time.Time{}.Add(time.Duration(5) * time.Second),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestProviderData_verifyIDToken(t *testing.T) {
 | |
| 	failureIDToken := defaultIDToken
 | |
| 	failureIDToken.Id = failureTokenID
 | |
| 
 | |
| 	testCases := map[string]struct {
 | |
| 		IDToken       *idTokenClaims
 | |
| 		Verifier      bool
 | |
| 		ExpectIDToken bool
 | |
| 		ExpectedError error
 | |
| 	}{
 | |
| 		"Valid ID Token": {
 | |
| 			IDToken:       &defaultIDToken,
 | |
| 			Verifier:      true,
 | |
| 			ExpectIDToken: true,
 | |
| 			ExpectedError: nil,
 | |
| 		},
 | |
| 		"Invalid ID Token": {
 | |
| 			IDToken:       &failureIDToken,
 | |
| 			Verifier:      true,
 | |
| 			ExpectIDToken: false,
 | |
| 			ExpectedError: errors.New("failed to verify signature: the validation failed for subject [123456789]"),
 | |
| 		},
 | |
| 		"Missing ID Token": {
 | |
| 			IDToken:       nil,
 | |
| 			Verifier:      true,
 | |
| 			ExpectIDToken: false,
 | |
| 			ExpectedError: ErrMissingIDToken,
 | |
| 		},
 | |
| 		"OIDC Verifier not Configured": {
 | |
| 			IDToken:       &defaultIDToken,
 | |
| 			Verifier:      false,
 | |
| 			ExpectIDToken: false,
 | |
| 			ExpectedError: ErrMissingOIDCVerifier,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for testName, tc := range testCases {
 | |
| 		t.Run(testName, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			token := newTestOauth2Token()
 | |
| 			if tc.IDToken != nil {
 | |
| 				idToken, err := newSignedTestIDToken(*tc.IDToken)
 | |
| 				g.Expect(err).ToNot(HaveOccurred())
 | |
| 				token = token.WithExtra(map[string]interface{}{
 | |
| 					"id_token": idToken,
 | |
| 				})
 | |
| 			}
 | |
| 
 | |
| 			provider := &ProviderData{}
 | |
| 			if tc.Verifier {
 | |
| 				provider.Verifier = oidc.NewVerifier(
 | |
| 					oidcIssuer,
 | |
| 					mockJWKS{},
 | |
| 					&oidc.Config{ClientID: oidcClientID},
 | |
| 				)
 | |
| 			}
 | |
| 			verified, err := provider.verifyIDToken(context.Background(), token)
 | |
| 			if err != nil {
 | |
| 				g.Expect(err).To(Equal(tc.ExpectedError))
 | |
| 			}
 | |
| 
 | |
| 			if tc.ExpectIDToken {
 | |
| 				g.Expect(verified).ToNot(BeNil())
 | |
| 				g.Expect(*verified).To(BeAssignableToTypeOf(oidc.IDToken{}))
 | |
| 			} else {
 | |
| 				g.Expect(verified).To(BeNil())
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestProviderData_buildSessionFromClaims(t *testing.T) {
 | |
| 	testCases := map[string]struct {
 | |
| 		IDToken         idTokenClaims
 | |
| 		AllowUnverified bool
 | |
| 		EmailClaim      string
 | |
| 		GroupsClaim     string
 | |
| 		ExpectedError   error
 | |
| 		ExpectedSession *sessions.SessionState
 | |
| 	}{
 | |
| 		"Standard": {
 | |
| 			IDToken:         defaultIDToken,
 | |
| 			AllowUnverified: false,
 | |
| 			EmailClaim:      "email",
 | |
| 			GroupsClaim:     "groups",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "janed@me.com",
 | |
| 				Groups:            []string{"test:a", "test:b"},
 | |
| 				PreferredUsername: "Jane Dobbs",
 | |
| 			},
 | |
| 		},
 | |
| 		"Unverified Denied": {
 | |
| 			IDToken:         unverifiedIDToken,
 | |
| 			AllowUnverified: false,
 | |
| 			EmailClaim:      "email",
 | |
| 			GroupsClaim:     "groups",
 | |
| 			ExpectedError:   errors.New("email in id_token (unverified@email.com) isn't verified"),
 | |
| 		},
 | |
| 		"Unverified Allowed": {
 | |
| 			IDToken:         unverifiedIDToken,
 | |
| 			AllowUnverified: true,
 | |
| 			EmailClaim:      "email",
 | |
| 			GroupsClaim:     "groups",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "unverified@email.com",
 | |
| 				Groups:            []string{"test:a", "test:b"},
 | |
| 				PreferredUsername: "Mystery Man",
 | |
| 			},
 | |
| 		},
 | |
| 		"Complex Groups": {
 | |
| 			IDToken:         complexGroupsIDToken,
 | |
| 			AllowUnverified: true,
 | |
| 			EmailClaim:      "email",
 | |
| 			GroupsClaim:     "groups",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "complex@claims.com",
 | |
| 				Groups:            []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
 | |
| 				PreferredUsername: "Complex Claim",
 | |
| 			},
 | |
| 		},
 | |
| 		"Email Claim Switched": {
 | |
| 			IDToken:         unverifiedIDToken,
 | |
| 			AllowUnverified: true,
 | |
| 			EmailClaim:      "phone_number",
 | |
| 			GroupsClaim:     "groups",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "+4025205729",
 | |
| 				Groups:            []string{"test:a", "test:b"},
 | |
| 				PreferredUsername: "Mystery Man",
 | |
| 			},
 | |
| 		},
 | |
| 		"Email Claim Switched to Non String": {
 | |
| 			IDToken:         unverifiedIDToken,
 | |
| 			AllowUnverified: true,
 | |
| 			EmailClaim:      "roles",
 | |
| 			GroupsClaim:     "groups",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "[test:c test:d]",
 | |
| 				Groups:            []string{"test:a", "test:b"},
 | |
| 				PreferredUsername: "Mystery Man",
 | |
| 			},
 | |
| 		},
 | |
| 		"Email Claim Non Existent": {
 | |
| 			IDToken:         unverifiedIDToken,
 | |
| 			AllowUnverified: true,
 | |
| 			EmailClaim:      "aksjdfhjksadh",
 | |
| 			GroupsClaim:     "groups",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "",
 | |
| 				Groups:            []string{"test:a", "test:b"},
 | |
| 				PreferredUsername: "Mystery Man",
 | |
| 			},
 | |
| 		},
 | |
| 		"Groups Claim Switched": {
 | |
| 			IDToken:         defaultIDToken,
 | |
| 			AllowUnverified: false,
 | |
| 			EmailClaim:      "email",
 | |
| 			GroupsClaim:     "roles",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "janed@me.com",
 | |
| 				Groups:            []string{"test:c", "test:d"},
 | |
| 				PreferredUsername: "Jane Dobbs",
 | |
| 			},
 | |
| 		},
 | |
| 		"Groups Claim Non Existent": {
 | |
| 			IDToken:         defaultIDToken,
 | |
| 			AllowUnverified: false,
 | |
| 			EmailClaim:      "email",
 | |
| 			GroupsClaim:     "alskdjfsalkdjf",
 | |
| 			ExpectedSession: &sessions.SessionState{
 | |
| 				User:              "123456789",
 | |
| 				Email:             "janed@me.com",
 | |
| 				Groups:            nil,
 | |
| 				PreferredUsername: "Jane Dobbs",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	for testName, tc := range testCases {
 | |
| 		t.Run(testName, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			provider := &ProviderData{
 | |
| 				Verifier: oidc.NewVerifier(
 | |
| 					oidcIssuer,
 | |
| 					mockJWKS{},
 | |
| 					&oidc.Config{ClientID: oidcClientID},
 | |
| 				),
 | |
| 			}
 | |
| 			provider.AllowUnverifiedEmail = tc.AllowUnverified
 | |
| 			provider.EmailClaim = tc.EmailClaim
 | |
| 			provider.GroupsClaim = tc.GroupsClaim
 | |
| 
 | |
| 			rawIDToken, err := newSignedTestIDToken(tc.IDToken)
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 
 | |
| 			idToken, err := provider.Verifier.Verify(context.Background(), rawIDToken)
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 
 | |
| 			ss, err := provider.buildSessionFromClaims(idToken)
 | |
| 			if err != nil {
 | |
| 				g.Expect(err).To(Equal(tc.ExpectedError))
 | |
| 			}
 | |
| 			if ss != nil {
 | |
| 				g.Expect(ss).To(Equal(tc.ExpectedSession))
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestProviderData_extractGroups(t *testing.T) {
 | |
| 	testCases := map[string]struct {
 | |
| 		Claims         map[string]interface{}
 | |
| 		GroupsClaim    string
 | |
| 		ExpectedGroups []string
 | |
| 	}{
 | |
| 		"Standard String Groups": {
 | |
| 			Claims: map[string]interface{}{
 | |
| 				"email":  "this@does.not.matter.com",
 | |
| 				"groups": []interface{}{"three", "string", "groups"},
 | |
| 			},
 | |
| 			GroupsClaim:    "groups",
 | |
| 			ExpectedGroups: []string{"three", "string", "groups"},
 | |
| 		},
 | |
| 		"Different Claim Name": {
 | |
| 			Claims: map[string]interface{}{
 | |
| 				"email": "this@does.not.matter.com",
 | |
| 				"roles": []interface{}{"three", "string", "roles"},
 | |
| 			},
 | |
| 			GroupsClaim:    "roles",
 | |
| 			ExpectedGroups: []string{"three", "string", "roles"},
 | |
| 		},
 | |
| 		"Numeric Groups": {
 | |
| 			Claims: map[string]interface{}{
 | |
| 				"email":  "this@does.not.matter.com",
 | |
| 				"groups": []interface{}{1, 2, 3},
 | |
| 			},
 | |
| 			GroupsClaim:    "groups",
 | |
| 			ExpectedGroups: []string{"1", "2", "3"},
 | |
| 		},
 | |
| 		"Complex Groups": {
 | |
| 			Claims: map[string]interface{}{
 | |
| 				"email": "this@does.not.matter.com",
 | |
| 				"groups": []interface{}{
 | |
| 					map[string]interface{}{
 | |
| 						"groupId": "Admin Group Id",
 | |
| 						"roles":   []string{"Admin"},
 | |
| 					},
 | |
| 					12345,
 | |
| 					"Just::A::String",
 | |
| 				},
 | |
| 			},
 | |
| 			GroupsClaim: "groups",
 | |
| 			ExpectedGroups: []string{
 | |
| 				"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
 | |
| 				"12345",
 | |
| 				"Just::A::String",
 | |
| 			},
 | |
| 		},
 | |
| 		"Missing Groups Claim Returns Nil": {
 | |
| 			Claims: map[string]interface{}{
 | |
| 				"email": "this@does.not.matter.com",
 | |
| 			},
 | |
| 			GroupsClaim:    "groups",
 | |
| 			ExpectedGroups: nil,
 | |
| 		},
 | |
| 		"Non List Groups": {
 | |
| 			Claims: map[string]interface{}{
 | |
| 				"email":  "this@does.not.matter.com",
 | |
| 				"groups": "singleton",
 | |
| 			},
 | |
| 			GroupsClaim:    "groups",
 | |
| 			ExpectedGroups: []string{"singleton"},
 | |
| 		},
 | |
| 	}
 | |
| 	for testName, tc := range testCases {
 | |
| 		t.Run(testName, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			provider := &ProviderData{
 | |
| 				Verifier: oidc.NewVerifier(
 | |
| 					oidcIssuer,
 | |
| 					mockJWKS{},
 | |
| 					&oidc.Config{ClientID: oidcClientID},
 | |
| 				),
 | |
| 			}
 | |
| 			provider.GroupsClaim = tc.GroupsClaim
 | |
| 
 | |
| 			groups := provider.extractGroups(tc.Claims)
 | |
| 			if tc.ExpectedGroups != nil {
 | |
| 				g.Expect(groups).To(Equal(tc.ExpectedGroups))
 | |
| 			} else {
 | |
| 				g.Expect(groups).To(BeNil())
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |