Merge pull request #877 from vmware-tanzu/upstream-ldap-refresh

Upstream ldap refresh
This commit is contained in:
Mo Khan 2021-11-05 18:08:45 -04:00 committed by GitHub
commit 5a3f83f90f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1503 additions and 171 deletions

View File

@ -7,7 +7,7 @@ package authenticators
import ( import (
"context" "context"
"k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user"
) )
// This interface is similar to the k8s token authenticator, but works with username/passwords instead // This interface is similar to the k8s token authenticator, but works with username/passwords instead
@ -31,5 +31,10 @@ import (
// See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator // See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator
// interface, as well as the Response type. // interface, as well as the Response type.
type UserAuthenticator interface { type UserAuthenticator interface {
AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) AuthenticateUser(ctx context.Context, username, password string) (*Response, bool, error)
}
type Response struct {
User user.Info
DN string
} }

View File

@ -328,14 +328,22 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
"providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt", "providerType": "闣ʬ橳(ý綃ʃʚƟ覣k眐4Ĉt",
"oidc": { "oidc": {
"upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬" "upstreamRefreshToken": "嵽痊w©Ź榨Q|ôɵt毇妬"
},
"ldap": {
"userDN": "6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾"
},
"activedirectory": {
"userDN": "|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞"
} }
} }
}, },
"requestedAudience": [ "requestedAudience": [
"6鉢緋uƴŤȱʀļÂ?墖\u003cƬb獭潜Ʃ饾" "ŚB碠k9"
], ],
"grantedAudience": [ "grantedAudience": [
"|鬌R蜚蠣麹概÷驣7Ʀ澉1æɽ誮rʨ鷞" "ʘ赱",
"ď逳鞪?3)藵睋邔\u0026Ű惫蜀Ģ¡圔",
"墀jMʥ"
] ]
}, },
"version": "2" "version": "2"

View File

@ -15,9 +15,9 @@ import (
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"k8s.io/apiserver/pkg/authentication/authenticator"
supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc" supervisoroidc "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/httputil/httperr"
"go.pinniped.dev/internal/httputil/securityheader" "go.pinniped.dev/internal/httputil/securityheader"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
@ -112,6 +112,7 @@ func handleAuthRequestForLDAPUpstream(
subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse) subject := downstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse)
username = authenticateResponse.User.GetName() username = authenticateResponse.User.GetName()
groups := authenticateResponse.User.GetGroups() groups := authenticateResponse.User.GetGroups()
dn := authenticateResponse.DN
customSessionData := &psession.CustomSessionData{ customSessionData := &psession.CustomSessionData{
ProviderUID: ldapUpstream.GetResourceUID(), ProviderUID: ldapUpstream.GetResourceUID(),
@ -119,6 +120,17 @@ func handleAuthRequestForLDAPUpstream(
ProviderType: idpType, ProviderType: idpType,
} }
if idpType == psession.ProviderTypeLDAP {
customSessionData.LDAP = &psession.LDAPSessionData{
UserDN: dn,
}
}
if idpType == psession.ProviderTypeActiveDirectory {
customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{
UserDN: dn,
}
}
return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w, return makeDownstreamSessionAndReturnAuthcodeRedirect(r, w,
oauthHelper, authorizeRequester, subject, username, groups, customSessionData) oauthHelper, authorizeRequester, subject, username, groups, customSessionData)
} }
@ -470,10 +482,7 @@ func addCSRFSetCookieHeader(w http.ResponseWriter, csrfValue csrftoken.CSRFToken
return nil return nil
} }
func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticator.Response) string { func downstreamSubjectFromUpstreamLDAP(ldapUpstream provider.UpstreamLDAPIdentityProviderI, authenticateResponse *authenticators.Response) string {
ldapURL := *ldapUpstream.GetURL() ldapURL := *ldapUpstream.GetURL()
q := ldapURL.Query() return downstreamsession.DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL)
q.Set(oidc.IDTokenSubjectClaim, authenticateResponse.User.GetUID())
ldapURL.RawQuery = q.Encode()
return ldapURL.String()
} }

View File

@ -19,12 +19,12 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/here" "go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/csrftoken"
@ -267,22 +267,24 @@ func TestAuthorizationEndpoint(t *testing.T) {
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username" happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
happyLDAPPassword := "some-ldap-password" //nolint:gosec happyLDAPPassword := "some-ldap-password" //nolint:gosec
happyLDAPUID := "some-ldap-uid" happyLDAPUID := "some-ldap-uid"
happyLDAPUserDN := "cn=foo,dn=bar"
happyLDAPGroups := []string{"group1", "group2", "group3"} happyLDAPGroups := []string{"group1", "group2", "group3"}
parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL) parsedUpstreamLDAPURL, err := url.Parse(upstreamLDAPURL)
require.NoError(t, err) require.NoError(t, err)
ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { ldapAuthenticateFunc := func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
if username == "" || password == "" { if username == "" || password == "" {
return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator") return nil, false, fmt.Errorf("should not have passed empty username or password to the authenticator")
} }
if username == happyLDAPUsername && password == happyLDAPPassword { if username == happyLDAPUsername && password == happyLDAPPassword {
return &authenticator.Response{ return &authenticators.Response{
User: &user.DefaultInfo{ User: &user.DefaultInfo{
Name: happyLDAPUsernameFromAuthenticator, Name: happyLDAPUsernameFromAuthenticator,
UID: happyLDAPUID, UID: happyLDAPUID,
Groups: happyLDAPGroups, Groups: happyLDAPGroups,
}, },
DN: happyLDAPUserDN,
}, true, nil }, true, nil
} }
return nil, false, nil return nil, false, nil
@ -305,7 +307,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName, Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID, ResourceUID: ldapUpstreamResourceUID,
AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
return nil, false, fmt.Errorf("some ldap upstream auth error") return nil, false, fmt.Errorf("some ldap upstream auth error")
}, },
} }
@ -438,6 +440,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
ProviderName: activeDirectoryUpstreamName, ProviderName: activeDirectoryUpstreamName,
ProviderType: psession.ProviderTypeActiveDirectory, ProviderType: psession.ProviderTypeActiveDirectory,
OIDC: nil, OIDC: nil,
LDAP: nil,
ActiveDirectory: &psession.ActiveDirectorySessionData{
UserDN: happyLDAPUserDN,
},
} }
expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{ expectedHappyLDAPUpstreamCustomSession := &psession.CustomSessionData{
@ -445,6 +451,10 @@ func TestAuthorizationEndpoint(t *testing.T) {
ProviderName: ldapUpstreamName, ProviderName: ldapUpstreamName,
ProviderType: psession.ProviderTypeLDAP, ProviderType: psession.ProviderTypeLDAP,
OIDC: nil, OIDC: nil,
LDAP: &psession.LDAPSessionData{
UserDN: happyLDAPUserDN,
},
ActiveDirectory: nil,
} }
expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{ expectedHappyOIDCPasswordGrantCustomSession := &psession.CustomSessionData{

View File

@ -169,6 +169,13 @@ func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenCl
return valueAsString, nil return valueAsString, nil
} }
func DownstreamLDAPSubject(uid string, ldapURL url.URL) string {
q := ldapURL.Query()
q.Set(oidc.IDTokenSubjectClaim, uid)
ldapURL.RawQuery = q.Encode()
return ldapURL.String()
}
func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string) string { func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string) string {
return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidc.IDTokenSubjectClaim, url.QueryEscape(upstreamSubject)) return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, oidc.IDTokenSubjectClaim, url.QueryEscape(upstreamSubject))
} }

View File

