mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-03-23 21:50:48 +02:00
Move generic OIDC functionality to be available to all providers
This commit is contained in:
parent
a1877434b2
commit
74ac4274c6
@ -233,7 +233,10 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
|
|||||||
p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
|
p.ValidateURL, msgs = parseURL(o.ValidateURL, "validate", msgs)
|
||||||
p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
|
p.ProtectedResource, msgs = parseURL(o.ProtectedResource, "resource", msgs)
|
||||||
|
|
||||||
// Make the OIDC Verifier accessible to all providers that can support it
|
// Make the OIDC options available to all providers that support it
|
||||||
|
p.AllowUnverifiedEmail = o.InsecureOIDCAllowUnverifiedEmail
|
||||||
|
p.EmailClaim = o.UserIDClaim
|
||||||
|
p.GroupsClaim = o.OIDCGroupsClaim
|
||||||
p.Verifier = o.GetOIDCVerifier()
|
p.Verifier = o.GetOIDCVerifier()
|
||||||
|
|
||||||
p.SetAllowedGroups(o.AllowedGroups)
|
p.SetAllowedGroups(o.AllowedGroups)
|
||||||
|
@ -4,26 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const emailClaim = "email"
|
|
||||||
|
|
||||||
// OIDCProvider represents an OIDC based Identity Provider
|
// OIDCProvider represents an OIDC based Identity Provider
|
||||||
type OIDCProvider struct {
|
type OIDCProvider struct {
|
||||||
*ProviderData
|
*ProviderData
|
||||||
|
|
||||||
AllowUnverifiedEmail bool
|
|
||||||
EmailClaim string
|
|
||||||
GroupsClaim string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOIDCProvider initiates a new OIDCProvider
|
// NewOIDCProvider initiates a new OIDCProvider
|
||||||
@ -213,7 +204,7 @@ func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string)
|
|||||||
// createSession takes an oauth2.Token and creates a SessionState from it.
|
// createSession takes an oauth2.Token and creates a SessionState from it.
|
||||||
// It alters behavior if called from Redeem vs Refresh
|
// It alters behavior if called from Redeem vs Refresh
|
||||||
func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, refresh bool) (*sessions.SessionState, error) {
|
func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, refresh bool) (*sessions.SessionState, error) {
|
||||||
idToken, err := p.findVerifiedIDToken(ctx, token)
|
idToken, err := p.verifyIDToken(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not verify id_token: %v", err)
|
return nil, fmt.Errorf("could not verify id_token: %v", err)
|
||||||
}
|
}
|
||||||
@ -238,90 +229,3 @@ func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, r
|
|||||||
|
|
||||||
return ss, nil
|
return ss, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OIDCProvider) findVerifiedIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
|
|
||||||
rawIDToken := getIDToken(token)
|
|
||||||
if strings.TrimSpace(rawIDToken) != "" {
|
|
||||||
return p.Verifier.Verify(ctx, rawIDToken)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildSessionFromClaims uses IDToken claims to populate a fresh SessionState
|
|
||||||
// with non-Token related fields.
|
|
||||||
func (p *OIDCProvider) buildSessionFromClaims(idToken *oidc.IDToken) (*sessions.SessionState, error) {
|
|
||||||
ss := &sessions.SessionState{}
|
|
||||||
|
|
||||||
if idToken == nil {
|
|
||||||
return ss, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, err := p.getClaims(idToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("couldn't extract claims from id_token (%v)", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ss.User = claims.Subject
|
|
||||||
ss.Email = claims.Email
|
|
||||||
ss.Groups = claims.Groups
|
|
||||||
|
|
||||||
// TODO (@NickMeves) Deprecate for dynamic claim to session mapping
|
|
||||||
if pref, ok := claims.rawClaims["preferred_username"].(string); ok {
|
|
||||||
ss.PreferredUsername = pref
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyEmail := (p.EmailClaim == emailClaim) && !p.AllowUnverifiedEmail
|
|
||||||
if verifyEmail && claims.Verified != nil && !*claims.Verified {
|
|
||||||
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ss, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type OIDCClaims struct {
|
|
||||||
Subject string `json:"sub"`
|
|
||||||
Email string `json:"-"`
|
|
||||||
Groups []string `json:"-"`
|
|
||||||
Verified *bool `json:"email_verified"`
|
|
||||||
|
|
||||||
rawClaims map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getClaims extracts IDToken claims into an OIDCClaims
|
|
||||||
func (p *OIDCProvider) getClaims(idToken *oidc.IDToken) (*OIDCClaims, error) {
|
|
||||||
claims := &OIDCClaims{}
|
|
||||||
|
|
||||||
// Extract default claims.
|
|
||||||
if err := idToken.Claims(&claims); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse default id_token claims: %v", err)
|
|
||||||
}
|
|
||||||
// Extract custom claims.
|
|
||||||
if err := idToken.Claims(&claims.rawClaims); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse all id_token claims: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
email := claims.rawClaims[p.EmailClaim]
|
|
||||||
if email != nil {
|
|
||||||
claims.Email = fmt.Sprint(email)
|
|
||||||
}
|
|
||||||
claims.Groups = p.extractGroups(claims.rawClaims)
|
|
||||||
|
|
||||||
return claims, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *OIDCProvider) extractGroups(claims map[string]interface{}) []string {
|
|
||||||
groups := []string{}
|
|
||||||
rawGroups, ok := claims[p.GroupsClaim].([]interface{})
|
|
||||||
if rawGroups != nil && ok {
|
|
||||||
for _, rawGroup := range rawGroups {
|
|
||||||
formattedGroup, err := formatGroup(rawGroup)
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("Warning: unable to format group of type %s with error %s",
|
|
||||||
reflect.TypeOf(rawGroup), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
groups = append(groups, formattedGroup)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groups
|
|
||||||
}
|
|
||||||
|
@ -2,42 +2,18 @@ package providers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
|
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const accessToken = "access_token"
|
|
||||||
const refreshToken = "refresh_token"
|
|
||||||
const clientID = "https://test.myapp.com"
|
|
||||||
const secret = "secret"
|
|
||||||
|
|
||||||
type idTokenClaims struct {
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Email string `json:"email,omitempty"`
|
|
||||||
Phone string `json:"phone_number,omitempty"`
|
|
||||||
Picture string `json:"picture,omitempty"`
|
|
||||||
Groups interface{} `json:"groups,omitempty"`
|
|
||||||
OtherGroups interface{} `json:"other_groups,omitempty"`
|
|
||||||
jwt.StandardClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
type redeemTokenResponse struct {
|
type redeemTokenResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
@ -46,88 +22,12 @@ type redeemTokenResponse struct {
|
|||||||
IDToken string `json:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultIDToken idTokenClaims = idTokenClaims{
|
|
||||||
"Jane Dobbs",
|
|
||||||
"janed@me.com",
|
|
||||||
"+4798765432",
|
|
||||||
"http://mugbook.com/janed/me.jpg",
|
|
||||||
[]string{"test:a", "test:b"},
|
|
||||||
[]string{"test:c", "test:d"},
|
|
||||||
jwt.StandardClaims{
|
|
||||||
Audience: "https://test.myapp.com",
|
|
||||||
ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
|
|
||||||
Id: "id-some-id",
|
|
||||||
IssuedAt: time.Now().Unix(),
|
|
||||||
Issuer: "https://issuer.example.com",
|
|
||||||
NotBefore: 0,
|
|
||||||
Subject: "123456789",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var customGroupClaimIDToken idTokenClaims = idTokenClaims{
|
|
||||||
"Jane Dobbs",
|
|
||||||
"janed@me.com",
|
|
||||||
"+4798765432",
|
|
||||||
"http://mugbook.com/janed/me.jpg",
|
|
||||||
[]map[string]interface{}{
|
|
||||||
{
|
|
||||||
"groupId": "Admin Group Id",
|
|
||||||
"roles": []string{"Admin"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[]string{"test:c", "test:d"},
|
|
||||||
jwt.StandardClaims{
|
|
||||||
Audience: "https://test.myapp.com",
|
|
||||||
ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
|
|
||||||
Id: "id-some-id",
|
|
||||||
IssuedAt: time.Now().Unix(),
|
|
||||||
Issuer: "https://issuer.example.com",
|
|
||||||
NotBefore: 0,
|
|
||||||
Subject: "123456789",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var minimalIDToken idTokenClaims = idTokenClaims{
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
[]string{},
|
|
||||||
[]string{},
|
|
||||||
jwt.StandardClaims{
|
|
||||||
Audience: "https://test.myapp.com",
|
|
||||||
ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
|
|
||||||
Id: "id-some-id",
|
|
||||||
IssuedAt: time.Now().Unix(),
|
|
||||||
Issuer: "https://issuer.example.com",
|
|
||||||
NotBefore: 0,
|
|
||||||
Subject: "minimal",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeKeySetStub struct{}
|
|
||||||
|
|
||||||
func (fakeKeySetStub) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
|
|
||||||
decodeString, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tokenClaims := &idTokenClaims{}
|
|
||||||
err = json.Unmarshal(decodeString, tokenClaims)
|
|
||||||
|
|
||||||
if err != nil || tokenClaims.Id == "this-id-fails-validation" {
|
|
||||||
return nil, fmt.Errorf("the validation failed for subject [%v]", tokenClaims.Subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decodeString, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
|
func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
|
||||||
|
|
||||||
providerData := &ProviderData{
|
providerData := &ProviderData{
|
||||||
ProviderName: "oidc",
|
ProviderName: "oidc",
|
||||||
ClientID: clientID,
|
ClientID: oidcClientID,
|
||||||
ClientSecret: secret,
|
ClientSecret: oidcSecret,
|
||||||
LoginURL: &url.URL{
|
LoginURL: &url.URL{
|
||||||
Scheme: serverURL.Scheme,
|
Scheme: serverURL.Scheme,
|
||||||
Host: serverURL.Host,
|
Host: serverURL.Host,
|
||||||
@ -145,17 +45,16 @@ func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
|
|||||||
Host: serverURL.Host,
|
Host: serverURL.Host,
|
||||||
Path: "/api"},
|
Path: "/api"},
|
||||||
Scope: "openid profile offline_access",
|
Scope: "openid profile offline_access",
|
||||||
|
EmailClaim: "email",
|
||||||
|
GroupsClaim: "groups",
|
||||||
Verifier: oidc.NewVerifier(
|
Verifier: oidc.NewVerifier(
|
||||||
"https://issuer.example.com",
|
oidcIssuer,
|
||||||
fakeKeySetStub{},
|
mockJWKS{},
|
||||||
&oidc.Config{ClientID: clientID},
|
&oidc.Config{ClientID: oidcClientID},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &OIDCProvider{
|
p := &OIDCProvider{ProviderData: providerData}
|
||||||
ProviderData: providerData,
|
|
||||||
EmailClaim: "email",
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
@ -169,21 +68,6 @@ func newOIDCServer(body []byte) (*url.URL, *httptest.Server) {
|
|||||||
return u, s
|
return u, s
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
|
|
||||||
key, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
|
|
||||||
return standardClaims.SignedString(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOauth2Token() *oauth2.Token {
|
|
||||||
return &oauth2.Token{
|
|
||||||
AccessToken: accessToken,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
RefreshToken: refreshToken,
|
|
||||||
Expiry: time.Time{}.Add(time.Duration(5) * time.Second),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestSetup(body []byte) (*httptest.Server, *OIDCProvider) {
|
func newTestSetup(body []byte) (*httptest.Server, *OIDCProvider) {
|
||||||
redeemURL, server := newOIDCServer(body)
|
redeemURL, server := newOIDCServer(body)
|
||||||
provider := newOIDCProvider(redeemURL)
|
provider := newOIDCProvider(redeemURL)
|
||||||
@ -234,12 +118,6 @@ func TestOIDCProviderRedeem_custom_userid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCProvider_EnrichSession(t *testing.T) {
|
func TestOIDCProvider_EnrichSession(t *testing.T) {
|
||||||
const (
|
|
||||||
idToken = "Unchanged ID Token"
|
|
||||||
accessToken = "Unchanged Access Token"
|
|
||||||
refreshToken = "Unchanged Refresh Token"
|
|
||||||
)
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
ExistingSession *sessions.SessionState
|
ExistingSession *sessions.SessionState
|
||||||
EmailClaim string
|
EmailClaim string
|
||||||
@ -550,8 +428,6 @@ func TestOIDCProviderRefreshSessionIfNeededWithIdToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
|
func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
|
||||||
const profileURLEmail = "janed@me.com"
|
|
||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
IDToken idTokenClaims
|
IDToken idTokenClaims
|
||||||
GroupsClaim string
|
GroupsClaim string
|
||||||
@ -562,36 +438,35 @@ func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
|
|||||||
"Default IDToken": {
|
"Default IDToken": {
|
||||||
IDToken: defaultIDToken,
|
IDToken: defaultIDToken,
|
||||||
GroupsClaim: "groups",
|
GroupsClaim: "groups",
|
||||||
ExpectedUser: defaultIDToken.Subject,
|
ExpectedUser: "123456789",
|
||||||
ExpectedEmail: defaultIDToken.Email,
|
ExpectedEmail: "janed@me.com",
|
||||||
ExpectedGroups: []string{"test:a", "test:b"},
|
ExpectedGroups: []string{"test:a", "test:b"},
|
||||||
},
|
},
|
||||||
"Minimal IDToken with no email claim": {
|
"Minimal IDToken with no email claim": {
|
||||||
IDToken: minimalIDToken,
|
IDToken: minimalIDToken,
|
||||||
GroupsClaim: "groups",
|
GroupsClaim: "groups",
|
||||||
ExpectedUser: minimalIDToken.Subject,
|
ExpectedUser: "123456789",
|
||||||
ExpectedEmail: minimalIDToken.Subject,
|
ExpectedEmail: "123456789",
|
||||||
ExpectedGroups: []string{},
|
ExpectedGroups: []string{},
|
||||||
},
|
},
|
||||||
"Custom Groups Claim": {
|
"Custom Groups Claim": {
|
||||||
IDToken: defaultIDToken,
|
IDToken: defaultIDToken,
|
||||||
GroupsClaim: "other_groups",
|
GroupsClaim: "roles",
|
||||||
ExpectedUser: defaultIDToken.Subject,
|
ExpectedUser: "123456789",
|
||||||
ExpectedEmail: defaultIDToken.Email,
|
ExpectedEmail: "janed@me.com",
|
||||||
ExpectedGroups: []string{"test:c", "test:d"},
|
ExpectedGroups: []string{"test:c", "test:d"},
|
||||||
},
|
},
|
||||||
"Custom Groups Claim2": {
|
"Complex Groups Claim": {
|
||||||
IDToken: customGroupClaimIDToken,
|
IDToken: complexGroupsIDToken,
|
||||||
GroupsClaim: "groups",
|
GroupsClaim: "groups",
|
||||||
ExpectedUser: customGroupClaimIDToken.Subject,
|
ExpectedUser: "123456789",
|
||||||
ExpectedEmail: customGroupClaimIDToken.Email,
|
ExpectedEmail: "complex@claims.com",
|
||||||
ExpectedGroups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
|
ExpectedGroups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for testName, tc := range testCases {
|
for testName, tc := range testCases {
|
||||||
t.Run(testName, func(t *testing.T) {
|
t.Run(testName, func(t *testing.T) {
|
||||||
jsonResp := []byte(fmt.Sprintf(`{"email":"%s"}`, profileURLEmail))
|
server, provider := newTestSetup([]byte(`{}`))
|
||||||
server, provider := newTestSetup(jsonResp)
|
|
||||||
provider.GroupsClaim = tc.GroupsClaim
|
provider.GroupsClaim = tc.GroupsClaim
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
@ -610,40 +485,3 @@ func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCProvider_findVerifiedIDToken(t *testing.T) {
|
|
||||||
|
|
||||||
server, provider := newTestSetup([]byte(""))
|
|
||||||
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
token := newOauth2Token()
|
|
||||||
signedIDToken, _ := newSignedTestIDToken(defaultIDToken)
|
|
||||||
tokenWithIDToken := token.WithExtra(map[string]interface{}{
|
|
||||||
"id_token": signedIDToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
verifiedIDToken, err := provider.findVerifiedIDToken(context.Background(), tokenWithIDToken)
|
|
||||||
assert.Equal(t, true, err == nil)
|
|
||||||
if verifiedIDToken == nil {
|
|
||||||
t.Fatal("verifiedIDToken is nil")
|
|
||||||
}
|
|
||||||
assert.Equal(t, defaultIDToken.Issuer, verifiedIDToken.Issuer)
|
|
||||||
assert.Equal(t, defaultIDToken.Subject, verifiedIDToken.Subject)
|
|
||||||
|
|
||||||
// When the validation fails the response should be nil
|
|
||||||
defaultIDToken.Id = "this-id-fails-validation"
|
|
||||||
signedIDToken, _ = newSignedTestIDToken(defaultIDToken)
|
|
||||||
tokenWithIDToken = token.WithExtra(map[string]interface{}{
|
|
||||||
"id_token": signedIDToken,
|
|
||||||
})
|
|
||||||
|
|
||||||
verifiedIDToken, err = provider.findVerifiedIDToken(context.Background(), tokenWithIDToken)
|
|
||||||
assert.Equal(t, errors.New("failed to verify signature: the validation failed for subject [123456789]"), err)
|
|
||||||
assert.Equal(t, true, verifiedIDToken == nil)
|
|
||||||
|
|
||||||
// When there is no id token in the oauth token
|
|
||||||
verifiedIDToken, err = provider.findVerifiedIDToken(context.Background(), newOauth2Token())
|
|
||||||
assert.Equal(t, nil, err)
|
|
||||||
assert.Equal(t, true, verifiedIDToken == nil)
|
|
||||||
}
|
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
package providers
|
package providers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc"
|
"github.com/coreos/go-oidc"
|
||||||
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProviderData contains information required to configure all implementations
|
// ProviderData contains information required to configure all implementations
|
||||||
@ -27,6 +33,11 @@ type ProviderData struct {
|
|||||||
ClientSecretFile string
|
ClientSecretFile string
|
||||||
Scope string
|
Scope string
|
||||||
Prompt string
|
Prompt string
|
||||||
|
|
||||||
|
// Common OIDC options for any OIDC-based providers to consume
|
||||||
|
AllowUnverifiedEmail bool
|
||||||
|
EmailClaim string
|
||||||
|
GroupsClaim string
|
||||||
Verifier *oidc.IDTokenVerifier
|
Verifier *oidc.IDTokenVerifier
|
||||||
|
|
||||||
// Universal Group authorization data structure
|
// Universal Group authorization data structure
|
||||||
@ -94,3 +105,99 @@ func defaultURL(u *url.URL, d *url.URL) *url.URL {
|
|||||||
}
|
}
|
||||||
return &url.URL{}
|
return &url.URL{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ****************************************************************************
|
||||||
|
// These private OIDC helper methods are available to any providers that are
|
||||||
|
// OIDC compliant
|
||||||
|
// ****************************************************************************
|
||||||
|
|
||||||
|
// OIDCClaims is a struct to unmarshal the OIDC claims from an ID Token payload
|
||||||
|
type OIDCClaims struct {
|
||||||
|
Subject string `json:"sub"`
|
||||||
|
Email string `json:"-"`
|
||||||
|
Groups []string `json:"-"`
|
||||||
|
Verified *bool `json:"email_verified"`
|
||||||
|
|
||||||
|
raw map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProviderData) verifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
|
||||||
|
rawIDToken := getIDToken(token)
|
||||||
|
if strings.TrimSpace(rawIDToken) != "" {
|
||||||
|
return p.Verifier.Verify(ctx, rawIDToken)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSessionFromClaims uses IDToken claims to populate a fresh SessionState
|
||||||
|
// with non-Token related fields.
|
||||||
|
func (p *ProviderData) buildSessionFromClaims(idToken *oidc.IDToken) (*sessions.SessionState, error) {
|
||||||
|
ss := &sessions.SessionState{}
|
||||||
|
|
||||||
|
if idToken == nil {
|
||||||
|
return ss, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := p.getClaims(idToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't extract claims from id_token (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ss.User = claims.Subject
|
||||||
|
ss.Email = claims.Email
|
||||||
|
ss.Groups = claims.Groups
|
||||||
|
|
||||||
|
// TODO (@NickMeves) Deprecate for dynamic claim to session mapping
|
||||||
|
if pref, ok := claims.raw["preferred_username"].(string); ok {
|
||||||
|
ss.PreferredUsername = pref
|
||||||
|
}
|
||||||
|
|
||||||
|
// `email_verified` must be present and explicitly set to `false` to be
|
||||||
|
// considered unverified.
|
||||||
|
verifyEmail := (p.EmailClaim == emailClaim) && !p.AllowUnverifiedEmail
|
||||||
|
if verifyEmail && claims.Verified != nil && !*claims.Verified {
|
||||||
|
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ss, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClaims extracts IDToken claims into an OIDCClaims
|
||||||
|
func (p *ProviderData) getClaims(idToken *oidc.IDToken) (*OIDCClaims, error) {
|
||||||
|
claims := &OIDCClaims{}
|
||||||
|
|
||||||
|
// Extract default claims.
|
||||||
|
if err := idToken.Claims(&claims); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse default id_token claims: %v", err)
|
||||||
|
}
|
||||||
|
// Extract custom claims.
|
||||||
|
if err := idToken.Claims(&claims.raw); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse all id_token claims: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
email := claims.raw[p.EmailClaim]
|
||||||
|
if email != nil {
|
||||||
|
claims.Email = fmt.Sprint(email)
|
||||||
|
}
|
||||||
|
claims.Groups = p.extractGroups(claims.raw)
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractGroups extracts groups from a claim to a list in a type safe manner
|
||||||
|
func (p *ProviderData) extractGroups(claims map[string]interface{}) []string {
|
||||||
|
groups := []string{}
|
||||||
|
rawGroups, ok := claims[p.GroupsClaim].([]interface{})
|
||||||
|
if rawGroups != nil && ok {
|
||||||
|
for _, rawGroup := range rawGroups {
|
||||||
|
formattedGroup, err := formatGroup(rawGroup)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Warning: unable to format group of type %s with error %s",
|
||||||
|
reflect.TypeOf(rawGroup), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groups = append(groups, formattedGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const emailClaim = "email"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNotImplemented is returned when a provider did not override a default
|
// ErrNotImplemented is returned when a provider did not override a default
|
||||||
// implementation method that doesn't have sensible defaults
|
// implementation method that doesn't have sensible defaults
|
||||||
|
Loading…
x
Reference in New Issue
Block a user