You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-08-08 22:46:33 +02:00
Move generic OIDC functionality to be available to all providers
This commit is contained in:
414
providers/provider_data_test.go
Normal file
414
providers/provider_data_test.go
Normal file
@ -0,0 +1,414 @@
|
||||
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
|
||||
ExpectIDToken bool
|
||||
ExpectedError error
|
||||
}{
|
||||
"Valid ID Token": {
|
||||
IDToken: &defaultIDToken,
|
||||
ExpectIDToken: true,
|
||||
ExpectedError: nil,
|
||||
},
|
||||
"Invalid ID Token": {
|
||||
IDToken: &failureIDToken,
|
||||
ExpectIDToken: false,
|
||||
ExpectedError: errors.New("failed to verify signature: the validation failed for subject [123456789]"),
|
||||
},
|
||||
"Missing ID Token": {
|
||||
IDToken: nil,
|
||||
ExpectIDToken: false,
|
||||
ExpectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
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{
|
||||
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: []string{},
|
||||
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": {
|
||||
Claims: map[string]interface{}{
|
||||
"email": "this@does.not.matter.com",
|
||||
},
|
||||
GroupsClaim: "groups",
|
||||
ExpectedGroups: []string{},
|
||||
},
|
||||
}
|
||||
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)
|
||||
g.Expect(groups).To(Equal(tc.ExpectedGroups))
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user