@ -88,6 +88,9 @@ type UpstreamLDAPIdentityProviderI interface {
// UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider. // UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider.
authenticators.UserAuthenticator authenticators.UserAuthenticator
// PerformRefresh performs a refresh against the upstream LDAP identity provider
PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error
} }
type DynamicUpstreamIDPProvider interface { type DynamicUpstreamIDPProvider interface {

View File

@ -75,6 +75,12 @@ func NewHandler(
func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error { func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester, providerCache oidc.UpstreamIdentityProvidersLister) error {
session := accessRequest.GetSession().(*psession.PinnipedSession) session := accessRequest.GetSession().(*psession.PinnipedSession)
downstreamUsername, err := getDownstreamUsernameFromPinnipedSession(session)
if err != nil {
return err
}
downstreamSubject := session.Fosite.Claims.Subject
customSessionData := session.Custom customSessionData := session.Custom
if customSessionData == nil { if customSessionData == nil {
return errorsx.WithStack(errMissingUpstreamSessionInternalError) return errorsx.WithStack(errMissingUpstreamSessionInternalError)
@ -89,14 +95,12 @@ func upstreamRefresh(ctx context.Context, accessRequest fosite.AccessRequester,
case psession.ProviderTypeOIDC: case psession.ProviderTypeOIDC:
return upstreamOIDCRefresh(ctx, customSessionData, providerCache) return upstreamOIDCRefresh(ctx, customSessionData, providerCache)
case psession.ProviderTypeLDAP: case psession.ProviderTypeLDAP:
// upstream refresh not yet implemented for LDAP, so do nothing return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
case psession.ProviderTypeActiveDirectory: case psession.ProviderTypeActiveDirectory:
// upstream refresh not yet implemented for AD, so do nothing return upstreamLDAPRefresh(ctx, customSessionData, providerCache, downstreamUsername, downstreamSubject)
default: default:
return errorsx.WithStack(errMissingUpstreamSessionInternalError) return errorsx.WithStack(errMissingUpstreamSessionInternalError)
} }
return nil
} }
func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error { func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister) error {
@ -114,9 +118,9 @@ func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, pro
refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken) refreshedTokens, err := p.PerformRefresh(ctx, s.OIDC.UpstreamRefreshToken)
if err != nil { if err != nil {
return errorsx.WithStack(errUpstreamRefreshError.WithHintf( return errorsx.WithStack(errUpstreamRefreshError.WithHint(
"Upstream refresh failed using provider %q of type %q.", "Upstream refresh failed.",
s.ProviderName, s.ProviderType).WithWrap(err)) ).WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
} }
// Upstream refresh may or may not return a new ID token. From the spec: // Upstream refresh may or may not return a new ID token. From the spec:
@ -129,8 +133,7 @@ func upstreamOIDCRefresh(ctx context.Context, s *psession.CustomSessionData, pro
_, err = p.ValidateToken(ctx, refreshedTokens, "") _, err = p.ValidateToken(ctx, refreshedTokens, "")
if err != nil { if err != nil {
return errorsx.WithStack(errUpstreamRefreshError.WithHintf( return errorsx.WithStack(errUpstreamRefreshError.WithHintf(
"Upstream refresh returned an invalid ID token using provider %q of type %q.", "Upstream refresh returned an invalid ID token.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
s.ProviderName, s.ProviderType).WithWrap(err))
} }
} else { } else {
plog.Debug("upstream refresh request did not return a new ID token", plog.Debug("upstream refresh request did not return a new ID token",
@ -163,5 +166,72 @@ func findOIDCProviderByNameAndValidateUID(
} }
} }
return nil, errorsx.WithStack(errUpstreamRefreshError. return nil, errorsx.WithStack(errUpstreamRefreshError.
WithHintf("Provider %q of type %q from upstream session data was not found.", s.ProviderName, s.ProviderType)) WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
}
func upstreamLDAPRefresh(ctx context.Context, s *psession.CustomSessionData, providerCache oidc.UpstreamIdentityProvidersLister, username string, subject string) error {
// if you have neither a valid ldap session config nor a valid active directory session config
validLDAP := s.ProviderType == psession.ProviderTypeLDAP && s.LDAP != nil && s.LDAP.UserDN != ""
validAD := s.ProviderType == psession.ProviderTypeActiveDirectory && s.ActiveDirectory != nil && s.ActiveDirectory.UserDN != ""
if !(validLDAP || validAD) {
return errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
// get ldap/ad provider out of cache
p, dn, err := findLDAPProviderByNameAndValidateUID(s, providerCache)
if err != nil {
return err
}
// run PerformRefresh
err = p.PerformRefresh(ctx, dn, username, subject)
if err != nil {
return errorsx.WithStack(errUpstreamRefreshError.WithHint(
"Upstream refresh failed.").WithWrap(err).WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
}
return nil
}
func findLDAPProviderByNameAndValidateUID(
s *psession.CustomSessionData,
providerCache oidc.UpstreamIdentityProvidersLister,
) (provider.UpstreamLDAPIdentityProviderI, string, error) {
var providers []provider.UpstreamLDAPIdentityProviderI
var dn string
if s.ProviderType == psession.ProviderTypeLDAP {
providers = providerCache.GetLDAPIdentityProviders()
dn = s.LDAP.UserDN
} else if s.ProviderType == psession.ProviderTypeActiveDirectory {
providers = providerCache.GetActiveDirectoryIdentityProviders()
dn = s.ActiveDirectory.UserDN
}
for _, p := range providers {
if p.GetName() == s.ProviderName {
if p.GetResourceUID() != s.ProviderUID {
return nil, "", errorsx.WithStack(errUpstreamRefreshError.WithHint(
"Provider from upstream session data has changed its resource UID since authentication.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
}
return p, dn, nil
}
}
return nil, "", errorsx.WithStack(errUpstreamRefreshError.
WithHint("Provider from upstream session data was not found.").WithDebugf("provider name: %q, provider type: %q", s.ProviderName, s.ProviderType))
}
func getDownstreamUsernameFromPinnipedSession(session *psession.PinnipedSession) (string, error) {
extra := session.Fosite.Claims.Extra
if extra == nil {
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
downstreamUsernameInterface := extra["username"]
if downstreamUsernameInterface == nil {
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
downstreamUsername, ok := downstreamUsernameInterface.(string)
if !ok || len(downstreamUsername) == 0 {
return "", errorsx.WithStack(errMissingUpstreamSessionInternalError)
}
return downstreamUsername, nil
} }

View File

@ -232,7 +232,7 @@ type tokenEndpointResponseExpectedValues struct {
wantErrorResponseBody string wantErrorResponseBody string
wantRequestedScopes []string wantRequestedScopes []string
wantGrantedScopes []string wantGrantedScopes []string
wantUpstreamOIDCRefreshCall *expectedUpstreamRefresh wantUpstreamRefreshCall *expectedUpstreamRefresh
wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens wantUpstreamOIDCValidateTokenCall *expectedUpstreamValidateTokens
wantCustomSessionDataStored *psession.CustomSessionData wantCustomSessionDataStored *psession.CustomSessionData
} }
@ -879,8 +879,20 @@ func TestRefreshGrant(t *testing.T) {
oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token" oidcUpstreamInitialRefreshToken = "initial-upstream-refresh-token"
oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token" oidcUpstreamRefreshedIDToken = "fake-refreshed-id-token"
oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token" oidcUpstreamRefreshedRefreshToken = "fake-refreshed-refresh-token"
ldapUpstreamName = "some-ldap-idp"
ldapUpstreamResourceUID = "ldap-resource-uid"
ldapUpstreamType = "ldap"
ldapUpstreamDN = "some-ldap-user-dn"
activeDirectoryUpstreamName = "some-ad-idp"
activeDirectoryUpstreamResourceUID = "ad-resource-uid"
activeDirectoryUpstreamType = "activedirectory"
activeDirectoryUpstreamDN = "some-ad-user-dn"
) )
ldapUpstreamURL, _ := url.Parse("some-url")
// The below values are funcs so every test can have its own copy of the objects, to avoid data races // The below values are funcs so every test can have its own copy of the objects, to avoid data races
// in these parallel tests. // in these parallel tests.
@ -907,7 +919,7 @@ func TestRefreshGrant(t *testing.T) {
return sessionData return sessionData
} }
happyUpstreamRefreshCall := func() *expectedUpstreamRefresh { happyOIDCUpstreamRefreshCall := func() *expectedUpstreamRefresh {
return &expectedUpstreamRefresh{ return &expectedUpstreamRefresh{
performedByUpstreamName: oidcUpstreamName, performedByUpstreamName: oidcUpstreamName,
args: &oidctestutil.PerformRefreshArgs{ args: &oidctestutil.PerformRefreshArgs{
@ -917,6 +929,30 @@ func TestRefreshGrant(t *testing.T) {
} }
} }
happyLDAPUpstreamRefreshCall := func() *expectedUpstreamRefresh {
return &expectedUpstreamRefresh{
performedByUpstreamName: ldapUpstreamName,
args: &oidctestutil.PerformRefreshArgs{
Ctx: nil,
DN: ldapUpstreamDN,
ExpectedSubject: goodSubject,
ExpectedUsername: goodUsername,
},
}
}
happyActiveDirectoryUpstreamRefreshCall := func() *expectedUpstreamRefresh {
return &expectedUpstreamRefresh{
performedByUpstreamName: activeDirectoryUpstreamName,
args: &oidctestutil.PerformRefreshArgs{
Ctx: nil,
DN: activeDirectoryUpstreamDN,
ExpectedSubject: goodSubject,
ExpectedUsername: goodUsername,
},
}
}
happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token) *expectedUpstreamValidateTokens { happyUpstreamValidateTokenCall := func(expectedTokens *oauth2.Token) *expectedUpstreamValidateTokens {
return &expectedUpstreamValidateTokens{ return &expectedUpstreamValidateTokens{
performedByUpstreamName: oidcUpstreamName, performedByUpstreamName: oidcUpstreamName,
@ -944,7 +980,7 @@ func TestRefreshGrant(t *testing.T) {
// same as the same values as the authcode exchange case. // same as the same values as the authcode exchange case.
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored) want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
// Should always try to perform an upstream refresh. // Should always try to perform an upstream refresh.
want.wantUpstreamOIDCRefreshCall = happyUpstreamRefreshCall() want.wantUpstreamRefreshCall = happyOIDCUpstreamRefreshCall()
// Should only try to ValidateToken when there was an id token returned by the upstream refresh. // Should only try to ValidateToken when there was an id token returned by the upstream refresh.
if expectToValidateToken != nil { if expectToValidateToken != nil {
want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken) want.wantUpstreamOIDCValidateTokenCall = happyUpstreamValidateTokenCall(expectToValidateToken)
@ -952,6 +988,18 @@ func TestRefreshGrant(t *testing.T) {
return want return want
} }
happyRefreshTokenResponseForLDAP := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues {
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
want.wantUpstreamRefreshCall = happyLDAPUpstreamRefreshCall()
return want
}
happyRefreshTokenResponseForActiveDirectory := func(wantCustomSessionDataStored *psession.CustomSessionData) tokenEndpointResponseExpectedValues {
want := happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(wantCustomSessionDataStored)
want.wantUpstreamRefreshCall = happyActiveDirectoryUpstreamRefreshCall()
return want
}
refreshedUpstreamTokensWithRefreshTokenWithoutIDToken := func() *oauth2.Token { refreshedUpstreamTokensWithRefreshTokenWithoutIDToken := func() *oauth2.Token {
return &oauth2.Token{ return &oauth2.Token{
AccessToken: "fake-refreshed-access-token", AccessToken: "fake-refreshed-access-token",
@ -972,11 +1020,28 @@ func TestRefreshGrant(t *testing.T) {
return tokens return tokens
} }
happyActiveDirectoryCustomSessionData := &psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName,
ProviderType: activeDirectoryUpstreamType,
ActiveDirectory: &psession.ActiveDirectorySessionData{
UserDN: activeDirectoryUpstreamDN,
},
}
happyLDAPCustomSessionData := &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: ldapUpstreamType,
LDAP: &psession.LDAPSessionData{
UserDN: ldapUpstreamDN,
},
}
tests := []struct { tests := []struct {
name string name string
idps *oidctestutil.UpstreamIDPListerBuilder idps *oidctestutil.UpstreamIDPListerBuilder
authcodeExchange authcodeExchangeInputs authcodeExchange authcodeExchangeInputs
refreshRequest refreshRequestInputs refreshRequest refreshRequestInputs
modifyRefreshTokenStorage func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string)
}{ }{
{ {
name: "happy path refresh grant with openid scope granted (id token returned)", name: "happy path refresh grant with openid scope granted (id token returned)",
@ -1015,7 +1080,7 @@ func TestRefreshGrant(t *testing.T) {
wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"offline_access"}, wantRequestedScopes: []string{"offline_access"},
wantGrantedScopes: []string{"offline_access"}, wantGrantedScopes: []string{"offline_access"},
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
}, },
@ -1096,7 +1161,7 @@ func TestRefreshGrant(t *testing.T) {
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}, wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, wantRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, wantGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken), wantCustomSessionDataStored: upstreamOIDCCustomSessionDataWithNewRefreshToken(oidcUpstreamRefreshedRefreshToken),
}, },
@ -1400,7 +1465,7 @@ func TestRefreshGrant(t *testing.T) {
wantErrorResponseBody: here.Doc(` wantErrorResponseBody: here.Doc(`
{ {
"error": "error", "error": "error",
"error_description": "Error during upstream refresh. Provider 'this-name-will-not-be-found' of type 'oidc' from upstream session data was not found." "error_description": "Error during upstream refresh. Provider from upstream session data was not found."
} }
`), `),
}, },
@ -1449,12 +1514,12 @@ func TestRefreshGrant(t *testing.T) {
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(` wantErrorResponseBody: here.Doc(`
{ {
"error": "error", "error": "error",
"error_description": "Error during upstream refresh. Upstream refresh failed using provider 'some-oidc-idp' of type 'oidc'." "error_description": "Error during upstream refresh. Upstream refresh failed."
} }
`), `),
}, },
@ -1474,13 +1539,520 @@ func TestRefreshGrant(t *testing.T) {
}, },
refreshRequest: refreshRequestInputs{ refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{ want: tokenEndpointResponseExpectedValues{
wantUpstreamOIDCRefreshCall: happyUpstreamRefreshCall(), wantUpstreamRefreshCall: happyOIDCUpstreamRefreshCall(),
wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()), wantUpstreamOIDCValidateTokenCall: happyUpstreamValidateTokenCall(refreshedUpstreamTokensWithIDAndRefreshTokens()),
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(` wantErrorResponseBody: here.Doc(`
{ {
"error": "error", "error": "error",
"error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token using provider 'some-oidc-idp' of type 'oidc'." "error_description": "Error during upstream refresh. Upstream refresh returned an invalid ID token."
}
`),
},
},
},
{
name: "upstream ldap refresh happy path",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: happyRefreshTokenResponseForLDAP(
happyLDAPCustomSessionData,
),
},
},
{
name: "upstream active directory refresh happy path",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: activeDirectoryUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: happyRefreshTokenResponseForActiveDirectory(
happyActiveDirectoryCustomSessionData,
),
},
},
{
name: "upstream ldap refresh when the LDAP session data is nil",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: ldapUpstreamType,
LDAP: nil,
},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: ldapUpstreamType,
LDAP: nil,
},
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "upstream active directory refresh when the ad session data is nil",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: activeDirectoryUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: &psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName,
ProviderType: activeDirectoryUpstreamType,
ActiveDirectory: nil,
},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{
ProviderUID: activeDirectoryUpstreamResourceUID,
ProviderName: activeDirectoryUpstreamName,
ProviderType: activeDirectoryUpstreamType,
ActiveDirectory: nil,
},
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "upstream ldap refresh when the LDAP session data does not contain dn",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: ldapUpstreamType,
LDAP: &psession.LDAPSessionData{
UserDN: "",
},
},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: ldapUpstreamType,
LDAP: &psession.LDAPSessionData{
UserDN: "",
},
},
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "upstream active directory refresh when the active directory session data does not contain dn",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: activeDirectoryUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: &psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: ldapUpstreamType,
ActiveDirectory: &psession.ActiveDirectorySessionData{
UserDN: "",
},
},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
&psession.CustomSessionData{
ProviderUID: ldapUpstreamResourceUID,
ProviderName: ldapUpstreamName,
ProviderType: ldapUpstreamType,
ActiveDirectory: &psession.ActiveDirectorySessionData{
UserDN: "",
},
},
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "upstream ldap refresh returns an error",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
PerformRefreshErr: errors.New("Some error performing upstream refresh"),
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantUpstreamRefreshCall: happyLDAPUpstreamRefreshCall(),
wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Upstream refresh failed."
}
`),
},
},
},
{
name: "upstream active directory refresh returns an error",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: activeDirectoryUpstreamResourceUID,
URL: ldapUpstreamURL,
PerformRefreshErr: errors.New("Some error performing upstream refresh"),
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantUpstreamRefreshCall: happyActiveDirectoryUpstreamRefreshCall(),
wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Upstream refresh failed."
}
`),
},
},
},
{
name: "upstream ldap idp not found",
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
}
`),
},
},
},
{
name: "upstream active directory idp not found",
idps: oidctestutil.NewUpstreamIDPListerBuilder(),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Provider from upstream session data was not found."
}
`),
},
},
},
{
name: "fosite session is empty",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
require.NoError(t, err)
session := firstRequester.GetSession().(*psession.PinnipedSession)
session.Fosite = &openid.DefaultSession{}
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
require.NoError(t, err)
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
require.NoError(t, err)
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "username not found in extra field",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
//fositeSessionData: &openid.DefaultSession{},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
require.NoError(t, err)
session := firstRequester.GetSession().(*psession.PinnipedSession)
session.Fosite = &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{
Extra: map[string]interface{}{},
},
}
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
require.NoError(t, err)
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
require.NoError(t, err)
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "username in extra is not a string",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
//fositeSessionData: &openid.DefaultSession{},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
require.NoError(t, err)
session := firstRequester.GetSession().(*psession.PinnipedSession)
session.Fosite = &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{
Extra: map[string]interface{}{"username": 123},
},
}
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
require.NoError(t, err)
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
require.NoError(t, err)
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "username in extra is an empty string",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: ldapUpstreamResourceUID,
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
//fositeSessionData: &openid.DefaultSession{},
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
modifyRefreshTokenStorage: func(t *testing.T, oauthStore *oidc.KubeStorage, refreshToken string) {
refreshTokenSignature := getFositeDataSignature(t, refreshToken)
firstRequester, err := oauthStore.GetRefreshTokenSession(context.Background(), refreshTokenSignature, nil)
require.NoError(t, err)
session := firstRequester.GetSession().(*psession.PinnipedSession)
session.Fosite = &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{
Extra: map[string]interface{}{"username": ""},
},
}
err = oauthStore.DeleteRefreshTokenSession(context.Background(), refreshTokenSignature)
require.NoError(t, err)
err = oauthStore.CreateRefreshTokenSession(context.Background(), refreshTokenSignature, firstRequester)
require.NoError(t, err)
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusInternalServerError,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "There was an internal server error. Required upstream data not found in session."
}
`),
},
},
},
{
name: "when the ldap provider in the session storage is found but has the wrong resource UID during the refresh request",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: ldapUpstreamName,
ResourceUID: "the-wrong-uid",
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyLDAPCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyLDAPCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
}
`),
},
},
},
{
name: "when the active directory provider in the session storage is found but has the wrong resource UID during the refresh request",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{
Name: activeDirectoryUpstreamName,
ResourceUID: "the-wrong-uid",
URL: ldapUpstreamURL,
}),
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access") },
customSessionData: happyActiveDirectoryCustomSessionData,
want: happyAuthcodeExchangeTokenResponseForOpenIDAndOfflineAccess(
happyActiveDirectoryCustomSessionData,
),
},
refreshRequest: refreshRequestInputs{
want: tokenEndpointResponseExpectedValues{
wantStatus: http.StatusUnauthorized,
wantErrorResponseBody: here.Doc(`
{
"error": "error",
"error_description": "Error during upstream refresh. Provider from upstream session data has changed its resource UID since authentication."
} }
`), `),
}, },
@ -1493,6 +2065,8 @@ func TestRefreshGrant(t *testing.T) {
t.Parallel() t.Parallel()
// First exchange the authcode for tokens, including a refresh token. // First exchange the authcode for tokens, including a refresh token.
// its actually fine to use this function even when simulating ldap (which uses a different flow) because it's
// just populating a secret in storage.
subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange, test.idps.Build()) subject, rsp, authCode, jwtSigningKey, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange, test.idps.Build())
var parsedAuthcodeExchangeResponseBody map[string]interface{} var parsedAuthcodeExchangeResponseBody map[string]interface{}
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody)) require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody))
@ -1511,6 +2085,10 @@ func TestRefreshGrant(t *testing.T) {
// Send the refresh token back and preform a refresh. // Send the refresh token back and preform a refresh.
firstRefreshToken := parsedAuthcodeExchangeResponseBody["refresh_token"].(string) firstRefreshToken := parsedAuthcodeExchangeResponseBody["refresh_token"].(string)
require.NotEmpty(t, firstRefreshToken) require.NotEmpty(t, firstRefreshToken)
if test.modifyRefreshTokenStorage != nil {
test.modifyRefreshTokenStorage(t, oauthStore, firstRefreshToken)
}
reqContext := context.WithValue(context.Background(), struct{ name string }{name: "test"}, "request-context") reqContext := context.WithValue(context.Background(), struct{ name string }{name: "test"}, "request-context")
req := httptest.NewRequest("POST", "/path/shouldn't/matter", req := httptest.NewRequest("POST", "/path/shouldn't/matter",
happyRefreshRequestBody(firstRefreshToken).ReadCloser()).WithContext(reqContext) happyRefreshRequestBody(firstRefreshToken).ReadCloser()).WithContext(reqContext)
@ -1525,11 +2103,11 @@ func TestRefreshGrant(t *testing.T) {
t.Logf("second response body: %q", refreshResponse.Body.String()) t.Logf("second response body: %q", refreshResponse.Body.String())
// Test that we did or did not make a call to the upstream OIDC provider interface to perform a token refresh. // Test that we did or did not make a call to the upstream OIDC provider interface to perform a token refresh.
if test.refreshRequest.want.wantUpstreamOIDCRefreshCall != nil { if test.refreshRequest.want.wantUpstreamRefreshCall != nil {
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args.Ctx = reqContext test.refreshRequest.want.wantUpstreamRefreshCall.args.Ctx = reqContext
test.idps.RequireExactlyOneCallToPerformRefresh(t, test.idps.RequireExactlyOneCallToPerformRefresh(t,
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.performedByUpstreamName, test.refreshRequest.want.wantUpstreamRefreshCall.performedByUpstreamName,
test.refreshRequest.want.wantUpstreamOIDCRefreshCall.args, test.refreshRequest.want.wantUpstreamRefreshCall.args,
) )
} else { } else {
test.idps.RequireExactlyZeroCallsToPerformRefresh(t) test.idps.RequireExactlyZeroCallsToPerformRefresh(t)

View File

@ -45,6 +45,10 @@ type CustomSessionData struct {
// Only used when ProviderType == "oidc". // Only used when ProviderType == "oidc".
OIDC *OIDCSessionData `json:"oidc,omitempty"` OIDC *OIDCSessionData `json:"oidc,omitempty"`
LDAP *LDAPSessionData `json:"ldap,omitempty"`
ActiveDirectory *ActiveDirectorySessionData `json:"activedirectory,omitempty"`
} }
type ProviderType string type ProviderType string
@ -60,6 +64,16 @@ type OIDCSessionData struct {
UpstreamRefreshToken string `json:"upstreamRefreshToken"` UpstreamRefreshToken string `json:"upstreamRefreshToken"`
} }
// LDAPSessionData is the additional data needed by Pinniped when the upstream IDP is an LDAP provider.
type LDAPSessionData struct {
UserDN string `json:"userDN"`
}
// ActiveDirectorySessionData is the additional data needed by Pinniped when the upstream IDP is an Active Directory provider.
type ActiveDirectorySessionData struct {
UserDN string `json:"userDN"`
}
// NewPinnipedSession returns a new empty session. // NewPinnipedSession returns a new empty session.
func NewPinnipedSession() *PinnipedSession { func NewPinnipedSession() *PinnipedSession {
return &PinnipedSession{ return &PinnipedSession{

View File

@ -21,10 +21,10 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage/authorizationcode" "go.pinniped.dev/internal/fositestorage/authorizationcode"
"go.pinniped.dev/internal/fositestorage/openidconnect" "go.pinniped.dev/internal/fositestorage/openidconnect"
@ -61,8 +61,11 @@ type PasswordCredentialsGrantAndValidateTokensArgs struct {
// PerformRefreshArgs is used to spy on calls to // PerformRefreshArgs is used to spy on calls to
// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc(). // TestUpstreamOIDCIdentityProvider.PerformRefreshFunc().
type PerformRefreshArgs struct { type PerformRefreshArgs struct {
Ctx context.Context Ctx context.Context
RefreshToken string RefreshToken string
DN string
ExpectedUsername string
ExpectedSubject string
} }
// ValidateTokenArgs is used to spy on calls to // ValidateTokenArgs is used to spy on calls to
@ -74,10 +77,13 @@ type ValidateTokenArgs struct {
} }
type TestUpstreamLDAPIdentityProvider struct { type TestUpstreamLDAPIdentityProvider struct {
Name string Name string
ResourceUID types.UID ResourceUID types.UID
URL *url.URL URL *url.URL
AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) AuthenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error)
performRefreshCallCount int
performRefreshArgs []*PerformRefreshArgs
PerformRefreshErr error
} }
var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} var _ provider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{}
@ -90,7 +96,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetName() string {
return u.Name return u.Name
} }
func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
return u.AuthenticateFunc(ctx, username, password) return u.AuthenticateFunc(ctx, username, password)
} }
@ -98,6 +104,34 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL {
return u.URL return u.URL
} }
func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
if u.performRefreshArgs == nil {
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
}
u.performRefreshCallCount++
u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{
Ctx: ctx,
DN: userDN,
ExpectedUsername: expectedUsername,
ExpectedSubject: expectedSubject,
})
if u.PerformRefreshErr != nil {
return u.PerformRefreshErr
}
return nil
}
func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshCallCount() int {
return u.performRefreshCallCount
}
func (u *TestUpstreamLDAPIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs {
if u.performRefreshArgs == nil {
u.performRefreshArgs = make([]*PerformRefreshArgs, 0)
}
return u.performRefreshArgs[call]
}
type TestUpstreamOIDCIdentityProvider struct { type TestUpstreamOIDCIdentityProvider struct {
Name string Name string
ClientID string ClientID string
@ -390,31 +424,54 @@ func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh(
t.Helper() t.Helper()
var actualArgs *PerformRefreshArgs var actualArgs *PerformRefreshArgs
var actualNameOfUpstreamWhichMadeCall string var actualNameOfUpstreamWhichMadeCall string
actualCallCountAcrossAllOIDCUpstreams := 0 actualCallCountAcrossAllUpstreams := 0
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount
actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
if callCountOnThisUpstream == 1 { if callCountOnThisUpstream == 1 {
actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name
actualArgs = upstreamOIDC.performRefreshArgs[0] actualArgs = upstreamOIDC.performRefreshArgs[0]
} }
} }
require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders {
"should have been exactly one call to PerformRefresh() by all OIDC upstreams", callCountOnThisUpstream := upstreamLDAP.performRefreshCallCount
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
if callCountOnThisUpstream == 1 {
actualNameOfUpstreamWhichMadeCall = upstreamLDAP.Name
actualArgs = upstreamLDAP.performRefreshArgs[0]
}
}
for _, upstreamAD := range b.upstreamActiveDirectoryIdentityProviders {
callCountOnThisUpstream := upstreamAD.performRefreshCallCount
actualCallCountAcrossAllUpstreams += callCountOnThisUpstream
if callCountOnThisUpstream == 1 {
actualNameOfUpstreamWhichMadeCall = upstreamAD.Name
actualArgs = upstreamAD.performRefreshArgs[0]
}
}
require.Equal(t, 1, actualCallCountAcrossAllUpstreams,
"should have been exactly one call to PerformRefresh() by all upstreams",
) )
require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall,
"PerformRefresh() was called on the wrong OIDC upstream", "PerformRefresh() was called on the wrong upstream",
) )
require.Equal(t, expectedArgs, actualArgs) require.Equal(t, expectedArgs, actualArgs)
} }
func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) { func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) {
t.Helper() t.Helper()
actualCallCountAcrossAllOIDCUpstreams := 0 actualCallCountAcrossAllUpstreams := 0
for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders {
actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.performRefreshCallCount actualCallCountAcrossAllUpstreams += upstreamOIDC.performRefreshCallCount
} }
require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders {
actualCallCountAcrossAllUpstreams += upstreamLDAP.performRefreshCallCount
}
for _, upstreamActiveDirectory := range b.upstreamActiveDirectoryIdentityProviders {
actualCallCountAcrossAllUpstreams += upstreamActiveDirectory.performRefreshCallCount
}
require.Equal(t, 0, actualCallCountAcrossAllUpstreams,
"expected exactly zero calls to PerformRefresh()", "expected exactly zero calls to PerformRefresh()",
) )
} }

View File

@ -21,12 +21,12 @@ import (
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/google/uuid" "github.com/google/uuid"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/utils/trace" "k8s.io/utils/trace"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/endpointaddr"
"go.pinniped.dev/internal/oidc/downstreamsession"
"go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/plog"
) )
@ -169,6 +169,73 @@ func (p *Provider) GetConfig() ProviderConfig {
return p.c return p.c
} }
func (p *Provider) PerformRefresh(ctx context.Context, userDN, expectedUsername, expectedSubject string) error {
t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()})
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
searchResult, err := p.performRefresh(ctx, userDN)
if err != nil {
p.traceRefreshFailure(t, err)
return err
}
// if any more or less than one entry, error.
// we don't need to worry about logging this because we know it's a dn.
if len(searchResult.Entries) != 1 {
return fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`,
userDN, len(searchResult.Entries),
)
}
userEntry := searchResult.Entries[0]
if len(userEntry.DN) == 0 {
return fmt.Errorf(`searching for user with original DN "%s" resulted in search result without DN`, userDN)
}
newUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, userDN)
if err != nil {
return err
}
if newUsername != expectedUsername {
return fmt.Errorf(`searching for user "%s" returned a different username than the previous value. expected: "%s", actual: "%s"`,
userDN, expectedUsername, newUsername,
)
}
newUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, userDN)
if err != nil {
return err
}
newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL())
if newSubject != expectedSubject {
return fmt.Errorf(`searching for user "%s" produced a different subject than the previous value. expected: "%s", actual: "%s"`, userDN, expectedSubject, newSubject)
}
// we checked that the user still exists and their information is the same, so just return.
return nil
}
func (p *Provider) performRefresh(ctx context.Context, userDN string) (*ldap.SearchResult, error) {
search := p.refreshUserSearchRequest(userDN)
conn, err := p.dial(ctx)
if err != nil {
return nil, fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err)
}
defer conn.Close()
err = conn.Bind(p.c.BindUsername, p.c.BindPassword)
if err != nil {
return nil, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
}
searchResult, err := conn.Search(search)
if err != nil {
return nil, fmt.Errorf(`error searching for user "%s": %w`, userDN, err)
}
return searchResult, nil
}
func (p *Provider) dial(ctx context.Context) (Conn, error) { func (p *Provider) dial(ctx context.Context) (Conn, error) {
tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort) tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort)
if err != nil { if err != nil {
@ -310,7 +377,7 @@ func (p *Provider) TestConnection(ctx context.Context) error {
// authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does // authentication for a given end user's username. It runs the same logic as AuthenticateUser except it does
// not bind as that user, so it does not test their password. It returns the same values that a real call to // not bind as that user, so it does not test their password. It returns the same values that a real call to
// AuthenticateUser with the correct password would return. // AuthenticateUser with the correct password would return.
func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticator.Response, bool, error) { func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticators.Response, bool, error) {
endUserBindFunc := func(conn Conn, foundUserDN string) error { endUserBindFunc := func(conn Conn, foundUserDN string) error {
// Act as if the end user bind always succeeds. // Act as if the end user bind always succeeds.
return nil return nil
@ -319,14 +386,14 @@ func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string)
} }
// Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator. // Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator.
func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticators.Response, bool, error) {
endUserBindFunc := func(conn Conn, foundUserDN string) error { endUserBindFunc := func(conn Conn, foundUserDN string) error {
return conn.Bind(foundUserDN, password) return conn.Bind(foundUserDN, password)
} }
return p.authenticateUserImpl(ctx, username, endUserBindFunc) return p.authenticateUserImpl(ctx, username, endUserBindFunc)
} }
func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticator.Response, bool, error) { func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, bool, error) {
t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()}) t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()})
defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches
@ -355,24 +422,16 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi
return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err)
} }
mappedUsername, mappedUID, mappedGroupNames, err := p.searchAndBindUser(conn, username, bindFunc) response, err := p.searchAndBindUser(conn, username, bindFunc)
if err != nil { if err != nil {
p.traceAuthFailure(t, err) p.traceAuthFailure(t, err)
return nil, false, err return nil, false, err
} }
if len(mappedUsername) == 0 || len(mappedUID) == 0 { if response == nil {
// Couldn't find the username or couldn't bind using the password.
p.traceAuthFailure(t, fmt.Errorf("bad username or password")) p.traceAuthFailure(t, fmt.Errorf("bad username or password"))
return nil, false, nil return nil, false, nil
} }
response := &authenticator.Response{
User: &user.DefaultInfo{
Name: mappedUsername,
UID: mappedUID,
Groups: mappedGroupNames,
},
}
p.traceAuthSuccess(t) p.traceAuthSuccess(t)
return response, true, nil return response, true, nil
} }
@ -454,7 +513,7 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e
return searchBase, nil return searchBase, nil
} }
func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) { func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticators.Response, error) {
searchResult, err := conn.Search(p.userSearchRequest(username)) searchResult, err := conn.Search(p.userSearchRequest(username))
if err != nil { if err != nil {
plog.All(`error searching for user`, plog.All(`error searching for user`,
@ -462,7 +521,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
"username", username, "username", username,
"err", err, "err", err,
) )
return "", "", nil, fmt.Errorf(`error searching for user: %w`, err) return nil, fmt.Errorf(`error searching for user: %w`, err)
} }
if len(searchResult.Entries) == 0 { if len(searchResult.Entries) == 0 {
if plog.Enabled(plog.LevelAll) { if plog.Enabled(plog.LevelAll) {
@ -473,38 +532,38 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
} else { } else {
plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName()) plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName())
} }
return "", "", nil, nil return nil, nil
} }
// At this point, we have matched at least one entry, so we can be confident that the username is not actually // At this point, we have matched at least one entry, so we can be confident that the username is not actually
// someone's password mistakenly entered into the username field, so we can log it without concern. // someone's password mistakenly entered into the username field, so we can log it without concern.
if len(searchResult.Entries) > 1 { if len(searchResult.Entries) > 1 {
return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`, return nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`,
username, len(searchResult.Entries), username, len(searchResult.Entries),
) )
} }
userEntry := searchResult.Entries[0] userEntry := searchResult.Entries[0]
if len(userEntry.DN) == 0 { if len(userEntry.DN) == 0 {
return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) return nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username)
} }
mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username) mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username)
if err != nil { if err != nil {
return "", "", nil, err return nil, err
} }
// We would like to support binary typed attributes for UIDs, so always read them as binary and encode them, // We would like to support binary typed attributes for UIDs, so always read them as binary and encode them,
// even when the attribute may not be binary. // even when the attribute may not be binary.
mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, username) mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, username)
if err != nil { if err != nil {
return "", "", nil, err return nil, err
} }
mappedGroupNames := []string{} mappedGroupNames := []string{}
if len(p.c.GroupSearch.Base) > 0 { if len(p.c.GroupSearch.Base) > 0 {
mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN) mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN)
if err != nil { if err != nil {
return "", "", nil, err return nil, err
} }
} }
sort.Strings(mappedGroupNames) sort.Strings(mappedGroupNames)
@ -516,12 +575,26 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN) err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN)
ldapErr := &ldap.Error{} ldapErr := &ldap.Error{}
if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials { if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials {
return "", "", nil, nil return nil, nil
} }
return "", "", nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) return nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err)
} }
return mappedUsername, mappedUID, mappedGroupNames, nil if len(mappedUsername) == 0 || len(mappedUID) == 0 {
// Couldn't find the username or couldn't bind using the password.
return nil, nil
}
response := &authenticators.Response{
User: &user.DefaultInfo{
Name: mappedUsername,
UID: mappedUID,
Groups: mappedGroupNames,
},
DN: userEntry.DN,
}
return response, nil
} }
func (p *Provider) defaultNamingContextRequest() *ldap.SearchRequest { func (p *Provider) defaultNamingContextRequest() *ldap.SearchRequest {
@ -568,6 +641,21 @@ func (p *Provider) groupSearchRequest(userDN string) *ldap.SearchRequest {
} }
} }
func (p *Provider) refreshUserSearchRequest(dn string) *ldap.SearchRequest {
// See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options.
return &ldap.SearchRequest{
BaseDN: dn,
Scope: ldap.ScopeBaseObject,
DerefAliases: ldap.NeverDerefAliases,
SizeLimit: 2,
TimeLimit: 90,
TypesOnly: false,
Filter: "(objectClass=*)", // we already have the dn, so the filter doesn't matter
Attributes: p.userSearchRequestedAttributes(),
Controls: nil, // this could be used to enable paging, but we're already limiting the result max size
}
}
func (p *Provider) userSearchRequestedAttributes() []string { func (p *Provider) userSearchRequestedAttributes() []string {
attributes := []string{} attributes := []string{}
if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName { if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName {
@ -687,6 +775,12 @@ func (p *Provider) traceSearchBaseDiscoveryFailure(t *trace.Trace, err error) {
trace.Field{Key: "reason", Value: err.Error()}) trace.Field{Key: "reason", Value: err.Error()})
} }
func (p *Provider) traceRefreshFailure(t *trace.Trace, err error) {
t.Step("refresh failed",
trace.Field{Key: "reason", Value: err.Error()},
)
}
func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) { func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) {
// validation has already been done so we can just get the attribute... // validation has already been done so we can just get the attribute...
return func(entry *ldap.Entry) (string, error) { return func(entry *ldap.Entry) (string, error) {

View File

@ -18,9 +18,9 @@ import (
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/endpointaddr"
"go.pinniped.dev/internal/mocks/mockldapconn" "go.pinniped.dev/internal/mocks/mockldapconn"
@ -151,7 +151,7 @@ func TestEndUserAuthentication(t *testing.T) {
} }
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult. // The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticator.Response { expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticators.Response {
u := &user.DefaultInfo{ u := &user.DefaultInfo{
Name: testUserSearchResultUsernameAttributeValue, Name: testUserSearchResultUsernameAttributeValue,
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)), UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
@ -160,7 +160,7 @@ func TestEndUserAuthentication(t *testing.T) {
if editFunc != nil { if editFunc != nil {
editFunc(u) editFunc(u)
} }
return &authenticator.Response{User: u} return &authenticators.Response{User: u, DN: testUserSearchResultDNValue}
} }
tests := []struct { tests := []struct {
@ -173,7 +173,7 @@ func TestEndUserAuthentication(t *testing.T) {
dialError error dialError error
wantError string wantError string
wantToSkipDial bool wantToSkipDial bool
wantAuthResponse *authenticator.Response wantAuthResponse *authenticators.Response
wantUnauthenticated bool wantUnauthenticated bool
skipDryRunAuthenticateUser bool // tests about when the end user bind fails don't make sense for DryRunAuthenticateUser() skipDryRunAuthenticateUser bool // tests about when the end user bind fails don't make sense for DryRunAuthenticateUser()
}{ }{
@ -498,12 +498,13 @@ func TestEndUserAuthentication(t *testing.T) {
bindEndUserMocks: func(conn *mockldapconn.MockConn) { bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
}, },
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{ User: &user.DefaultInfo{
Name: testUserSearchResultUsernameAttributeValue, Name: testUserSearchResultUsernameAttributeValue,
UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)), UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
Groups: []string{"a", "b", "c"}, Groups: []string{"a", "b", "c"},
}, },
DN: testUserSearchResultDNValue,
}, },
}, },
{ {
@ -1212,6 +1213,340 @@ func TestEndUserAuthentication(t *testing.T) {
} }
} }
func TestUpstreamRefresh(t *testing.T) {
expectedUserSearch := &ldap.SearchRequest{
BaseDN: testUserSearchResultDNValue,
Scope: ldap.ScopeBaseObject,
DerefAliases: ldap.NeverDerefAliases,
SizeLimit: 2,
TimeLimit: 90,
TypesOnly: false,
Filter: "(objectClass=*)",
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute},
Controls: nil, // don't need paging because we set the SizeLimit so small
}
happyPathUserSearchResult := &ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
},
},
},
}
providerConfig := &ProviderConfig{
Name: "some-provider-name",
Host: testHost,
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test
ConnectionProtocol: TLS,
BindUsername: testBindUsername,
BindPassword: testBindPassword,
UserSearch: UserSearchConfig{
Base: testUserSearchBase,
UIDAttribute: testUserSearchUIDAttribute,
UsernameAttribute: testUserSearchUsernameAttribute,
},
}
tests := []struct {
name string
providerConfig *ProviderConfig
setupMocks func(conn *mockldapconn.MockConn)
dialError error
wantErr string
}{
{
name: "happy path where searching the dn returns a single entry",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
},
{
name: "error where dial fails",
providerConfig: providerConfig,
dialError: errors.New("some dial error"),
wantErr: "error dialing host \"ldap.example.com:8443\": some dial error",
},
{
name: "error binding",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "error binding as \"cn=some-bind-username,dc=pinniped,dc=dev\" before user search: some bind error",
},
{
name: "search result returns no entries",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "searching for user \"some-upstream-user-dn\" resulted in 0 search results, but expected 1 result",
},
{
name: "error searching",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(nil, errors.New("some search error"))
conn.EXPECT().Close().Times(1)
},
wantErr: "error searching for user \"some-upstream-user-dn\": some search error",
},
{
name: "search result returns more than one entry",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{},
},
{
DN: "doesn't-matter",
Attributes: []*ldap.EntryAttribute{},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "searching for user \"some-upstream-user-dn\" resulted in 2 search results, but expected 1 result",
},
{
name: "search result has wrong uid",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte("wrong-uid")},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=d3JvbmctdWlk\"",
},
{
name: "search result has wrong username",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{"wrong-username"},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "searching for user \"some-upstream-user-dn\" returned a different username than the previous value. expected: \"some-upstream-username-value\", actual: \"wrong-username\"",
},
{
name: "search result has no dn",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "searching for user with original DN \"some-upstream-user-dn\" resulted in search result without DN",
},
{
name: "search result has 0 values for username attribute",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "found 0 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
},
{
name: "search result has more than one value for username attribute",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue, "something-else"},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "found 2 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
},
{
name: "search result has 0 values for uid attribute",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "found 0 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
},
{
name: "search result has 2 values for uid attribute",
providerConfig: providerConfig,
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserSearchResultDNValue,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue), []byte("other-uid-value")},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
wantErr: "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
conn := mockldapconn.NewMockConn(ctrl)
if tt.setupMocks != nil {
tt.setupMocks(conn)
}
dialWasAttempted := false
providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) {
dialWasAttempted = true
require.Equal(t, providerConfig.Host, addr.Endpoint())
if tt.dialError != nil {
return nil, tt.dialError
}
return conn, nil
})
provider := New(*providerConfig)
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
err := provider.PerformRefresh(context.Background(), testUserSearchResultDNValue, testUserSearchResultUsernameAttributeValue, subject)
if tt.wantErr != "" {
require.Error(t, err)
require.Equal(t, tt.wantErr, err.Error())
} else {
require.NoError(t, err)
}
require.Equal(t, true, dialWasAttempted)
})
}
}
func TestTestConnection(t *testing.T) { func TestTestConnection(t *testing.T) {
providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig { providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig {
config := &ProviderConfig{ config := &ProviderConfig{

View File

@ -17,9 +17,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/upstreamldap" "go.pinniped.dev/internal/upstreamldap"
"go.pinniped.dev/test/testlib" "go.pinniped.dev/test/testlib"
) )
@ -74,7 +74,7 @@ func TestLDAPSearch_Parallel(t *testing.T) {
password string password string
provider *upstreamldap.Provider provider *upstreamldap.Provider
wantError string wantError string
wantAuthResponse *authenticator.Response wantAuthResponse *authenticators.Response
wantUnauthenticated bool wantUnauthenticated bool
}{ }{
{ {
@ -82,8 +82,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
username: "pinny", username: "pinny",
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(nil)), provider: upstreamldap.New(*providerConfig(nil)),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -94,8 +94,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
p.Host = "127.0.0.1:" + ldapLocalhostPort p.Host = "127.0.0.1:" + ldapLocalhostPort
p.ConnectionProtocol = upstreamldap.StartTLS p.ConnectionProtocol = upstreamldap.StartTLS
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -103,8 +103,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
username: "pinny", username: "pinny",
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -112,8 +112,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
username: "pinny", username: "pinny",
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -124,8 +124,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
p.UserSearch.UsernameAttribute = "dn" p.UserSearch.UsernameAttribute = "dn"
p.UserSearch.Filter = "cn={}" p.UserSearch.Filter = "cn={}"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -135,8 +135,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.UserSearch.Filter = "(|(cn={})(mail={}))" p.UserSearch.Filter = "(|(cn={})(mail={}))"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -146,8 +146,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.UserSearch.Filter = "(|(cn={})(mail={}))" p.UserSearch.Filter = "(|(cn={})(mail={}))"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -155,8 +155,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
username: "pinny", username: "pinny",
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -164,8 +164,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
username: "pinny", username: "pinny",
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -173,8 +173,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
username: "seAl", // note that this is not case-sensitive! sn=Seal. The server decides which fields are compared case-sensitive. username: "seAl", // note that this is not case-sensitive! sn=Seal. The server decides which fields are compared case-sensitive.
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, // note that the final answer has case preserved from the entry User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev", // note that the final answer has case preserved from the entry
}, },
}, },
{ {
@ -186,8 +186,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
p.UserSearch.UsernameAttribute = "givenName" p.UserSearch.UsernameAttribute = "givenName"
p.UserSearch.UIDAttribute = "givenName" p.UserSearch.UIDAttribute = "givenName"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -198,8 +198,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
p.UserSearch.Filter = "givenName={}" p.UserSearch.Filter = "givenName={}"
p.UserSearch.UsernameAttribute = "cn" p.UserSearch.UsernameAttribute = "cn"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -219,8 +219,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.GroupSearch.Base = "" p.GroupSearch.Base = ""
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -230,8 +230,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -241,11 +241,11 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.GroupSearch.GroupNameAttribute = "dn" p.GroupSearch.GroupNameAttribute = "dn"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{ User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev", "cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
"cn=seals,ou=groups,dc=pinniped,dc=dev", "cn=seals,ou=groups,dc=pinniped,dc=dev",
}}, }}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -255,11 +255,11 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.GroupSearch.GroupNameAttribute = "" p.GroupSearch.GroupNameAttribute = ""
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{ User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev", "cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
"cn=seals,ou=groups,dc=pinniped,dc=dev", "cn=seals,ou=groups,dc=pinniped,dc=dev",
}}, }}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -269,8 +269,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -280,8 +280,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))" p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -291,8 +291,8 @@ func TestLDAPSearch_Parallel(t *testing.T) {
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}}, DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, },
}, },
{ {
@ -670,14 +670,15 @@ func TestSimultaneousLDAPRequestsOnSingleProvider(t *testing.T) {
// Record failures but allow the test to keep running so that all the background goroutines have a chance to try. // Record failures but allow the test to keep running so that all the background goroutines have a chance to try.
assert.NoError(t, result.err) assert.NoError(t, result.err)
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not") assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
assert.Equal(t, &authenticator.Response{ assert.Equal(t, &authenticators.Response{
User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
DN: "cn=pinny,ou=users,dc=pinniped,dc=dev",
}, result.response) }, result.response)
} }
} }
type authUserResult struct { type authUserResult struct {
response *authenticator.Response response *authenticators.Response
authenticated bool authenticated bool
err error err error
} }

View File

@ -44,7 +44,7 @@ func TestSupervisorLogin(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
maybeSkip func(t *testing.T) maybeSkip func(t *testing.T)
createIDP func(t *testing.T) createIDP func(t *testing.T) string
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
wantDownstreamIDTokenSubjectToMatch string wantDownstreamIDTokenSubjectToMatch string
wantDownstreamIDTokenUsernameToMatch string wantDownstreamIDTokenUsernameToMatch string
@ -55,16 +55,16 @@ func TestSupervisorLogin(t *testing.T) {
// We don't necessarily have any way to revoke the user's session on the upstream provider, // We don't necessarily have any way to revoke the user's session on the upstream provider,
// so to cause the upstream refresh to fail we can cheat by manipulating the user's session // so to cause the upstream refresh to fail we can cheat by manipulating the user's session
// data in such a way that it should cause the next upstream refresh attempt to fail. // data in such a way that it should cause the next upstream refresh attempt to fail.
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData) breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName string)
}{ }{
{ {
name: "oidc with default username and groups claim settings", name: "oidc with default username and groups claim settings",
maybeSkip: func(t *testing.T) { maybeSkip: func(t *testing.T) {
// never need to skip this test // never need to skip this test
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer, Issuer: env.SupervisorUpstreamOIDC.Issuer,
TLS: &idpv1alpha1.TLSSpec{ TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
@ -73,9 +73,11 @@ func TestSupervisorLogin(t *testing.T) {
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
}, },
}, idpv1alpha1.PhaseReady) }, idpv1alpha1.PhaseReady)
return oidcIDP.Name
}, },
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) { breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType) require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
@ -90,9 +92,9 @@ func TestSupervisorLogin(t *testing.T) {
maybeSkip: func(t *testing.T) { maybeSkip: func(t *testing.T) {
// never need to skip this test // never need to skip this test
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer, Issuer: env.SupervisorUpstreamOIDC.Issuer,
TLS: &idpv1alpha1.TLSSpec{ TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
@ -108,9 +110,11 @@ func TestSupervisorLogin(t *testing.T) {
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
}, },
}, idpv1alpha1.PhaseReady) }, idpv1alpha1.PhaseReady)
return oidcIDP.Name
}, },
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow,
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) { breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType) require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
@ -124,9 +128,9 @@ func TestSupervisorLogin(t *testing.T) {
maybeSkip: func(t *testing.T) { maybeSkip: func(t *testing.T) {
// never need to skip this test // never need to skip this test
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer, Issuer: env.SupervisorUpstreamOIDC.Issuer,
TLS: &idpv1alpha1.TLSSpec{ TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
@ -138,6 +142,7 @@ func TestSupervisorLogin(t *testing.T) {
AllowPasswordGrant: true, // allow the CLI password flow for this OIDCIdentityProvider AllowPasswordGrant: true, // allow the CLI password flow for this OIDCIdentityProvider
}, },
}, idpv1alpha1.PhaseReady) }, idpv1alpha1.PhaseReady)
return oidcIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -148,7 +153,8 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: func(t *testing.T, customSessionData *psession.CustomSessionData) { breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType) require.Equal(t, psession.ProviderTypeOIDC, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken)
customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token"
@ -166,7 +172,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("LDAP integration test requires connectivity to an LDAP server") t.Skip("LDAP integration test requires connectivity to an LDAP server")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
map[string]string{ map[string]string{
@ -204,6 +210,7 @@ func TestSupervisorLogin(t *testing.T) {
secret.Name, secret.ResourceVersion, secret.Name, secret.ResourceVersion,
) )
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
return ldapIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -214,7 +221,13 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.LDAP.UserDN)
fositeSessionData := pinnipedSession.Fosite
fositeSessionData.Claims.Subject = "not-right"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+ "ldaps://"+env.SupervisorUpstreamLDAP.Host+
@ -233,7 +246,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("LDAP integration test requires connectivity to an LDAP server") t.Skip("LDAP integration test requires connectivity to an LDAP server")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
map[string]string{ map[string]string{
@ -271,6 +284,7 @@ func TestSupervisorLogin(t *testing.T) {
secret.Name, secret.ResourceVersion, secret.Name, secret.ResourceVersion,
) )
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
return ldapIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -281,7 +295,13 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.LDAP.UserDN)
fositeSessionData := pinnipedSession.Fosite
fositeSessionData.Claims.Extra["username"] = "not-the-same"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+ "ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+
@ -300,7 +320,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("LDAP integration test requires connectivity to an LDAP server") t.Skip("LDAP integration test requires connectivity to an LDAP server")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
map[string]string{ map[string]string{
@ -338,6 +358,7 @@ func TestSupervisorLogin(t *testing.T) {
secret.Name, secret.ResourceVersion, secret.Name, secret.ResourceVersion,
) )
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
return ldapIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -348,9 +369,8 @@ func TestSupervisorLogin(t *testing.T) {
true, true,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", wantErrorType: "access_denied",
wantErrorType: "access_denied",
}, },
{ {
name: "ldap login still works after updating bind secret", name: "ldap login still works after updating bind secret",
@ -360,7 +380,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("LDAP integration test requires connectivity to an LDAP server") t.Skip("LDAP integration test requires connectivity to an LDAP server")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
@ -416,6 +436,7 @@ func TestSupervisorLogin(t *testing.T) {
requireEventually.NoError(err) requireEventually.NoError(err)
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg) requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
}, time.Minute, 500*time.Millisecond) }, time.Minute, 500*time.Millisecond)
return ldapIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -426,7 +447,12 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.LDAP.UserDN)
customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+ "ldaps://"+env.SupervisorUpstreamLDAP.Host+
@ -445,7 +471,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("LDAP integration test requires connectivity to an LDAP server") t.Skip("LDAP integration test requires connectivity to an LDAP server")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
@ -515,6 +541,7 @@ func TestSupervisorLogin(t *testing.T) {
requireEventually.NoError(err) requireEventually.NoError(err)
requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg) requireEventuallySuccessfulLDAPIdentityProviderConditions(t, requireEventually, ldapIDP, expectedMsg)
}, time.Minute, 500*time.Millisecond) }, time.Minute, 500*time.Millisecond)
return ldapIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -525,7 +552,12 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeLDAP, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.LDAP.UserDN)
customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+ "ldaps://"+env.SupervisorUpstreamLDAP.Host+
@ -547,7 +579,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("Active Directory hostname not specified") t.Skip("Active Directory hostname not specified")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
map[string]string{ map[string]string{
@ -570,6 +602,7 @@ func TestSupervisorLogin(t *testing.T) {
secret.Name, secret.ResourceVersion, secret.Name, secret.ResourceVersion,
) )
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
return adIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -580,7 +613,13 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
fositeSessionData := pinnipedSession.Fosite
fositeSessionData.Claims.Extra["username"] = "not-the-same"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -601,7 +640,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("Active Directory hostname not specified") t.Skip("Active Directory hostname not specified")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
map[string]string{ map[string]string{
@ -638,6 +677,7 @@ func TestSupervisorLogin(t *testing.T) {
secret.Name, secret.ResourceVersion, secret.Name, secret.ResourceVersion,
) )
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
return adIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -648,7 +688,13 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
fositeSessionData := pinnipedSession.Fosite
fositeSessionData.Claims.Subject = "not-right"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -670,7 +716,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("Active Directory hostname not specified") t.Skip("Active Directory hostname not specified")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
@ -711,6 +757,7 @@ func TestSupervisorLogin(t *testing.T) {
requireEventually.NoError(err) requireEventually.NoError(err)
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg) requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
}, time.Minute, 500*time.Millisecond) }, time.Minute, 500*time.Millisecond)
return adIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -721,7 +768,12 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -743,7 +795,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("Active Directory hostname not specified") t.Skip("Active Directory hostname not specified")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
@ -799,6 +851,7 @@ func TestSupervisorLogin(t *testing.T) {
requireEventually.NoError(err) requireEventually.NoError(err)
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg) requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t, requireEventually, adIDP, expectedMsg)
}, time.Minute, 500*time.Millisecond) }, time.Minute, 500*time.Millisecond)
return adIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -809,7 +862,12 @@ func TestSupervisorLogin(t *testing.T) {
false, false,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _ string) {
customSessionData := pinnipedSession.Custom
require.Equal(t, psession.ProviderTypeActiveDirectory, customSessionData.ProviderType)
require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN)
customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+
@ -831,7 +889,7 @@ func TestSupervisorLogin(t *testing.T) {
t.Skip("Active Directory hostname not specified") t.Skip("Active Directory hostname not specified")
} }
}, },
createIDP: func(t *testing.T) { createIDP: func(t *testing.T) string {
t.Helper() t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth,
map[string]string{ map[string]string{
@ -854,6 +912,7 @@ func TestSupervisorLogin(t *testing.T) {
secret.Name, secret.ResourceVersion, secret.Name, secret.ResourceVersion,
) )
requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg)
return adIDP.Name
}, },
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t, requestAuthorizationUsingCLIPasswordFlow(t,
@ -864,10 +923,93 @@ func TestSupervisorLogin(t *testing.T) {
true, true,
) )
}, },
breakRefreshSessionData: nil, // upstream refresh not yet implemented for this IDP type breakRefreshSessionData: nil,
wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.",
wantErrorType: "access_denied", wantErrorType: "access_denied",
}, },
{
name: "ldap refresh fails when username changes from email as username to dn as username",
maybeSkip: func(t *testing.T) {
t.Helper()
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
t.Skip("LDAP integration test requires connectivity to an LDAP server")
}
},
createIDP: func(t *testing.T) string {
t.Helper()
secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
map[string]string{
v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername,
v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
},
)
ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
Host: env.SupervisorUpstreamLDAP.Host,
TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
},
Bind: idpv1alpha1.LDAPIdentityProviderBind{
SecretName: secret.Name,
},
UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{
Base: env.SupervisorUpstreamLDAP.UserSearchBase,
Filter: "",
Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{
Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName,
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
},
},
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
Base: env.SupervisorUpstreamLDAP.GroupSearchBase,
Filter: "",
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
GroupName: "dn",
},
},
}, idpv1alpha1.LDAPPhaseReady)
expectedMsg := fmt.Sprintf(
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername,
secret.Name, secret.ResourceVersion,
)
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
return ldapIDP.Name
},
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
requestAuthorizationUsingCLIPasswordFlow(t,
downstreamAuthorizeURL,
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login
httpClient,
false,
)
},
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string) {
// get the idp, update the config.
client := testlib.NewSupervisorClientset(t)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// Create the LDAPIdentityProvider using GenerateName to get a random name.
upstreams := client.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace)
ldapIDP, err := upstreams.Get(ctx, idpName, metav1.GetOptions{})
require.NoError(t, err)
ldapIDP.Spec.UserSearch.Attributes.Username = "dn"
_, err = upstreams.Update(ctx, ldapIDP, metav1.UpdateOptions{})
require.NoError(t, err)
time.Sleep(10 * time.Second) // wait for controllers to pick up the change
},
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
) + "$",
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$",
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
},
} }
for _, test := range tests { for _, test := range tests {
tt := test tt := test
@ -1007,9 +1149,9 @@ func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions(t *tes
func testSupervisorLogin( func testSupervisorLogin(
t *testing.T, t *testing.T,
createIDP func(t *testing.T), createIDP func(t *testing.T) string,
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client), requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
breakRefreshSessionData func(t *testing.T, customSessionData *psession.CustomSessionData), breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName string),
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
wantErrorDescription string, wantErrorType string, wantErrorDescription string, wantErrorType string,
) { ) {
@ -1097,7 +1239,7 @@ func testSupervisorLogin(
}, 30*time.Second, 200*time.Millisecond) }, 30*time.Second, 200*time.Millisecond)
// Create upstream IDP and wait for it to become ready. // Create upstream IDP and wait for it to become ready.
createIDP(t) idpName := createIDP(t)
// Perform OIDC discovery for our downstream. // Perform OIDC discovery for our downstream.
var discovery *coreosoidc.Provider var discovery *coreosoidc.Provider
@ -1191,7 +1333,7 @@ func testSupervisorLogin(
// Next mutate the part of the session that is used during upstream refresh. // Next mutate the part of the session that is used during upstream refresh.
pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession) pinnipedSession, ok := storedRefreshSession.GetSession().(*psession.PinnipedSession)
require.True(t, ok, "should have been able to cast session data to PinnipedSession") require.True(t, ok, "should have been able to cast session data to PinnipedSession")
breakRefreshSessionData(t, pinnipedSession.Custom) breakRefreshSessionData(t, pinnipedSession, idpName)
// Then save the mutated Secret back to Kubernetes. // Then save the mutated Secret back to Kubernetes.
// There is no update function, so delete and create again at the same name. // There is no update function, so delete and create again at the same name.
@ -1204,9 +1346,8 @@ func testSupervisorLogin(
require.Error(t, err) require.Error(t, err)
require.Regexp(t, require.Regexp(t,
regexp.QuoteMeta("oauth2: cannot fetch token: 401 Unauthorized\n")+ regexp.QuoteMeta("oauth2: cannot fetch token: 401 Unauthorized\n")+
regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed using provider '`)+ regexp.QuoteMeta(`Response: {"error":"error","error_description":"Error during upstream refresh. Upstream refresh failed`)+
"[^']+"+ // this would be the name of the identity provider CR "[^']+",
regexp.QuoteMeta(fmt.Sprintf(`' of type '%s'."`, pinnipedSession.Custom.ProviderType)),
err.Error(), err.Error(),
) )
} }