diff --git a/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl b/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl index 1e5873d32..f421ecaf6 100644 --- a/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl +++ b/apis/supervisor/oidc/types_supervisor_oidc.go.tmpl @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.21/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.22/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.23/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.24/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.25/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.26/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.26/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.26/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.26/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.27/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.27/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.27/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.27/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/1.28/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/1.28/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/1.28/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/1.28/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go b/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go index 1e5873d32..f421ecaf6 100644 --- a/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go +++ b/generated/latest/apis/supervisor/oidc/types_supervisor_oidc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -29,6 +29,10 @@ const ( // IDTokenClaimSubject is name of the subject claim defined by the OIDC spec. IDTokenClaimSubject = "sub" + // IDTokenSubClaimIDPNameQueryParam is the name of the query param used in the values of the "sub" claim + // in Supervisor-issued ID tokens to identify with which external identity provider the user authenticated. + IDTokenSubClaimIDPNameQueryParam = "idpName" + // IDTokenClaimAuthorizedParty is name of the authorized party claim defined by the OIDC spec. IDTokenClaimAuthorizedParty = "azp" diff --git a/internal/controller/supervisorstorage/garbage_collector_test.go b/internal/controller/supervisorstorage/garbage_collector_test.go index 39d530228..a52312567 100644 --- a/internal/controller/supervisorstorage/garbage_collector_test.go +++ b/internal/controller/supervisorstorage/garbage_collector_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorstorage @@ -34,6 +34,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" ) func TestGarbageCollectorControllerInformerFilters(t *testing.T) { @@ -359,7 +360,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -483,7 +484,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -560,7 +561,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -631,7 +632,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -702,7 +703,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -775,7 +776,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithResourceUID("upstream-oidc-provider-uid"). // make the upstream revocation fail in a retryable way WithRevokeTokenError(dynamicupstreamprovider.NewRetryableRevocationError(errors.New("some retryable upstream revocation error"))) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -800,7 +801,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithResourceUID("upstream-oidc-provider-uid"). // make the upstream revocation fail in a non-retryable way WithRevokeTokenError(errors.New("some upstream revocation error not worth retrying")) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -879,7 +880,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(errors.New("some upstream revocation error")) // the upstream revocation will fail - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -1002,7 +1003,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -1126,7 +1127,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -1204,7 +1205,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) @@ -1281,7 +1282,7 @@ func TestGarbageCollectorControllerSync(t *testing.T) { WithName("upstream-oidc-provider-name"). WithResourceUID("upstream-oidc-provider-uid"). WithRevokeTokenError(nil) - idpListerBuilder := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) + idpListerBuilder := testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyOIDCUpstream.Build()) startInformersAndController(idpListerBuilder.BuildDynamicUpstreamIDPProvider()) r.NoError(controllerlib.TestSync(t, subject, *syncContext)) diff --git a/internal/federationdomain/downstreamsession/downstream_session.go b/internal/federationdomain/downstreamsession/downstream_session.go index 6be000f08..63ad31f1b 100644 --- a/internal/federationdomain/downstreamsession/downstream_session.go +++ b/internal/federationdomain/downstreamsession/downstream_session.go @@ -6,59 +6,27 @@ package downstreamsession import ( "context" - "errors" "fmt" - "net/url" "time" "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/token/jwt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/strings/slices" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" - "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/federationdomain/oidc" - "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" - "go.pinniped.dev/pkg/oidcclient/oidctypes" ) -const ( - // The name of the email claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - emailClaimName = oidcapi.ScopeEmail - - // The name of the email_verified claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - emailVerifiedClaimName = "email_verified" - - requiredClaimMissingErr = constable.Error("required claim in upstream ID token missing") - requiredClaimInvalidFormatErr = constable.Error("required claim in upstream ID token has invalid format") - requiredClaimEmptyErr = constable.Error("required claim in upstream ID token is empty") - emailVerifiedClaimInvalidFormatErr = constable.Error("email_verified claim in upstream ID token has invalid format") - emailVerifiedClaimFalseErr = constable.Error("email_verified claim in upstream ID token has false value") - idTransformUnexpectedErr = constable.Error("configured identity transformation or policy resulted in unexpected error") - - idpNameSubjectQueryParam = "idpName" -) - -type Identity struct { - // Note that the username is stored in SessionData.Username. - SessionData *psession.CustomSessionData - Groups []string - Subject string - AdditionalClaims map[string]interface{} -} +const idTransformUnexpectedErr = constable.Error("configured identity transformation or policy resulted in unexpected error") // MakeDownstreamSession creates a downstream OIDC session. -func MakeDownstreamSession( - identity *Identity, - grantedScopes []string, - clientID string, -) *psession.PinnipedSession { +func MakeDownstreamSession(identity *resolvedprovider.Identity, grantedScopes []string, clientID string) *psession.PinnipedSession { now := time.Now().UTC() openIDSession := &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ @@ -91,105 +59,6 @@ func MakeDownstreamSession( return openIDSession } -func MakeDownstreamLDAPOrADCustomSessionData( - ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, - idpType psession.ProviderType, - authenticateResponse *authenticators.Response, - username string, - untransformedUpstreamUsername string, - untransformedUpstreamGroups []string, -) *psession.CustomSessionData { - customSessionData := &psession.CustomSessionData{ - Username: username, - UpstreamUsername: untransformedUpstreamUsername, - UpstreamGroups: untransformedUpstreamGroups, - ProviderUID: ldapUpstream.GetResourceUID(), - ProviderName: ldapUpstream.GetName(), - ProviderType: idpType, - } - - if idpType == psession.ProviderTypeLDAP { - customSessionData.LDAP = &psession.LDAPSessionData{ - UserDN: authenticateResponse.DN, - ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, - } - } - - if idpType == psession.ProviderTypeActiveDirectory { - customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{ - UserDN: authenticateResponse.DN, - ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, - } - } - - return customSessionData -} - -func MakeDownstreamOIDCCustomSessionData( - oidcUpstream upstreamprovider.UpstreamOIDCIdentityProviderI, - token *oidctypes.Token, - username string, - untransformedUpstreamUsername string, - untransformedUpstreamGroups []string, -) (*psession.CustomSessionData, error) { - upstreamSubject, err := ExtractStringClaimValue(oidcapi.IDTokenClaimSubject, oidcUpstream.GetName(), token.IDToken.Claims) - if err != nil { - return nil, err - } - upstreamIssuer, err := ExtractStringClaimValue(oidcapi.IDTokenClaimIssuer, oidcUpstream.GetName(), token.IDToken.Claims) - if err != nil { - return nil, err - } - - customSessionData := &psession.CustomSessionData{ - Username: username, - UpstreamUsername: untransformedUpstreamUsername, - UpstreamGroups: untransformedUpstreamGroups, - ProviderUID: oidcUpstream.GetResourceUID(), - ProviderName: oidcUpstream.GetName(), - ProviderType: psession.ProviderTypeOIDC, - OIDC: &psession.OIDCSessionData{ - UpstreamIssuer: upstreamIssuer, - UpstreamSubject: upstreamSubject, - }, - } - - const pleaseCheck = "please check configuration of OIDCIdentityProvider and the client in the " + - "upstream provider's API/UI and try to get a refresh token if possible" - logKV := []interface{}{ - "upstreamName", oidcUpstream.GetName(), - "scopes", oidcUpstream.GetScopes(), - "additionalParams", oidcUpstream.GetAdditionalAuthcodeParams(), - } - - hasRefreshToken := token.RefreshToken != nil && token.RefreshToken.Token != "" - hasAccessToken := token.AccessToken != nil && token.AccessToken.Token != "" - switch { - case hasRefreshToken: // we prefer refresh tokens, so check for this first - customSessionData.OIDC.UpstreamRefreshToken = token.RefreshToken.Token - case hasAccessToken: // as a fallback, we can use the access token as long as there is a userinfo endpoint - if !oidcUpstream.HasUserInfoURL() { - plog.Warning("access token was returned by upstream provider during login without a refresh token "+ - "and there was no userinfo endpoint available on the provider. "+pleaseCheck, logKV...) - return nil, errors.New("access token was returned by upstream provider but there was no userinfo endpoint") - } - plog.Info("refresh token not returned by upstream provider during login, using access token instead. "+pleaseCheck, logKV...) - customSessionData.OIDC.UpstreamAccessToken = token.AccessToken.Token - // When we are in a flow where we will be performing access token based refresh, issue a warning to the client if the access - // token lifetime is very short, since that would mean that the user's session is very short. - // The warnings are stored here and will be processed by the token handler. - threeHoursFromNow := metav1.NewTime(time.Now().Add(3 * time.Hour)) - if !token.AccessToken.Expiry.IsZero() && token.AccessToken.Expiry.Before(&threeHoursFromNow) { - customSessionData.Warnings = append(customSessionData.Warnings, "Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in.") - } - default: - plog.Warning("refresh token and access token not returned by upstream provider during login. "+pleaseCheck, logKV...) - return nil, errors.New("neither access token nor refresh token returned by upstream provider") - } - - return customSessionData, nil -} - // AutoApproveScopes auto-grants the scopes which we support and for which we do not require end-user approval, // if they were requested. This should only be called after it has been validated that the client is allowed to request // the scopes that it requested (which is a check performed by fosite). @@ -216,46 +85,8 @@ func AutoApproveScopes(authorizeRequester fosite.AuthorizeRequester) { } } -// GetDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order. -func GetDownstreamIdentityFromUpstreamIDToken( - upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, - idTokenClaims map[string]interface{}, - idpDisplayName string, -) (string, string, []string, error) { - subject, username, err := getSubjectAndUsernameFromUpstreamIDToken(upstreamIDPConfig, idTokenClaims, idpDisplayName) - if err != nil { - return "", "", nil, err - } - - groups, err := GetGroupsFromUpstreamIDToken(upstreamIDPConfig, idTokenClaims) - if err != nil { - return "", "", nil, err - } - - return subject, username, groups, err -} - -// MapAdditionalClaimsFromUpstreamIDToken returns the additionalClaims mapped from the upstream token, if any. -func MapAdditionalClaimsFromUpstreamIDToken( - upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, - idTokenClaims map[string]interface{}, -) map[string]interface{} { - mapped := make(map[string]interface{}, len(upstreamIDPConfig.GetAdditionalClaimMappings())) - for downstreamClaimName, upstreamClaimName := range upstreamIDPConfig.GetAdditionalClaimMappings() { - upstreamClaimValue, ok := idTokenClaims[upstreamClaimName] - if !ok { - plog.Warning( - "additionalClaims mapping claim in upstream ID token missing", - "upstreamName", upstreamIDPConfig.GetName(), - "claimName", upstreamClaimName, - ) - } else { - mapped[downstreamClaimName] = upstreamClaimValue - } - } - return mapped -} - +// ApplyIdentityTransformations applies an identity transformation pipeline to an upstream identity to transform +// or potentially reject the identity. func ApplyIdentityTransformations( ctx context.Context, identityTransforms *idtransform.TransformationPipeline, @@ -279,185 +110,3 @@ func ApplyIdentityTransformations( ) return transformationResult.Username, transformationResult.Groups, nil } - -func getSubjectAndUsernameFromUpstreamIDToken( - upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, - idTokenClaims map[string]interface{}, - idpDisplayName string, -) (string, string, error) { - // The spec says the "sub" claim is only unique per issuer, - // so we will prepend the issuer string to make it globally unique. - upstreamIssuer, err := ExtractStringClaimValue(oidcapi.IDTokenClaimIssuer, upstreamIDPConfig.GetName(), idTokenClaims) - if err != nil { - return "", "", err - } - upstreamSubject, err := ExtractStringClaimValue(oidcapi.IDTokenClaimSubject, upstreamIDPConfig.GetName(), idTokenClaims) - if err != nil { - return "", "", err - } - subject := downstreamSubjectFromUpstreamOIDC(upstreamIssuer, upstreamSubject, idpDisplayName) - - usernameClaimName := upstreamIDPConfig.GetUsernameClaim() - if usernameClaimName == "" { - return subject, downstreamUsernameFromUpstreamOIDCSubject(upstreamIssuer, upstreamSubject), nil - } - - // If the upstream username claim is configured to be the special "email" claim and the upstream "email_verified" - // claim is present, then validate that the "email_verified" claim is true. - emailVerifiedAsInterface, ok := idTokenClaims[emailVerifiedClaimName] - if usernameClaimName == emailClaimName && ok { - emailVerified, ok := emailVerifiedAsInterface.(bool) - if !ok { - plog.Warning( - "username claim configured as \"email\" and upstream email_verified claim is not a boolean", - "upstreamName", upstreamIDPConfig.GetName(), - "configuredUsernameClaim", usernameClaimName, - "emailVerifiedClaim", emailVerifiedAsInterface, - ) - return "", "", emailVerifiedClaimInvalidFormatErr - } - if !emailVerified { - plog.Warning( - "username claim configured as \"email\" and upstream email_verified claim has false value", - "upstreamName", upstreamIDPConfig.GetName(), - "configuredUsernameClaim", usernameClaimName, - ) - return "", "", emailVerifiedClaimFalseErr - } - } - - username, err := ExtractStringClaimValue(usernameClaimName, upstreamIDPConfig.GetName(), idTokenClaims) - if err != nil { - return "", "", err - } - - return subject, username, nil -} - -func ExtractStringClaimValue(claimName string, upstreamIDPName string, idTokenClaims map[string]interface{}) (string, error) { - value, ok := idTokenClaims[claimName] - if !ok { - plog.Warning( - "required claim in upstream ID token missing", - "upstreamName", upstreamIDPName, - "claimName", claimName, - ) - return "", requiredClaimMissingErr - } - - valueAsString, ok := value.(string) - if !ok { - plog.Warning( - "required claim in upstream ID token is not a string value", - "upstreamName", upstreamIDPName, - "claimName", claimName, - ) - return "", requiredClaimInvalidFormatErr - } - - if valueAsString == "" { - plog.Warning( - "required claim in upstream ID token has an empty string value", - "upstreamName", upstreamIDPName, - "claimName", claimName, - ) - return "", requiredClaimEmptyErr - } - - return valueAsString, nil -} - -func DownstreamSubjectFromUpstreamLDAP( - ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, - authenticateResponse *authenticators.Response, - idpDisplayName string, -) string { - ldapURL := *ldapUpstream.GetURL() - return DownstreamLDAPSubject(authenticateResponse.User.GetUID(), ldapURL, idpDisplayName) -} - -func DownstreamLDAPSubject(uid string, ldapURL url.URL, idpDisplayName string) string { - q := ldapURL.Query() - q.Set(idpNameSubjectQueryParam, idpDisplayName) - q.Set(oidcapi.IDTokenClaimSubject, uid) - ldapURL.RawQuery = q.Encode() - return ldapURL.String() -} - -func downstreamSubjectFromUpstreamOIDC(upstreamIssuerAsString string, upstreamSubject string, idpDisplayName string) string { - return fmt.Sprintf("%s?%s=%s&%s=%s", upstreamIssuerAsString, - idpNameSubjectQueryParam, url.QueryEscape(idpDisplayName), - oidcapi.IDTokenClaimSubject, url.QueryEscape(upstreamSubject), - ) -} - -func downstreamUsernameFromUpstreamOIDCSubject(upstreamIssuerAsString string, upstreamSubject string) string { - return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, - oidcapi.IDTokenClaimSubject, url.QueryEscape(upstreamSubject), - ) -} - -// GetGroupsFromUpstreamIDToken returns mapped group names coerced into a slice of strings. -// It returns nil when there is no configured groups claim name, or then when the configured claim name is not found -// in the provided map of claims. It returns an error when the claim exists but its value cannot be parsed. -func GetGroupsFromUpstreamIDToken( - upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, - idTokenClaims map[string]interface{}, -) ([]string, error) { - groupsClaimName := upstreamIDPConfig.GetGroupsClaim() - if groupsClaimName == "" { - return nil, nil - } - - groupsAsInterface, ok := idTokenClaims[groupsClaimName] - if !ok { - plog.Warning( - "no groups claim in upstream ID token", - "upstreamName", upstreamIDPConfig.GetName(), - "configuredGroupsClaim", groupsClaimName, - ) - return nil, nil // the upstream IDP may have omitted the claim if the user has no groups - } - - groupsAsArray, okAsArray := extractGroups(groupsAsInterface) - if !okAsArray { - plog.Warning( - "groups claim in upstream ID token has invalid format", - "upstreamName", upstreamIDPConfig.GetName(), - "configuredGroupsClaim", groupsClaimName, - ) - return nil, requiredClaimInvalidFormatErr - } - - return groupsAsArray, nil -} - -func extractGroups(groupsAsInterface interface{}) ([]string, bool) { - groupsAsString, okAsString := groupsAsInterface.(string) - if okAsString { - return []string{groupsAsString}, true - } - - groupsAsStringArray, okAsStringArray := groupsAsInterface.([]string) - if okAsStringArray { - return groupsAsStringArray, true - } - - groupsAsInterfaceArray, okAsArray := groupsAsInterface.([]interface{}) - if !okAsArray { - return nil, false - } - - var groupsAsStrings []string - for _, groupAsInterface := range groupsAsInterfaceArray { - groupAsString, okAsString := groupAsInterface.(string) - if !okAsString { - return nil, false - } - if groupAsString != "" { - groupsAsStrings = append(groupsAsStrings, groupAsString) - } - } - - return groupsAsStrings, true -} diff --git a/internal/federationdomain/downstreamsession/downstream_session_test.go b/internal/federationdomain/downstreamsession/downstream_session_test.go index 048c21b1c..91144400f 100644 --- a/internal/federationdomain/downstreamsession/downstream_session_test.go +++ b/internal/federationdomain/downstreamsession/downstream_session_test.go @@ -1,11 +1,10 @@ -// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package downstreamsession import ( "context" - "net/url" "testing" "time" @@ -13,69 +12,8 @@ import ( "go.pinniped.dev/internal/celtransformer" "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/testutil/oidctestutil" ) -func TestMapAdditionalClaimsFromUpstreamIDToken(t *testing.T) { - tests := []struct { - name string - additionalClaimMappings map[string]string - upstreamClaims map[string]interface{} - wantClaims map[string]interface{} - }{ - { - name: "happy path", - additionalClaimMappings: map[string]string{ - "email": "notification_email", - }, - upstreamClaims: map[string]interface{}{ - "notification_email": "test@example.com", - }, - wantClaims: map[string]interface{}{ - "email": "test@example.com", - }, - }, - { - name: "missing", - additionalClaimMappings: map[string]string{ - "email": "email", - }, - upstreamClaims: map[string]interface{}{}, - wantClaims: map[string]interface{}{}, - }, - { - name: "complex", - additionalClaimMappings: map[string]string{ - "complex": "complex", - }, - upstreamClaims: map[string]interface{}{ - "complex": map[string]string{ - "subClaim": "subValue", - }, - }, - wantClaims: map[string]interface{}{ - "complex": map[string]string{ - "subClaim": "subValue", - }, - }, - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - idp := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). - WithAdditionalClaimMappings(test.additionalClaimMappings). - Build() - actual := MapAdditionalClaimsFromUpstreamIDToken(idp, test.upstreamClaims) - - require.Equal(t, test.wantClaims, actual) - }) - } -} - func TestApplyIdentityTransformations(t *testing.T) { tests := []struct { name string @@ -157,118 +95,3 @@ func TestApplyIdentityTransformations(t *testing.T) { }) } } - -func TestDownstreamLDAPSubject(t *testing.T) { - tests := []struct { - name string - uid string - ldapURL string - idpDisplayName string - wantSubject string - }{ - { - name: "simple display name", - uid: "some uid", - ldapURL: "ldaps://server.example.com:1234", - idpDisplayName: "simpleName", - wantSubject: "ldaps://server.example.com:1234?idpName=simpleName&sub=some+uid", - }, - { - name: "interesting display name", - uid: "some uid", - ldapURL: "ldaps://server.example.com:1234", - idpDisplayName: "this is a 👍 display name that 🦭 can handle", - wantSubject: "ldaps://server.example.com:1234?idpName=this+is+a+%F0%9F%91%8D+display+name+that+%F0%9F%A6%AD+can+handle&sub=some+uid", - }, - { - name: "url already has query", - uid: "some uid", - ldapURL: "ldaps://server.example.com:1234?a=1&b=%F0%9F%A6%AD", - idpDisplayName: "some name", - wantSubject: "ldaps://server.example.com:1234?a=1&b=%F0%9F%A6%AD&idpName=some+name&sub=some+uid", - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - url, err := url.Parse(test.ldapURL) - require.NoError(t, err) - - actual := DownstreamLDAPSubject(test.uid, *url, test.idpDisplayName) - - require.Equal(t, test.wantSubject, actual) - }) - } -} - -func TestDownstreamSubjectFromUpstreamOIDC(t *testing.T) { - tests := []struct { - name string - upstreamIssuerAsString string - upstreamSubject string - idpDisplayName string - wantSubject string - }{ - { - name: "simple display name", - upstreamIssuerAsString: "https://server.example.com:1234/path", - upstreamSubject: "some subject", - idpDisplayName: "simpleName", - wantSubject: "https://server.example.com:1234/path?idpName=simpleName&sub=some+subject", - }, - { - name: "interesting display name", - upstreamIssuerAsString: "https://server.example.com:1234/path", - upstreamSubject: "some subject", - idpDisplayName: "this is a 👍 display name that 🦭 can handle", - wantSubject: "https://server.example.com:1234/path?idpName=this+is+a+%F0%9F%91%8D+display+name+that+%F0%9F%A6%AD+can+handle&sub=some+subject", - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - actual := downstreamSubjectFromUpstreamOIDC(test.upstreamIssuerAsString, test.upstreamSubject, test.idpDisplayName) - - require.Equal(t, test.wantSubject, actual) - }) - } -} - -func TestDownstreamUsernameFromUpstreamOIDCSubject(t *testing.T) { - tests := []struct { - name string - upstreamIssuerAsString string - upstreamSubject string - wantSubject string - }{ - { - name: "simple upstreamSubject", - upstreamIssuerAsString: "https://server.example.com:1234/path", - upstreamSubject: "some subject", - wantSubject: "https://server.example.com:1234/path?sub=some+subject", - }, - { - name: "interesting upstreamSubject", - upstreamIssuerAsString: "https://server.example.com:1234/path", - upstreamSubject: "this is a 👍 subject that 🦭 can handle", - wantSubject: "https://server.example.com:1234/path?sub=this+is+a+%F0%9F%91%8D+subject+that+%F0%9F%A6%AD+can+handle", - }, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - actual := downstreamUsernameFromUpstreamOIDCSubject(test.upstreamIssuerAsString, test.upstreamSubject) - - require.Equal(t, test.wantSubject, actual) - }) - } -} diff --git a/internal/federationdomain/downstreamsubject/downstream_subject.go b/internal/federationdomain/downstreamsubject/downstream_subject.go new file mode 100644 index 000000000..5c754d9aa --- /dev/null +++ b/internal/federationdomain/downstreamsubject/downstream_subject.go @@ -0,0 +1,26 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package downstreamsubject + +import ( + "fmt" + "net/url" + + "go.pinniped.dev/generated/latest/apis/supervisor/oidc" +) + +func LDAP(uid string, ldapURL url.URL, idpDisplayName string) string { + q := ldapURL.Query() + q.Set(oidc.IDTokenSubClaimIDPNameQueryParam, idpDisplayName) + q.Set(oidc.IDTokenClaimSubject, uid) + ldapURL.RawQuery = q.Encode() + return ldapURL.String() +} + +func OIDC(upstreamIssuerAsString string, upstreamSubject string, idpDisplayName string) string { + return fmt.Sprintf("%s?%s=%s&%s=%s", upstreamIssuerAsString, + oidc.IDTokenSubClaimIDPNameQueryParam, url.QueryEscape(idpDisplayName), + oidc.IDTokenClaimSubject, url.QueryEscape(upstreamSubject), + ) +} diff --git a/internal/federationdomain/downstreamsubject/downstream_subject_test.go b/internal/federationdomain/downstreamsubject/downstream_subject_test.go new file mode 100644 index 000000000..703624e94 --- /dev/null +++ b/internal/federationdomain/downstreamsubject/downstream_subject_test.go @@ -0,0 +1,93 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package downstreamsubject + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLDAP(t *testing.T) { + tests := []struct { + name string + uid string + ldapURL string + idpDisplayName string + wantSubject string + }{ + { + name: "simple display name", + uid: "some uid", + ldapURL: "ldaps://server.example.com:1234", + idpDisplayName: "simpleName", + wantSubject: "ldaps://server.example.com:1234?idpName=simpleName&sub=some+uid", + }, + { + name: "interesting display name", + uid: "some uid", + ldapURL: "ldaps://server.example.com:1234", + idpDisplayName: "this is a 👍 display name that 🦭 can handle", + wantSubject: "ldaps://server.example.com:1234?idpName=this+is+a+%F0%9F%91%8D+display+name+that+%F0%9F%A6%AD+can+handle&sub=some+uid", + }, + { + name: "url already has query", + uid: "some uid", + ldapURL: "ldaps://server.example.com:1234?a=1&b=%F0%9F%A6%AD", + idpDisplayName: "some name", + wantSubject: "ldaps://server.example.com:1234?a=1&b=%F0%9F%A6%AD&idpName=some+name&sub=some+uid", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + u, err := url.Parse(test.ldapURL) + require.NoError(t, err) + + actual := LDAP(test.uid, *u, test.idpDisplayName) + + require.Equal(t, test.wantSubject, actual) + }) + } +} + +func TestOIDC(t *testing.T) { + tests := []struct { + name string + upstreamIssuerAsString string + upstreamSubject string + idpDisplayName string + wantSubject string + }{ + { + name: "simple display name", + upstreamIssuerAsString: "https://server.example.com:1234/path", + upstreamSubject: "some subject", + idpDisplayName: "simpleName", + wantSubject: "https://server.example.com:1234/path?idpName=simpleName&sub=some+subject", + }, + { + name: "interesting display name", + upstreamIssuerAsString: "https://server.example.com:1234/path", + upstreamSubject: "some subject", + idpDisplayName: "this is a 👍 display name that 🦭 can handle", + wantSubject: "https://server.example.com:1234/path?idpName=this+is+a+%F0%9F%91%8D+display+name+that+%F0%9F%A6%AD+can+handle&sub=some+subject", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actual := OIDC(test.upstreamIssuerAsString, test.upstreamSubject, test.idpDisplayName) + + require.Equal(t, test.wantSubject, actual) + }) + } +} diff --git a/internal/federationdomain/endpoints/auth/auth_handler.go b/internal/federationdomain/endpoints/auth/auth_handler.go index ea4904835..de9eb42aa 100644 --- a/internal/federationdomain/endpoints/auth/auth_handler.go +++ b/internal/federationdomain/endpoints/auth/auth_handler.go @@ -5,7 +5,6 @@ package auth import ( - "context" "fmt" "net/http" "net/url" @@ -14,21 +13,17 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/token/jwt" - "golang.org/x/oauth2" "k8s.io/utils/strings/slices" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/federationdomain/csrftoken" "go.pinniped.dev/internal/federationdomain/downstreamsession" - "go.pinniped.dev/internal/federationdomain/endpoints/login" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/federationdomain/formposthtml" "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/federationdomain/resolvedprovider" - "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/httputil/responseutil" "go.pinniped.dev/internal/httputil/securityheader" - "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/psession" "go.pinniped.dev/pkg/oidcclient/nonce" @@ -40,16 +35,8 @@ const ( promptParamNone = "none" ) -var ( - errUnexpectedUpstreamError = &fosite.RFC6749Error{ - ErrorField: "error", // this string matches what fosite uses for generic errors - DescriptionField: "Unexpected error during upstream authentication.", - CodeField: http.StatusBadGateway, - } -) - type authorizeHandler struct { - downstreamIssuer string + downstreamIssuerURL string idpFinder federationdomainproviders.FederationDomainIdentityProvidersFinderI oauthHelperWithoutStorage fosite.OAuth2Provider oauthHelperWithStorage fosite.OAuth2Provider @@ -61,7 +48,7 @@ type authorizeHandler struct { } func NewHandler( - downstreamIssuer string, + downstreamIssuerURL string, idpFinder federationdomainproviders.FederationDomainIdentityProvidersFinderI, oauthHelperWithoutStorage fosite.OAuth2Provider, oauthHelperWithStorage fosite.OAuth2Provider, @@ -72,7 +59,7 @@ func NewHandler( cookieCodec oidc.Codec, ) http.Handler { h := &authorizeHandler{ - downstreamIssuer: downstreamIssuer, + downstreamIssuerURL: downstreamIssuerURL, idpFinder: idpFinder, oauthHelperWithoutStorage: oauthHelperWithoutStorage, oauthHelperWithStorage: oauthHelperWithStorage, @@ -137,13 +124,13 @@ func (h *authorizeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Redirect to the IDP chooser page with all the same query/form params. When the user chooses an IDP, // it will redirect back to here with all the same params again, with the pinniped_idp_name param added. http.Redirect(w, r, - fmt.Sprintf("%s%s?%s", h.downstreamIssuer, oidc.ChooseIDPEndpointPath, r.Form.Encode()), + fmt.Sprintf("%s%s?%s", h.downstreamIssuerURL, oidc.ChooseIDPEndpointPath, r.Form.Encode()), http.StatusSeeOther, ) return } - oidcUpstream, ldapUpstream, err := chooseUpstreamIDP(idpNameQueryParamValue, h.idpFinder) + upstreamProvider, err := chooseUpstreamIDP(idpNameQueryParamValue, h.idpFinder) if err != nil { oidc.WriteAuthorizeError(r, w, h.oauthHelperWithoutStorage, @@ -155,7 +142,7 @@ func (h *authorizeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - h.authorize(w, r, requestedBrowserlessFlow, idpNameQueryParamValue, oidcUpstream, ldapUpstream) + h.authorize(w, r, requestedBrowserlessFlow, idpNameQueryParamValue, upstreamProvider) } func (h *authorizeHandler) authorize( @@ -163,8 +150,7 @@ func (h *authorizeHandler) authorize( r *http.Request, requestedBrowserlessFlow bool, idpNameQueryParamValue string, - oidcUpstream *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, - ldapUpstream *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, + upstreamProvider resolvedprovider.FederationDomainResolvedIdentityProvider, ) { // Browser flows do not need session storage at this step. For browser flows, the request parameters // should be forwarded to the next step as upstream state parameters to avoid storing session state @@ -191,9 +177,9 @@ func (h *authorizeHandler) authorize( downstreamsession.AutoApproveScopes(authorizeRequester) if requestedBrowserlessFlow { - err = h.authorizeWithoutBrowser(r, w, oauthHelper, authorizeRequester, oidcUpstream, ldapUpstream) + err = h.authorizeWithoutBrowser(r, w, oauthHelper, authorizeRequester, upstreamProvider) } else { - err = h.authorizeWithBrowser(r, w, oauthHelper, authorizeRequester, oidcUpstream, ldapUpstream) + err = h.authorizeWithBrowser(r, w, oauthHelper, authorizeRequester, upstreamProvider) } if err != nil { oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, requestedBrowserlessFlow) @@ -205,8 +191,7 @@ func (h *authorizeHandler) authorizeWithoutBrowser( w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, - oidcUpstream *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, - ldapUpstream *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, + upstreamProvider resolvedprovider.FederationDomainResolvedIdentityProvider, ) error { if err := requireStaticClientForUsernameAndPasswordHeaders(authorizeRequester); err != nil { return err @@ -217,31 +202,9 @@ func (h *authorizeHandler) authorizeWithoutBrowser( return err } - var identity *downstreamsession.Identity - switch { - case oidcUpstream != nil: - identity, err = handleAuthRequestForOIDCUpstreamPasswordGrant(r.Context(), - oidcUpstream.Provider, - oidcUpstream.Transforms, - oidcUpstream.DisplayName, - submittedUsername, - submittedPassword, - ) - case ldapUpstream != nil: - identity, err = handleAuthRequestForLDAPUpstreamCLIFlow(r.Context(), - ldapUpstream.Provider, - ldapUpstream.Transforms, - ldapUpstream.DisplayName, - submittedUsername, - submittedPassword, - ldapUpstream.SessionProviderType, - !slices.Contains(authorizeRequester.GetGrantedScopes(), oidcapi.ScopeGroups), - ) - default: - // It should not actually be possible to reach this case. - return fosite.ErrServerError.WithHint("Huh? Unknown upstream IDP type.") - } + groupsWillBeIgnored := !slices.Contains(authorizeRequester.GetGrantedScopes(), oidcapi.ScopeGroups) + identity, err := upstreamProvider.Login(r.Context(), submittedUsername, submittedPassword, groupsWillBeIgnored) if err != nil { return err } @@ -260,36 +223,33 @@ func (h *authorizeHandler) authorizeWithBrowser( w http.ResponseWriter, oauthHelper fosite.OAuth2Provider, authorizeRequester fosite.AuthorizeRequester, - oidcUpstream *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, - ldapUpstream *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, + upstreamProvider resolvedprovider.FederationDomainResolvedIdentityProvider, ) error { - switch { - case oidcUpstream != nil: - return handleAuthRequestForOIDCUpstreamBrowserFlow(r, w, - oauthHelper, - authorizeRequester, - h.generateCSRF, h.generateNonce, h.generatePKCE, - oidcUpstream.DisplayName, - oidcUpstream.Provider, - h.downstreamIssuer, - h.upstreamStateEncoder, - h.cookieCodec, - ) - case ldapUpstream != nil: - return handleAuthRequestForLDAPUpstreamBrowserFlow(r, w, - oauthHelper, - authorizeRequester, - h.generateCSRF, h.generateNonce, h.generatePKCE, - ldapUpstream.DisplayName, - h.downstreamIssuer, - h.upstreamStateEncoder, - h.cookieCodec, - ldapUpstream.SessionProviderType, - ) - default: - // It should not actually be possible to reach this case. - return fosite.ErrServerError.WithHint("Huh? Unknown upstream IDP type.") + authRequestState, err := generateUpstreamAuthorizeRequestState(r, w, + authorizeRequester, + oauthHelper, + h.generateCSRF, + h.generateNonce, + h.generatePKCE, + upstreamProvider.GetDisplayName(), + upstreamProvider.GetSessionProviderType(), + h.cookieCodec, + h.upstreamStateEncoder, + ) + if err != nil { + return err } + + redirectURL, err := upstreamProvider.UpstreamAuthorizeRedirectURL(authRequestState, h.downstreamIssuerURL) + if err != nil { + return err + } + + http.Redirect(w, r, redirectURL, + http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 + ) + + return nil } func shouldShowIDPChooser( @@ -306,193 +266,6 @@ func shouldShowIDPChooser( !inBackwardsCompatMode && federationDomainSpecHasSomeValidIDPs } -func handleAuthRequestForLDAPUpstreamCLIFlow( - ctx context.Context, - ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, - identityTransforms *idtransform.TransformationPipeline, - idpDisplayName string, - submittedUsername string, - submittedPassword string, - idpType psession.ProviderType, - skipGroups bool, -) (*downstreamsession.Identity, error) { - authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(ctx, submittedUsername, submittedPassword, skipGroups) - if err != nil { - plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName()) - return nil, errUnexpectedUpstreamError.WithWrap(err) - } - if !authenticated { - return nil, fosite.ErrAccessDenied.WithHint("Username/password not accepted by LDAP provider.") - } - - subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP(ldapUpstream, authenticateResponse, idpDisplayName) - upstreamUsername := authenticateResponse.User.GetName() - upstreamGroups := authenticateResponse.User.GetGroups() - - username, groups, err := downstreamsession.ApplyIdentityTransformations(ctx, identityTransforms, upstreamUsername, upstreamGroups) - if err != nil { - return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) - } - - customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream, idpType, authenticateResponse, username, upstreamUsername, upstreamGroups) - - return &downstreamsession.Identity{ - SessionData: customSessionData, - Groups: groups, - Subject: subject, - }, nil -} - -func handleAuthRequestForLDAPUpstreamBrowserFlow( - r *http.Request, - w http.ResponseWriter, - oauthHelper fosite.OAuth2Provider, - authorizeRequester fosite.AuthorizeRequester, - generateCSRF func() (csrftoken.CSRFToken, error), - generateNonce func() (nonce.Nonce, error), - generatePKCE func() (pkce.Code, error), - idpDisplayName string, - downstreamIssuer string, - upstreamStateEncoder oidc.Encoder, - cookieCodec oidc.Codec, - idpType psession.ProviderType, -) error { - authRequestState, err := handleBrowserFlowAuthRequest(r, w, - authorizeRequester, - oauthHelper, - generateCSRF, - generateNonce, - generatePKCE, - idpDisplayName, - idpType, - cookieCodec, - upstreamStateEncoder, - ) - if err != nil { - return err - } - - err = login.RedirectToLoginPage(r, w, downstreamIssuer, authRequestState.encodedStateParam, login.ShowNoError) - if err != nil { - return fosite.ErrServerError.WithHint("Server could not formulate login UI URL for redirect.").WithWrap(err) - } - - return nil -} - -func handleAuthRequestForOIDCUpstreamPasswordGrant( - ctx context.Context, - oidcUpstream upstreamprovider.UpstreamOIDCIdentityProviderI, - identityTransforms *idtransform.TransformationPipeline, - idpDisplayName string, - submittedUsername string, - submittedPassword string, -) (*downstreamsession.Identity, error) { - if !oidcUpstream.AllowsPasswordGrant() { - // Return a user-friendly error for this case which is entirely within our control. - return nil, fosite.ErrAccessDenied.WithHint( - "Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.") - } - - token, err := oidcUpstream.PasswordCredentialsGrantAndValidateTokens(ctx, submittedUsername, submittedPassword) - if err != nil { - // Upstream password grant errors can be generic errors (e.g. a network failure) or can be oauth2.RetrieveError errors - // which represent the http response from the upstream server. These could be a 5XX or some other unexpected error, - // or could be a 400 with a JSON body as described by https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 - // which notes that wrong resource owner credentials should result in an "invalid_grant" error. - // However, the exact response is undefined in the sense that there is no such thing as a password grant in - // the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the - // spec and returns something other than an "invalid_grant" error for bad resource owner credentials.) - return nil, fosite.ErrAccessDenied.WithDebug(err.Error()) // WithDebug hides the error from the client - } - - subject, upstreamUsername, upstreamGroups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken( - oidcUpstream, token.IDToken.Claims, idpDisplayName, - ) - if err != nil { - // Return a user-friendly error for this case which is entirely within our control. - return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) - } - - username, groups, err := downstreamsession.ApplyIdentityTransformations(ctx, identityTransforms, upstreamUsername, upstreamGroups) - if err != nil { - return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) - } - - additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(oidcUpstream, token.IDToken.Claims) - - customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData(oidcUpstream, token, username, upstreamUsername, upstreamGroups) - if err != nil { - return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) - } - - return &downstreamsession.Identity{ - SessionData: customSessionData, - Groups: groups, - Subject: subject, - AdditionalClaims: additionalClaims, - }, nil -} - -func handleAuthRequestForOIDCUpstreamBrowserFlow( - r *http.Request, - w http.ResponseWriter, - oauthHelper fosite.OAuth2Provider, - authorizeRequester fosite.AuthorizeRequester, - generateCSRF func() (csrftoken.CSRFToken, error), - generateNonce func() (nonce.Nonce, error), - generatePKCE func() (pkce.Code, error), - idpDisplayName string, - oidcUpstream upstreamprovider.UpstreamOIDCIdentityProviderI, - downstreamIssuer string, - upstreamStateEncoder oidc.Encoder, - cookieCodec oidc.Codec, -) error { - authRequestState, err := handleBrowserFlowAuthRequest(r, w, - authorizeRequester, - oauthHelper, - generateCSRF, - generateNonce, - generatePKCE, - idpDisplayName, - psession.ProviderTypeOIDC, - cookieCodec, - upstreamStateEncoder, - ) - if err != nil { - return err - } - - upstreamOAuthConfig := oauth2.Config{ - ClientID: oidcUpstream.GetClientID(), - Endpoint: oauth2.Endpoint{ - AuthURL: oidcUpstream.GetAuthorizationURL().String(), - }, - RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuer), - Scopes: oidcUpstream.GetScopes(), - } - - authCodeOptions := []oauth2.AuthCodeOption{ - authRequestState.nonce.Param(), - authRequestState.pkce.Challenge(), - authRequestState.pkce.Method(), - } - - for key, val := range oidcUpstream.GetAdditionalAuthcodeParams() { - authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val)) - } - - http.Redirect(w, r, - upstreamOAuthConfig.AuthCodeURL( - authRequestState.encodedStateParam, - authCodeOptions..., - ), - http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 - ) - - return nil -} - func requireStaticClientForUsernameAndPasswordHeaders(authorizeRequester fosite.AuthorizeRequester) error { if !(authorizeRequester.GetClient().GetID() == oidcapi.ClientIDPinnipedCLI) { return fosite.ErrAccessDenied.WithHint("This client is not allowed to submit username or password headers to this endpoint.") @@ -530,7 +303,10 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { // chooseUpstreamIDP selects either an OIDC, an LDAP, or an AD IDP, or returns an error. // Note that AD and LDAP IDPs both return the same interface type, but different ProviderTypes values. -func chooseUpstreamIDP(idpDisplayName string, idpLister federationdomainproviders.FederationDomainIdentityProvidersFinderI) (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error) { +func chooseUpstreamIDP(idpDisplayName string, idpLister federationdomainproviders.FederationDomainIdentityProvidersFinderI) ( + resolvedprovider.FederationDomainResolvedIdentityProvider, + error, +) { // When a request is made to the authorization endpoint which does not specify the IDP name, then it might // be an old dynamic client (OIDCClient). We need to make this work, but only in the backwards compatibility case // where there is exactly one IDP defined in the namespace and no IDPs listed on the FederationDomain. @@ -557,21 +333,15 @@ func maybeLogDeprecationWarningForMissingIDPParam(idpNameQueryParamValue string, ) } -type browserFlowAuthRequestState struct { - encodedStateParam string - pkce pkce.Code - nonce nonce.Nonce -} - -// handleBrowserFlowAuthRequest performs the shared validations and setup between browser based -// auth requests regardless of IDP type-- LDAP, Active Directory and OIDC. +// generateUpstreamAuthorizeRequestState performs the shared validations and setup between browser based +// auth requests regardless of IDP type. // It generates the state param, sets the CSRF cookie, and validates the prompt param. // It returns an error when it encounters an error without handling it, leaving it to // the caller to decide how to handle it. // It returns nil with no error when it encounters an error and also has already handled writing // the error response to the ResponseWriter, in which case the caller should not also try to // write the error response. -func handleBrowserFlowAuthRequest( +func generateUpstreamAuthorizeRequestState( r *http.Request, w http.ResponseWriter, authorizeRequester fosite.AuthorizeRequester, @@ -583,7 +353,7 @@ func handleBrowserFlowAuthRequest( idpType psession.ProviderType, cookieCodec oidc.Codec, upstreamStateEncoder oidc.Encoder, -) (*browserFlowAuthRequestState, error) { +) (*resolvedprovider.UpstreamAuthorizeRequestState, error) { now := time.Now() _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &psession.PinnipedSession{ Fosite: &openid.DefaultSession{ @@ -637,10 +407,10 @@ func handleBrowserFlowAuthRequest( } } - return &browserFlowAuthRequestState{ - encodedStateParam: encodedStateParamValue, - pkce: pkceValue, - nonce: nonceValue, + return &resolvedprovider.UpstreamAuthorizeRequestState{ + EncodedStateParam: encodedStateParamValue, + PKCE: pkceValue, + Nonce: nonceValue, }, nil } diff --git a/internal/federationdomain/endpoints/auth/auth_handler_test.go b/internal/federationdomain/endpoints/auth/auth_handler_test.go index d08558185..21171a639 100644 --- a/internal/federationdomain/endpoints/auth/auth_handler_test.go +++ b/internal/federationdomain/endpoints/auth/auth_handler_test.go @@ -39,6 +39,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" "go.pinniped.dev/internal/testutil/transformtestutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/pkce" @@ -239,7 +240,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo fositeUpstreamAuthErrorQuery = map[string]string{ "error": "error", - "error_description": "Unexpected error during upstream authentication.", + "error_description": "Unexpected error during upstream LDAP authentication.", "state": happyState, } @@ -631,7 +632,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo type testCase struct { name string - idps *oidctestutil.UpstreamIDPListerBuilder + idps *testidplister.UpstreamIDPListerBuilder kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) generateCSRF func() (csrftoken.CSRFToken, error) generatePKCE func() (pkce.Code, error) @@ -677,7 +678,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo tests := []testCase{ { name: "OIDC upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -694,7 +695,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -712,7 +713,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -729,7 +730,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path using GET without a CSRF cookie using backwards compatibility mode to have a default IDP (display name does not need to be sent as query param)", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). WithDefaultIDPDisplayName(oidcUpstreamName), // specify which IDP is the backwards-compatibility mode IDP generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -747,7 +748,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "with multiple IDPs available, request does not choose which IDP to use", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, @@ -766,7 +767,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "with multiple IDPs available, request chooses to use OIDC browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, @@ -785,7 +786,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "with multiple IDPs available, request chooses to use LDAP browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()). WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, @@ -804,7 +805,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -822,7 +823,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -839,7 +840,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "Active Directory upstream browser flow happy path using GET without a CSRF cookie using a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -857,7 +858,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -879,7 +880,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant happy path using GET with identity transformations which change username and groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, @@ -907,7 +908,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant with identity transformations which rejects auth", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, @@ -921,7 +922,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant happy path using GET with additional claim mappings", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithAdditionalClaimMappings(map[string]string{ "downstreamCustomClaim": "upstreamCustomClaim", "downstreamOtherClaim": "upstreamOtherClaim", @@ -955,7 +956,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant happy path using GET with additional claim mappings, when upstream claims are not available", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithAdditionalClaimMappings(map[string]string{ "downstream": "upstream", }). @@ -983,7 +984,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP cli upstream happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1004,7 +1005,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP cli upstream happy path using GET with identity transformations which change username and groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(upstreamLDAPIdentityProviderBuilder().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, @@ -1031,7 +1032,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP cli upstream with identity transformations which reject auth", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(upstreamLDAPIdentityProviderBuilder().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, @@ -1044,7 +1045,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "ActiveDirectory cli upstream happy path using GET", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1065,7 +1066,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1082,7 +1083,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP upstream browser flow happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1099,7 +1100,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "Active Directory upstream browser flow happy path using GET with a CSRF cookie", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1116,7 +1117,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1135,7 +1136,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path using POST with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -1155,7 +1156,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1174,7 +1175,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP upstream browser flow happy path using POST with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -1194,7 +1195,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "Active Directory upstream browser flow happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1213,7 +1214,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "Active Directory upstream browser flow happy path using POST with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -1233,7 +1234,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -1257,7 +1258,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP cli upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -1280,7 +1281,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "Active Directory cli upstream happy path using POST", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -1303,7 +1304,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path with prompt param other than none that gets ignored", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1320,7 +1321,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path with custom IDP name and type query params, which are excluded from the query params in the upstream state", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1338,7 +1339,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path with extra params that get passed through", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123", "def": "456"}).Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().WithAdditionalAuthcodeParams(map[string]string{"prompt": "consent", "abc": "123", "def": "456"}).Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1355,7 +1356,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow with prompt param none throws an error because we want to independently decide the upstream prompt param", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1370,7 +1371,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow with error while decoding CSRF cookie just generates a new cookie and succeeds as usual", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1389,7 +1390,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1410,7 +1411,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path using dynamic client when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -1436,7 +1437,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant happy path when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client @@ -1460,7 +1461,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client @@ -1483,7 +1484,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream browser flow happy path when downstream requested scopes include offline_access", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1502,7 +1503,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC password grant happy path when upstream IDP returned empty refresh token but it did return an access token and has a userinfo endpoint", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1524,7 +1525,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC password grant happy path when upstream IDP returned empty refresh token and an access token that has a short lifetime", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1559,7 +1560,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC password grant happy path when upstream IDP did not return a refresh token but it did return an access token and has a userinfo endpoint", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1581,7 +1582,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "error during upstream LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(erroringUpstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(erroringUpstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1593,7 +1594,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "error during upstream Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1605,7 +1606,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "wrong upstream credentials for OIDC password grant authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). // This is similar to the error that would be returned by the underlying call to oauth2.PasswordCredentialsToken() WithPasswordGrantError(&oauth2.RetrieveError{Response: &http.Response{Status: "fake status"}, Body: []byte("fake body")}). @@ -1628,7 +1629,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "wrong upstream password for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1640,7 +1641,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "wrong upstream password for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1652,7 +1653,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "wrong upstream username for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To("wrong-username"), @@ -1664,7 +1665,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "wrong upstream username for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To("wrong-username"), @@ -1676,7 +1677,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing upstream username but has password on request for OIDC password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: nil, // do not send header @@ -1688,7 +1689,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing upstream username but has password on request for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: nil, // do not send header @@ -1700,7 +1701,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing upstream username on request for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: nil, // do not send header @@ -1712,7 +1713,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing upstream password on request for LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1724,7 +1725,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing upstream password on request for Active Directory authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForADUpstream, customUsernameHeader: ptr.To(happyLDAPUsername), @@ -1736,7 +1737,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "password grant returns an error when upstream IDP returns no refresh token with an access token but has no userinfo endpoint", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1749,7 +1750,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "password grant returns an error when upstream IDP returns empty refresh token with an access token but has no userinfo endpoint", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1762,7 +1763,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "password grant returns an error when upstream IDP returns empty refresh token and empty access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithEmptyAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1775,7 +1776,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "password grant returns an error when upstream IDP returns no refresh and no access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithoutAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1788,7 +1789,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "password grant returns an error when upstream IDP returns no refresh token and empty access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithEmptyAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1801,7 +1802,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "password grant returns an error when upstream IDP returns empty refresh token and no access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithoutAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().WithEmptyRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1814,7 +1815,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing upstream password on request for OIDC password grant authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCPasswordGrantUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1826,7 +1827,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "using the custom username header on request for OIDC password grant authentication when OIDCIdentityProvider does not allow password grants", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForOIDCUpstream, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1838,7 +1839,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "dynamic clients are not allowed to use OIDC password grant because we don't want them to handle user credentials", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), @@ -1851,7 +1852,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "dynamic clients are not allowed to use LDAP CLI-flow authentication because we don't want them to handle user credentials", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), @@ -1864,7 +1865,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "dynamic clients are not allowed to use Active Directory CLI-flow authentication because we don't want them to handle user credentials", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}), @@ -1877,7 +1878,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream redirect uri does not match what is configured for client when using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1893,7 +1894,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream redirect uri does not match what is configured for client when using OIDC upstream browser flow with a dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -1912,7 +1913,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream redirect uri does not match what is configured for client when using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", @@ -1925,7 +1926,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream redirect uri does not match what is configured for client when using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", @@ -1938,7 +1939,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream redirect uri does not match what is configured for client when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", @@ -1951,7 +1952,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream client does not exist when using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -1965,7 +1966,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream client does not exist when using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"client_id": "invalid-client"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -1976,7 +1977,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream client does not exist when using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, @@ -1985,7 +1986,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream client does not exist when using active directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": "invalid-client"}), wantStatus: http.StatusUnauthorized, @@ -1994,7 +1995,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2009,7 +2010,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using OIDC upstream browser flow with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2029,7 +2030,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2041,7 +2042,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using LDAP cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2053,7 +2054,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using LDAP browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, @@ -2063,7 +2064,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using LDAP browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{ @@ -2078,7 +2079,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using active directory cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": "unsupported"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2090,7 +2091,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using active directory browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": "unsupported"}), wantStatus: http.StatusSeeOther, @@ -2100,7 +2101,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response type is unsupported when using active directory browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{ @@ -2115,7 +2116,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2130,7 +2131,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream scopes do not match what is configured for client using OIDC upstream browser flow with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2146,7 +2147,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream scopes do not match what is configured for client using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"scope": "openid profile email tuna"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2158,7 +2159,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "form_post page is used to send errors to client using OIDC upstream browser flow with response_mode=form_post", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2172,7 +2173,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "response_mode form_post is not allowed for dynamic clients", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2187,7 +2188,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream scopes do not match what is configured for client using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"scope": "openid tuna"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2199,7 +2200,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream scopes do not match what is configured for client using Active Directory upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"scope": "openid tuna"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2211,7 +2212,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2226,7 +2227,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using OIDC upstream browser flow with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2242,7 +2243,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"response_type": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2254,7 +2255,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using LDAP cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2266,7 +2267,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using LDAP browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, @@ -2276,7 +2277,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using LDAP browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}), @@ -2287,7 +2288,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using Active Directory cli upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2299,7 +2300,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using Active Directory browser upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"response_type": ""}), wantStatus: http.StatusSeeOther, @@ -2309,7 +2310,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing response type in request using Active Directory browser upstream with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: modifiedHappyGetRequestPathForADUpstream(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}), @@ -2320,7 +2321,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing client id in request using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2334,7 +2335,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing client id in request using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"client_id": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2345,7 +2346,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing client id in request using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"client_id": ""}), wantStatus: http.StatusUnauthorized, @@ -2354,7 +2355,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2369,7 +2370,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge in request using OIDC upstream browser flow with dynamic client", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2385,7 +2386,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"code_challenge": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2399,7 +2400,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge": ""}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2412,7 +2413,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "invalid value for PKCE code_challenge_method in request using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2427,7 +2428,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "invalid value for PKCE code_challenge_method in request using OIDC upstream browser flow with dynamic client", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2443,7 +2444,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "invalid value for PKCE code_challenge_method in request using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2457,7 +2458,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "invalid value for PKCE code_challenge_method in request using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2470,7 +2471,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream browser flow", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2485,7 +2486,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream browser flow with dynamic client", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2501,7 +2502,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream password grant", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"code_challenge_method": "plain"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2515,7 +2516,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "when PKCE code_challenge_method in request is `plain` using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge_method": "plain"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2528,7 +2529,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge_method in request using OIDC upstream browser flow", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2543,7 +2544,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge_method in request using OIDC upstream browser flow with dynamic client", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2559,7 +2560,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge_method in request using OIDC upstream password grant", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"code_challenge_method": ""}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2573,7 +2574,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "missing PKCE code_challenge_method in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"code_challenge_method": ""}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2588,7 +2589,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library when using an OIDC upstream browser flow. name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2605,7 +2606,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library when using an OIDC upstream browser flow with a dynamic client. name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream browser flow with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2623,7 +2624,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library when using an OIDC upstream password grant. name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"prompt": "none login"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -2639,7 +2640,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library when using an LDAP upstream. name: "prompt param is not allowed to have none and another legal value at the same time using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"prompt": "none login"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -2652,7 +2653,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -2672,7 +2673,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream browser flow with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -2693,7 +2694,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, // The following prompt value is illegal when openid is requested, but note that openid is not requested. path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"prompt": "none login", "scope": "email"}), @@ -2716,7 +2717,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, // The following prompt value is illegal when openid is requested, but note that openid is not requested. path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"prompt": "none login", "scope": "email"}), @@ -2738,7 +2739,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutUsernameClaim().WithoutGroupsClaim().Build(), ), method: http.MethodGet, @@ -2767,7 +2768,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov").Build(), @@ -2798,7 +2799,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). @@ -2830,7 +2831,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithUsernameClaim("some-claim"). WithIDTokenClaim("some-claim", "joe"). @@ -2863,7 +2864,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). @@ -2881,7 +2882,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). @@ -2899,7 +2900,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP provides username claim configuration as `sub`, so the downstream token subject should be exactly what they asked for", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithUsernameClaim("sub").Build(), ), method: http.MethodGet, @@ -2928,7 +2929,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token has a non-array value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithIDTokenClaim(oidcUpstreamGroupsClaim, "notAnArrayGroup1 notAnArrayGroup2").Build(), ), @@ -2958,7 +2959,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream IDP's configured groups claim in the ID token is a slice of interfaces", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder(). WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"group1", "group2"}).Build(), ), @@ -2988,7 +2989,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token does not contain requested username claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim(oidcUpstreamUsernameClaim).Build(), ), method: http.MethodGet, @@ -3003,7 +3004,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token does not contain requested groups claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim(oidcUpstreamGroupsClaim).Build(), ), method: http.MethodGet, @@ -3032,7 +3033,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token contains username claim with weird format", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamUsernameClaim, 42).Build(), ), method: http.MethodGet, @@ -3047,7 +3048,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token contains username claim with empty string value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamUsernameClaim, "").Build(), ), method: http.MethodGet, @@ -3062,7 +3063,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token does not contain iss claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim("iss").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -3077,7 +3078,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token does has an empty string value for iss claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("iss", "").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -3092,7 +3093,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token has an non-string iss claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("iss", 42).WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -3107,7 +3108,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token does not contain sub claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithoutIDTokenClaim("sub").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -3122,7 +3123,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token does has an empty string value for sub claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("sub", "").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -3137,7 +3138,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token has an non-string sub claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim("sub", 42).WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -3152,7 +3153,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token contains groups claim with weird format", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamGroupsClaim, 42).Build(), ), method: http.MethodGet, @@ -3167,7 +3168,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token contains groups claim where one element is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"foo", 7}).Build(), ), method: http.MethodGet, @@ -3182,7 +3183,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "OIDC upstream password grant: upstream ID token contains groups claim with invalid null type", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( passwordGrantUpstreamOIDCIdentityProviderBuilder().WithIDTokenClaim(oidcUpstreamGroupsClaim, nil).Build(), ), method: http.MethodGet, @@ -3197,7 +3198,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream state does not have enough entropy using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -3212,7 +3213,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream state does not have enough entropy using OIDC upstream browser flow with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -3228,7 +3229,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream state does not have enough entropy using OIDC upstream password grant", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForOIDCPasswordGrantUpstream(map[string]string{"state": "short"}), customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -3240,7 +3241,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "downstream state does not have enough entropy using LDAP upstream", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProviderBuilder().Build()), method: http.MethodGet, path: modifiedHappyGetRequestPathForLDAPUpstream(map[string]string{"state": "short"}), customUsernameHeader: ptr.To(happyLDAPUsername), @@ -3252,7 +3253,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "error while encoding upstream state param using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -3267,7 +3268,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "error while encoding CSRF cookie value for new cookie using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -3282,7 +3283,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "error while generating CSRF token using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: sadCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -3297,7 +3298,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "error while generating nonce using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: sadNonceGenerator, @@ -3312,7 +3313,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "error while generating PKCE using OIDC upstream browser flow", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), generateCSRF: happyCSRFGenerator, generatePKCE: sadPKCEGenerator, generateNonce: happyNonceGenerator, @@ -3327,7 +3328,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "no default upstream provider is configured and no specific IDP was requested in the request params", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(), // empty + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(), // empty method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusBadRequest, @@ -3336,7 +3337,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "could not find requested IDP display name", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: happyGetRequestPathForLDAPUpstream, // includes param to request a different IDP display name than what is available wantStatus: http.StatusBadRequest, @@ -3345,7 +3346,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "with multiple IDPs, when using browserless flow, when pinniped_idp_name param is not specified, should be an error (browerless flows do not use IDP chooser page)", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().WithAllowPasswordGrant(true).Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().WithAllowPasswordGrant(true).Build()), method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: ptr.To(oidcUpstreamUsername), @@ -3356,7 +3357,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "post with invalid form in the body", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", contentType: formContentType, @@ -3367,7 +3368,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "post with invalid multipart form in the body", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPost, path: "/some/path", contentType: "multipart/form-data", @@ -3378,7 +3379,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "get with invalid query", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodGet, path: "/some/path?param=this-is-not-a-valid-query-due-to-the-semicolons;;;;", contentType: formContentType, @@ -3388,7 +3389,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "PUT is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPut, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -3397,7 +3398,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "PATCH is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodPatch, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -3406,7 +3407,7 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo }, { name: "DELETE is a bad method", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), method: http.MethodDelete, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -3545,8 +3546,15 @@ func TestAuthorizationEndpoint(t *testing.T) { //nolint:gocyclo oauthHelperWithNullStorage, _ := createOauthHelperWithNullStorage(secretsClient, oidcClientsClient) idps := test.idps.BuildFederationDomainIdentityProvidersListerFinder() + + oidcIDPsCount := 0 + for _, p := range idps.GetIdentityProviders() { + if p.GetSessionProviderType() == psession.ProviderTypeOIDC { + oidcIDPsCount++ + } + } if len(test.wantDownstreamAdditionalClaims) > 0 { - require.True(t, len(idps.GetOIDCIdentityProviders()) > 0, "wantDownstreamAdditionalClaims requires at least one OIDC IDP") + require.True(t, oidcIDPsCount > 0, "wantDownstreamAdditionalClaims requires at least one OIDC IDP") } subject := NewHandler( diff --git a/internal/federationdomain/endpoints/callback/callback_handler.go b/internal/federationdomain/endpoints/callback/callback_handler.go index 7f81a5be1..c0bd756cb 100644 --- a/internal/federationdomain/endpoints/callback/callback_handler.go +++ b/internal/federationdomain/endpoints/callback/callback_handler.go @@ -31,12 +31,11 @@ func NewHandler( return err } - resolvedOIDCIdentityProvider, _, err := upstreamIDPs.FindUpstreamIDPByDisplayName(state.UpstreamName) - if err != nil || resolvedOIDCIdentityProvider == nil { + oidcIdentityProvider, err := upstreamIDPs.FindUpstreamIDPByDisplayName(state.UpstreamName) + if err != nil || oidcIdentityProvider == nil { plog.Warning("upstream provider not found") return httperr.New(http.StatusUnprocessableEntity, "upstream provider not found") } - upstreamIDPConfig := resolvedOIDCIdentityProvider.Provider downstreamAuthParams, err := url.ParseQuery(state.AuthParams) if err != nil { @@ -58,48 +57,13 @@ func NewHandler( // an error if the client requested a scope that they are not allowed to request, so we don't need to worry about that here. downstreamsession.AutoApproveScopes(authorizeRequester) - token, err := upstreamIDPConfig.ExchangeAuthcodeAndValidateTokens( - r.Context(), - authcode(r), - state.PKCECode, - state.Nonce, - redirectURI, - ) + identity, err := oidcIdentityProvider.HandleCallback(r.Context(), authcode(r), state.PKCECode, state.Nonce, redirectURI) if err != nil { - plog.WarningErr("error exchanging and validating upstream tokens", err, "upstreamName", upstreamIDPConfig.GetName()) - return httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens") - } - - subject, upstreamUsername, upstreamGroups, err := downstreamsession.GetDownstreamIdentityFromUpstreamIDToken( - upstreamIDPConfig, token.IDToken.Claims, resolvedOIDCIdentityProvider.DisplayName, - ) - if err != nil { - return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) - } - - username, groups, err := downstreamsession.ApplyIdentityTransformations( - r.Context(), resolvedOIDCIdentityProvider.Transforms, upstreamUsername, upstreamGroups, - ) - if err != nil { - return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) - } - - additionalClaims := downstreamsession.MapAdditionalClaimsFromUpstreamIDToken(upstreamIDPConfig, token.IDToken.Claims) - - customSessionData, err := downstreamsession.MakeDownstreamOIDCCustomSessionData( - upstreamIDPConfig, token, username, upstreamUsername, upstreamGroups, - ) - if err != nil { - return httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) + return err } session := downstreamsession.MakeDownstreamSession( - &downstreamsession.Identity{ - SessionData: customSessionData, - Groups: groups, - Subject: subject, - AdditionalClaims: additionalClaims, - }, + identity, authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), ) @@ -107,7 +71,7 @@ func NewHandler( authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, session) if err != nil { plog.WarningErr("error while generating and saving authcode", err, - "upstreamName", upstreamIDPConfig.GetName(), "fositeErr", oidc.FositeErrorForLog(err)) + "identityProviderDisplayName", oidcIdentityProvider.GetDisplayName(), "fositeErr", oidc.FositeErrorForLog(err)) return httperr.Wrap(http.StatusInternalServerError, "error while generating and saving authcode", err) } diff --git a/internal/federationdomain/endpoints/callback/callback_handler_test.go b/internal/federationdomain/endpoints/callback/callback_handler_test.go index c7ff91e8e..d02eba516 100644 --- a/internal/federationdomain/endpoints/callback/callback_handler_test.go +++ b/internal/federationdomain/endpoints/callback/callback_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package callback @@ -28,6 +28,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" "go.pinniped.dev/internal/testutil/transformtestutil" "go.pinniped.dev/pkg/oidcclient/nonce" oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce" @@ -182,7 +183,7 @@ func TestCallbackEndpoint(t *testing.T) { tests := []struct { name string - idps *oidctestutil.UpstreamIDPListerBuilder + idps *testidplister.UpstreamIDPListerBuilder kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) method string path string @@ -209,7 +210,7 @@ func TestCallbackEndpoint(t *testing.T) { }{ { name: "GET with good state and cookie and successful upstream token exchange with response_mode=form_post returns 200 with HTML+JS form", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( happyUpstreamStateParam().WithAuthorizeRequestParams( @@ -240,7 +241,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "GET with good state and cookie with additional params", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream(). WithAdditionalClaimMappings(map[string]string{ "downstreamCustomClaim": "upstreamCustomClaim", "downstreamOtherClaim": "upstreamOtherClaim", @@ -283,7 +284,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -307,7 +308,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code when using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: newRequestPath().WithState(happyStateForDynamicClient).String(), @@ -332,7 +333,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "GET with authcode exchange that returns an access token but no refresh token when there is a userinfo endpoint returns 303 to downstream client callback with its state and code", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -356,7 +357,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "form_post happy path without username or groups scopes requested", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( happyUpstreamStateParam().WithAuthorizeRequestParams( @@ -391,7 +392,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "GET with authcode exchange that returns an access token but no refresh token but has a short token lifetime which is stored as a warning in the session", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(1*time.Hour))).WithUserInfoURL().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -428,7 +429,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithoutUsernameClaim().WithoutGroupsClaim().Build(), ), method: http.MethodGet, @@ -458,7 +459,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithUsernameClaim("email").WithIDTokenClaim("email", "joe@whitehouse.gov").Build(), ), method: http.MethodGet, @@ -488,7 +489,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). WithIDTokenClaim("email_verified", true).Build(), @@ -520,7 +521,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithUsernameClaim("some-claim"). WithIDTokenClaim("some-claim", "joe"). WithIDTokenClaim("email", "joe@whitehouse.gov"). @@ -553,7 +554,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithUsernameClaim("email"). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). WithIDTokenClaim("email_verified", "supposed to be boolean").Build(), ), @@ -570,7 +571,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "return an error when upstream IDP returned no refresh token with an access token when there is no userinfo endpoint", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithAccessToken(oidcUpstreamAccessToken, metav1.NewTime(time.Now().Add(9*time.Hour))).WithoutUserInfoURL().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -584,7 +585,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "return an error when upstream IDP returned no refresh token and no access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithoutAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -598,7 +599,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "return an error when upstream IDP returned an empty refresh token and empty access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithEmptyAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -612,7 +613,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "return an error when upstream IDP returned no refresh token and empty access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithEmptyAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithoutRefreshToken().WithEmptyAccessToken().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -626,7 +627,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "return an error when upstream IDP returned an empty refresh token and no access token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithoutAccessToken().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().WithEmptyRefreshToken().WithoutAccessToken().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -640,7 +641,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithUsernameClaim("email"). WithIDTokenClaim("email", "joe@whitehouse.gov"). WithIDTokenClaim("email_verified", false).Build(), @@ -658,7 +659,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP provides username claim configuration as `sub`, so the downstream token subject should be exactly what they asked for", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithUsernameClaim("sub").Build(), ), method: http.MethodGet, @@ -688,7 +689,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP's configured groups claim in the ID token has a non-array value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, "notAnArrayGroup1 notAnArrayGroup2").Build(), ), method: http.MethodGet, @@ -718,7 +719,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream IDP's configured groups claim in the ID token is a slice of interfaces", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"group1", "group2"}).Build(), ), method: http.MethodGet, @@ -748,7 +749,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is allowed to request username scope, but does not actually request username scope in authorize request, does not get username in ID token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: newRequestPath().WithState( @@ -778,7 +779,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is allowed to request groups scope, but does not actually request groups scope in authorize request, does not get groups in ID token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: newRequestPath().WithState( @@ -808,7 +809,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request username scope, and does not actually request username scope in authorize request, does not get username in ID token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -851,7 +852,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request groups scope, and does not actually request groups scope in authorize request, does not get groups in ID token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -894,7 +895,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using identity transformations which modify the username and group names", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(happyUpstream().WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), @@ -925,7 +926,7 @@ func TestCallbackEndpoint(t *testing.T) { // Pre-upstream-exchange verification { name: "PUT method is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodPut, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -934,7 +935,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "POST method is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodPost, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -943,7 +944,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "PATCH method is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodPatch, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -952,7 +953,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "DELETE method is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodDelete, path: newRequestPath().String(), wantStatus: http.StatusMethodNotAllowed, @@ -961,7 +962,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "code param was not included on request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).WithoutCode().String(), csrfCookie: happyCSRFCookie, @@ -971,7 +972,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state param was not included on request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithoutState().String(), csrfCookie: happyCSRFCookie, @@ -981,7 +982,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState("this-will-not-decode").String(), csrfCookie: happyCSRFCookie, @@ -993,7 +994,7 @@ func TestCallbackEndpoint(t *testing.T) { // This shouldn't happen in practice because the authorize endpoint should have already run the same // validations, but we would like to test the error handling in this endpoint anyway. name: "state param contains authorization request params which fail validation", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( happyUpstreamStateParam(). @@ -1013,7 +1014,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's internal version does not match what we want", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyUpstreamStateParam().WithStateVersion("wrong-state-version").Build(t, happyStateCodec)).String(), csrfCookie: happyCSRFCookie, @@ -1023,7 +1024,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params element is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyUpstreamStateParam(). WithAuthorizeRequestParams("the following is an invalid url encoding token, and therefore this is an invalid param: %z"). @@ -1035,7 +1036,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params are missing required value (e.g., client_id)", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( happyUpstreamStateParam(). @@ -1050,7 +1051,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params have invalid client_id", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState( happyUpstreamStateParam(). @@ -1065,7 +1066,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "dynamic clients do not allow response_mode=form_post", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, method: http.MethodGet, path: newRequestPath().WithState( @@ -1087,7 +1088,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request username scope in authorize request but requests it anyway", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -1116,7 +1117,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request groups scope in authorize request but requests it anyway", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -1145,7 +1146,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params does not contain openid scope", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath(). WithState( @@ -1174,7 +1175,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params does not contain openid, username, or groups scope", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath(). WithState( @@ -1204,7 +1205,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "state's downstream auth params also included offline_access scope", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath(). WithState( @@ -1233,7 +1234,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "the OIDCIdentityProvider resource has been deleted", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(otherUpstreamOIDCIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(otherUpstreamOIDCIdentityProvider), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, @@ -1243,7 +1244,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "the CSRF cookie does not exist on request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), wantStatus: http.StatusForbidden, @@ -1252,7 +1253,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", @@ -1262,7 +1263,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "cookie csrf value does not match state csrf value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), method: http.MethodGet, path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(), csrfCookie: happyCSRFCookie, @@ -1274,7 +1275,7 @@ func TestCallbackEndpoint(t *testing.T) { // Upstream exchange { name: "upstream auth code exchange fails", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithUpstreamAuthcodeExchangeError(errors.New("some error")).Build(), ), method: http.MethodGet, @@ -1290,7 +1291,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token does not contain requested username claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithoutIDTokenClaim(oidcUpstreamUsernameClaim).Build(), ), method: http.MethodGet, @@ -1306,7 +1307,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token does not contain requested groups claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithoutIDTokenClaim(oidcUpstreamGroupsClaim).Build(), ), method: http.MethodGet, @@ -1336,7 +1337,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token contains username claim with weird format", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim(oidcUpstreamUsernameClaim, 42).Build(), ), method: http.MethodGet, @@ -1352,7 +1353,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token contains username claim with empty string value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim(oidcUpstreamUsernameClaim, "").Build(), ), method: http.MethodGet, @@ -1368,7 +1369,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token does not contain iss claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithoutIDTokenClaim("iss").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -1384,7 +1385,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token does has an empty string value for iss claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim("iss", "").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -1400,7 +1401,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token has an non-string iss claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim("iss", 42).WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -1416,7 +1417,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token does not contain sub claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithoutIDTokenClaim("sub").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -1432,7 +1433,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token does has an empty string value for sub claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim("sub", "").WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -1448,7 +1449,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token has an non-string sub claim when using default username claim config", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim("sub", 42).WithoutUsernameClaim().Build(), ), method: http.MethodGet, @@ -1464,7 +1465,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token contains groups claim with weird format", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, 42).Build(), ), method: http.MethodGet, @@ -1480,7 +1481,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token contains groups claim where one element is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, []interface{}{"foo", 7}).Build(), ), method: http.MethodGet, @@ -1496,7 +1497,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "upstream ID token contains groups claim with invalid null type", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( happyUpstream().WithIDTokenClaim(oidcUpstreamGroupsClaim, nil).Build(), ), method: http.MethodGet, @@ -1512,7 +1513,7 @@ func TestCallbackEndpoint(t *testing.T) { }, { name: "using identity transformations which reject the authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(happyUpstream().WithTransformsForFederationDomain(rejectAuthPipeline).Build()), method: http.MethodGet, path: newRequestPath().WithState(happyState).String(), diff --git a/internal/federationdomain/endpoints/chooseidp/choose_idp_handler.go b/internal/federationdomain/endpoints/chooseidp/choose_idp_handler.go index ef1ae70a6..6b83f7601 100644 --- a/internal/federationdomain/endpoints/chooseidp/choose_idp_handler.go +++ b/internal/federationdomain/endpoints/chooseidp/choose_idp_handler.go @@ -1,4 +1,4 @@ -// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package chooseidp @@ -43,14 +43,8 @@ func NewHandler(authURL string, upstreamIDPs federationdomainproviders.Federatio } var idps []chooseidphtml.IdentityProvider - for _, p := range upstreamIDPs.GetOIDCIdentityProviders() { - idps = append(idps, newIDPForPageData(p.DisplayName)) - } - for _, p := range upstreamIDPs.GetLDAPIdentityProviders() { - idps = append(idps, newIDPForPageData(p.DisplayName)) - } - for _, p := range upstreamIDPs.GetActiveDirectoryIdentityProviders() { - idps = append(idps, newIDPForPageData(p.DisplayName)) + for _, p := range upstreamIDPs.GetIdentityProviders() { + idps = append(idps, newIDPForPageData(p.GetDisplayName())) } sort.SliceStable(idps, func(i, j int) bool { diff --git a/internal/federationdomain/endpoints/chooseidp/choose_idp_handler_test.go b/internal/federationdomain/endpoints/chooseidp/choose_idp_handler_test.go index d759b1c4e..9ef356afa 100644 --- a/internal/federationdomain/endpoints/chooseidp/choose_idp_handler_test.go +++ b/internal/federationdomain/endpoints/chooseidp/choose_idp_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package chooseidp @@ -16,6 +16,7 @@ import ( "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" ) func TestChooseIDPHandler(t *testing.T) { @@ -44,7 +45,7 @@ func TestChooseIDPHandler(t *testing.T) { name: "happy path", method: http.MethodGet, reqTarget: "/some/path" + oidc.ChooseIDPEndpointPath + "?" + testReqQuery.Encode(), - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("oidc2").Build()). WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("ldap1").Build()). WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("z-ad1").Build()). @@ -68,7 +69,7 @@ func TestChooseIDPHandler(t *testing.T) { name: "happy path when there are special characters in the IDP name", method: http.MethodGet, reqTarget: "/some/path" + oidc.ChooseIDPEndpointPath + "?" + testReqQuery.Encode(), - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName(`This is Ryan's IDP 👍\~!@#$%^&*()-+[]{}\|;'"<>,.?`).Build()). WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName(`This is Josh's IDP 🦭`).Build()). BuildFederationDomainIdentityProvidersListerFinder(), @@ -90,7 +91,7 @@ func TestChooseIDPHandler(t *testing.T) { name: "no valid IDPs are configured on the FederationDomain", method: http.MethodGet, reqTarget: "/some/path" + oidc.ChooseIDPEndpointPath + "?" + testReqQuery.Encode(), - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). BuildFederationDomainIdentityProvidersListerFinder(), wantStatus: http.StatusInternalServerError, wantContentType: "text/plain; charset=utf-8", @@ -100,7 +101,7 @@ func TestChooseIDPHandler(t *testing.T) { name: "no query params on the request", method: http.MethodGet, reqTarget: "/some/path" + oidc.ChooseIDPEndpointPath, - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("x-some-idp").Build()). BuildFederationDomainIdentityProvidersListerFinder(), wantStatus: http.StatusBadRequest, @@ -111,7 +112,7 @@ func TestChooseIDPHandler(t *testing.T) { name: "missing required query param(s) on the request", method: http.MethodGet, reqTarget: "/some/path" + oidc.ChooseIDPEndpointPath + "?client_id=foo", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("x-some-idp").Build()). BuildFederationDomainIdentityProvidersListerFinder(), wantStatus: http.StatusBadRequest, @@ -122,7 +123,7 @@ func TestChooseIDPHandler(t *testing.T) { name: "bad request method", method: http.MethodPost, reqTarget: oidc.ChooseIDPEndpointPath, - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("x-some-idp").Build()). BuildFederationDomainIdentityProvidersListerFinder(), wantStatus: http.StatusMethodNotAllowed, diff --git a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go index 5efb9a577..c63fd8383 100644 --- a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go +++ b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package idpdiscovery provides a handler for the upstream IDP discovery endpoint. @@ -40,29 +40,11 @@ func responseAsJSON(upstreamIDPs federationdomainproviders.FederationDomainIdent r := v1alpha1.IDPDiscoveryResponse{PinnipedIDPs: []v1alpha1.PinnipedIDP{}} // The cache of IDPs could change at any time, so always recalculate the list. - for _, federationDomainIdentityProvider := range upstreamIDPs.GetLDAPIdentityProviders() { + for _, federationDomainIdentityProvider := range upstreamIDPs.GetIdentityProviders() { r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ - Name: federationDomainIdentityProvider.DisplayName, - Type: v1alpha1.IDPTypeLDAP, - Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode}, - }) - } - for _, federationDomainIdentityProvider := range upstreamIDPs.GetActiveDirectoryIdentityProviders() { - r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ - Name: federationDomainIdentityProvider.DisplayName, - Type: v1alpha1.IDPTypeActiveDirectory, - Flows: []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode}, - }) - } - for _, federationDomainIdentityProvider := range upstreamIDPs.GetOIDCIdentityProviders() { - flows := []v1alpha1.IDPFlow{v1alpha1.IDPFlowBrowserAuthcode} - if federationDomainIdentityProvider.Provider.AllowsPasswordGrant() { - flows = append(flows, v1alpha1.IDPFlowCLIPassword) - } - r.PinnipedIDPs = append(r.PinnipedIDPs, v1alpha1.PinnipedIDP{ - Name: federationDomainIdentityProvider.DisplayName, - Type: v1alpha1.IDPTypeOIDC, - Flows: flows, + Name: federationDomainIdentityProvider.GetDisplayName(), + Type: federationDomainIdentityProvider.GetIDPDiscoveryType(), + Flows: federationDomainIdentityProvider.GetIDPDiscoveryFlows(), }) } diff --git a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go index 5257c2543..892a1dc84 100644 --- a/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/federationdomain/endpoints/idpdiscovery/idp_discovery_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package idpdiscovery @@ -13,6 +13,7 @@ import ( "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" ) func TestIDPDiscovery(t *testing.T) { @@ -38,8 +39,8 @@ func TestIDPDiscovery(t *testing.T) { "pinniped_identity_providers": [ {"name": "a-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, {"name": "a-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode"]}, - {"name": "x-some-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, - {"name": "x-some-idp", "type": "oidc", "flows": ["browser_authcode"]}, + {"name": "x-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, + {"name": "x-some-oidc-idp", "type": "oidc", "flows": ["browser_authcode"]}, {"name": "y-some-ad-idp", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, {"name": "z-some-ad-idp", "type": "activedirectory", "flows": ["cli_password", "browser_authcode"]}, {"name": "z-some-ldap-idp", "type": "ldap", "flows": ["cli_password", "browser_authcode"]}, @@ -69,13 +70,13 @@ func TestIDPDiscovery(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - idpLister := oidctestutil.NewUpstreamIDPListerBuilder(). + idpLister := testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("z-some-oidc-idp").WithAllowPasswordGrant(true).Build()). - WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("x-some-idp").Build()). + WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("x-some-oidc-idp").Build()). WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("a-some-ldap-idp").Build()). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().WithName("a-some-oidc-idp").Build()). WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("z-some-ldap-idp").Build()). - WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("x-some-idp").Build()). + WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("x-some-ldap-idp").Build()). WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("z-some-ad-idp").Build()). WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder().WithName("y-some-ad-idp").Build()). BuildFederationDomainIdentityProvidersListerFinder() diff --git a/internal/federationdomain/endpoints/login/get_login_handler.go b/internal/federationdomain/endpoints/login/get_login_handler.go index 567e6a9e3..8b5beb2ff 100644 --- a/internal/federationdomain/endpoints/login/get_login_handler.go +++ b/internal/federationdomain/endpoints/login/get_login_handler.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login @@ -7,6 +7,7 @@ import ( "net/http" "go.pinniped.dev/internal/federationdomain/endpoints/login/loginhtml" + "go.pinniped.dev/internal/federationdomain/endpoints/loginurl" "go.pinniped.dev/internal/federationdomain/oidc" ) @@ -31,10 +32,10 @@ func NewGetHandler(loginPath string) HandlerFunc { } func getAlert(r *http.Request) (string, bool) { - errorParamValue := r.URL.Query().Get(errParamName) + errorParamValue := r.URL.Query().Get(loginurl.ErrParamName) message := internalErrorMessage - if errorParamValue == string(ShowBadUserPassErr) { + if errorParamValue == string(loginurl.ShowBadUserPassErr) { message = incorrectUsernameOrPasswordErrorMessage } diff --git a/internal/federationdomain/endpoints/login/login_handler.go b/internal/federationdomain/endpoints/login/login_handler.go index f7892c70e..2b2bf4491 100644 --- a/internal/federationdomain/endpoints/login/login_handler.go +++ b/internal/federationdomain/endpoints/login/login_handler.go @@ -1,11 +1,10 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login import ( "net/http" - "net/url" idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" "go.pinniped.dev/internal/federationdomain/endpoints/login/loginhtml" @@ -16,19 +15,6 @@ import ( "go.pinniped.dev/internal/plog" ) -type ErrorParamValue string - -const ( - usernameParamName = "username" - passwordParamName = "password" - stateParamName = "state" - errParamName = "err" - - ShowNoError ErrorParamValue = "" - ShowInternalError ErrorParamValue = "internal_error" - ShowBadUserPassErr ErrorParamValue = "login_error" -) - // HandlerFunc is a function that can handle either a GET or POST request for the login endpoint. type HandlerFunc func( w http.ResponseWriter, @@ -93,33 +79,3 @@ func wrapSecurityHeaders(handler http.Handler) http.Handler { wrapped.ServeHTTP(w, r) }) } - -// RedirectToLoginPage redirects to the GET /login page of the specified issuer. -// The specified issuer should never end with a "/", which is validated by -// provider.FederationDomainIssuer when the issuer string comes from that type. -func RedirectToLoginPage( - r *http.Request, - w http.ResponseWriter, - downstreamIssuer string, - encodedStateParamValue string, - errToDisplay ErrorParamValue, -) error { - loginURL, err := url.Parse(downstreamIssuer + oidc.PinnipedLoginPath) - if err != nil { - return err - } - - q := loginURL.Query() - q.Set(stateParamName, encodedStateParamValue) - if errToDisplay != ShowNoError { - q.Set(errParamName, string(errToDisplay)) - } - loginURL.RawQuery = q.Encode() - - http.Redirect(w, r, - loginURL.String(), - http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 - ) - - return nil -} diff --git a/internal/federationdomain/endpoints/login/post_login_handler.go b/internal/federationdomain/endpoints/login/post_login_handler.go index 4d55b2231..d08d52131 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler.go +++ b/internal/federationdomain/endpoints/login/post_login_handler.go @@ -4,6 +4,7 @@ package login import ( + "errors" "net/http" "net/url" @@ -12,8 +13,10 @@ import ( oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" "go.pinniped.dev/internal/federationdomain/downstreamsession" + "go.pinniped.dev/internal/federationdomain/endpoints/loginurl" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/federationdomain/oidc" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/plog" ) @@ -21,7 +24,7 @@ import ( func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.FederationDomainIdentityProvidersFinderI, oauthHelper fosite.OAuth2Provider) HandlerFunc { return func(w http.ResponseWriter, r *http.Request, encodedState string, decodedState *oidc.UpstreamStateParamData) error { // Note that the login handler prevents this handler from being called with OIDC upstreams. - _, ldapUpstream, err := upstreamIDPs.FindUpstreamIDPByDisplayName(decodedState.UpstreamName) + ldapUpstream, err := upstreamIDPs.FindUpstreamIDPByDisplayName(decodedState.UpstreamName) if err != nil { // This shouldn't normally happen because the authorization endpoint ensured that this provider existed // at that time. It would be possible in the unlikely event that the provider was deleted during the login. @@ -54,63 +57,39 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed downstreamsession.AutoApproveScopes(authorizeRequester) // Get the username and password form params from the POST body. - submittedUsername := r.PostFormValue(usernameParamName) - submittedPassword := r.PostFormValue(passwordParamName) + submittedUsername := r.PostFormValue(loginurl.UsernameParamName) + submittedPassword := r.PostFormValue(loginurl.PasswordParamName) // Treat blank username or password as a bad username/password combination, as opposed to an internal error. if submittedUsername == "" || submittedPassword == "" { // User forgot to enter one of the required fields. // The user may try to log in again if they'd like, so redirect back to the login page with an error. - return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr) + return redirectToLoginPage(r, w, issuerURL, encodedState, loginurl.ShowBadUserPassErr) } + skipGroups := !slices.Contains(authorizeRequester.GetGrantedScopes(), oidcapi.ScopeGroups) + // Attempt to authenticate the user with the upstream IDP. - authenticateResponse, authenticated, err := ldapUpstream.Provider.AuthenticateUser(r.Context(), - submittedUsername, - submittedPassword, - !slices.Contains(authorizeRequester.GetGrantedScopes(), oidcapi.ScopeGroups), - ) + identity, err := ldapUpstream.Login(r.Context(), submittedUsername, submittedPassword, skipGroups) if err != nil { - plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.Provider.GetName()) - // There was some problem during authentication with the upstream, aside from bad username/password. - // The user may try to log in again if they'd like, so redirect back to the login page with an error. - return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowInternalError) + switch { + case errors.Is(err, resolvedldap.ErrUnexpectedUpstreamLDAPError): + // There was some problem during authentication with the upstream, aside from bad username/password. + // The user may try to log in again if they'd like, so redirect back to the login page with an error. + return redirectToLoginPage(r, w, issuerURL, encodedState, loginurl.ShowInternalError) + case err == resolvedldap.ErrAccessDeniedDueToUsernamePasswordNotAccepted: + // The upstream did not accept the username/password combination. + // The user may try to log in again if they'd like, so redirect back to the login page with an error. + return redirectToLoginPage(r, w, issuerURL, encodedState, loginurl.ShowBadUserPassErr) + default: + // Some other error happened, e.g. a configured identity transformation failed. + oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, err, false) + return nil + } } - if !authenticated { - // The upstream did not accept the username/password combination. - // The user may try to log in again if they'd like, so redirect back to the login page with an error. - return RedirectToLoginPage(r, w, issuerURL, encodedState, ShowBadUserPassErr) - } - - // We had previously interrupted the regular steps of the OIDC authcode flow to show the login page UI. - // Now the upstream IDP has authenticated the user, so now we're back into the regular OIDC authcode flow steps. - // Both success and error responses from this point onwards should look like the usual fosite redirect - // responses, and a happy redirect response will include a downstream authcode. - subject := downstreamsession.DownstreamSubjectFromUpstreamLDAP( - ldapUpstream.Provider, authenticateResponse, ldapUpstream.DisplayName, - ) - upstreamUsername := authenticateResponse.User.GetName() - upstreamGroups := authenticateResponse.User.GetGroups() - - username, groups, err := downstreamsession.ApplyIdentityTransformations( - r.Context(), ldapUpstream.Transforms, upstreamUsername, upstreamGroups, - ) - if err != nil { - oidc.WriteAuthorizeError(r, w, oauthHelper, authorizeRequester, - fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()), false, - ) - return nil - } - - customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData( - ldapUpstream.Provider, ldapUpstream.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups) session := downstreamsession.MakeDownstreamSession( - &downstreamsession.Identity{ - SessionData: customSessionData, - Groups: groups, - Subject: subject, - }, + identity, authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), ) @@ -120,3 +99,24 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed return nil } } + +// redirectToLoginPage redirects to the GET /login page of the specified issuer. +func redirectToLoginPage( + r *http.Request, + w http.ResponseWriter, + downstreamIssuer string, + encodedStateParamValue string, + errToDisplay loginurl.ErrorParamValue, +) error { + loginURL, err := loginurl.URL(downstreamIssuer, encodedStateParamValue, errToDisplay) + if err != nil { + return err + } + + http.Redirect(w, r, + loginURL, + http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11 + ) + + return nil +} diff --git a/internal/federationdomain/endpoints/login/post_login_handler_test.go b/internal/federationdomain/endpoints/login/post_login_handler_test.go index 153cb9ded..cb894ca5c 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler_test.go +++ b/internal/federationdomain/endpoints/login/post_login_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package login @@ -27,6 +27,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" "go.pinniped.dev/internal/testutil/transformtestutil" ) @@ -289,7 +290,7 @@ func TestPostLoginEndpoint(t *testing.T) { tests := []struct { name string - idps *oidctestutil.UpstreamIDPListerBuilder + idps *testidplister.UpstreamIDPListerBuilder kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) decodedState *oidc.UpstreamStateParamData formParams url.Values @@ -329,7 +330,7 @@ func TestPostLoginEndpoint(t *testing.T) { }{ { name: "happy LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(upstreamLDAPIdentityProvider). // should pick this one WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, @@ -352,7 +353,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login with identity transformations which modify the username and group names", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(upstreamLDAPIdentityProviderBuilder.WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()). // should pick this one WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, @@ -380,7 +381,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(upstreamLDAPIdentityProvider). // should pick this one WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, @@ -404,7 +405,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy AD login", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(erroringUpstreamLDAPIdentityProvider). WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), // should pick this one decodedState: happyActiveDirectoryDecodedState, @@ -427,7 +428,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy AD login with identity transformations which modify the username and group names", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(erroringUpstreamLDAPIdentityProvider). WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder.WithTransformsForFederationDomain(prefixUsernameAndGroupsPipeline).Build()), // should pick this one decodedState: happyActiveDirectoryDecodedState, @@ -455,7 +456,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy AD login with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(erroringUpstreamLDAPIdentityProvider). WithActiveDirectory(upstreamActiveDirectoryIdentityProvider), // should pick this one kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, @@ -479,7 +480,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"response_mode": "form_post"}, @@ -504,7 +505,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"redirect_uri": "http://127.0.0.1:4242/callback"}, @@ -529,7 +530,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -555,7 +556,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access pinniped:request-audience"}, @@ -581,7 +582,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is allowed to request username and groups but does not request them", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -607,7 +608,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is not allowed to request username and does not request username", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -641,7 +642,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client, when dynamic client is not allowed to request groups and does not request groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -675,7 +676,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{ @@ -705,7 +706,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "happy LDAP login when username and groups scopes are not requested", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(upstreamLDAPIdentityProvider). // should pick this one WithActiveDirectory(erroringUpstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { @@ -733,7 +734,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "bad username LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{"wrong!"}, passParam: []string{happyLDAPPassword}}, wantStatus: http.StatusSeeOther, @@ -743,7 +744,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "bad password LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{"wrong!"}}, wantStatus: http.StatusSeeOther, @@ -753,7 +754,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "blank username LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{""}, passParam: []string{happyLDAPPassword}}, wantStatus: http.StatusSeeOther, @@ -763,7 +764,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "blank password LDAP login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: url.Values{userParam: []string{happyLDAPUsername}, passParam: []string{""}}, wantStatus: http.StatusSeeOther, @@ -773,7 +774,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "username and password sent as URI query params should be ignored since they are expected in form post body", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, reqURIQuery: happyUsernamePasswordFormParams, wantStatus: http.StatusSeeOther, @@ -783,7 +784,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "error during upstream LDAP authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(erroringUpstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(erroringUpstreamLDAPIdentityProvider), decodedState: happyLDAPDecodedState, formParams: happyUsernamePasswordFormParams, wantStatus: http.StatusSeeOther, @@ -793,7 +794,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"redirect_uri": "http://127.0.0.1/wrong_callback"}, @@ -804,7 +805,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -816,7 +817,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream client does not exist", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": "wrong_client_id"}, @@ -827,7 +828,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream client is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": ""}, @@ -838,7 +839,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "response type is unsupported", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"response_type": "unsupported"}, @@ -849,7 +850,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "response type form_post is unsupported for dynamic clients", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -861,7 +862,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "response type is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"response_type": ""}, @@ -872,7 +873,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge": ""}, @@ -887,7 +888,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is invalid", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}, @@ -902,7 +903,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is `plain`", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge_method": "plain"}, // plain is not allowed @@ -917,7 +918,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is missing", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"code_challenge_method": ""}, @@ -932,7 +933,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "PKCE code_challenge_method is missing with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -948,7 +949,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "prompt param is not allowed to have none and another legal value at the same time", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"prompt": "none login"}, @@ -963,7 +964,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "LDAP login using identity transformations which reject the authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(upstreamLDAPIdentityProviderBuilder.WithTransformsForFederationDomain(rejectAuthPipeline).Build()), decodedState: happyLDAPDecodedState, formParams: happyUsernamePasswordFormParams, @@ -974,7 +975,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "AD login using identity transformations which reject the authentication", - idps: oidctestutil.NewUpstreamIDPListerBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder(). WithActiveDirectory(upstreamActiveDirectoryIdentityProviderBuilder.WithTransformsForFederationDomain(rejectAuthPipeline).Build()), decodedState: happyActiveDirectoryDecodedState, formParams: happyUsernamePasswordFormParams, @@ -985,7 +986,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream state does not have enough entropy", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"state": "short"}, @@ -996,7 +997,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"scope": "openid offline_access pinniped:request-audience scope_not_allowed"}, @@ -1007,7 +1008,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request username scope in authorize request but requests it anyway", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -1027,7 +1028,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "using dynamic client which is not allowed to request groups scope in authorize request but requests it anyway", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) { oidcClient, secret := testutil.OIDCClientAndStorageSecret(t, "some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, @@ -1047,7 +1048,7 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client with dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources, decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient, @@ -1059,14 +1060,14 @@ func TestPostLoginEndpoint(t *testing.T) { }, { name: "no upstream providers are configured or provider cannot be found by name", - idps: oidctestutil.NewUpstreamIDPListerBuilder(), // empty + idps: testidplister.NewUpstreamIDPListerBuilder(), // empty decodedState: happyLDAPDecodedState, formParams: happyUsernamePasswordFormParams, wantErr: "error finding upstream provider: did not find IDP with name \"some-ldap-idp\"", }, { name: "upstream provider cannot be found by name and type", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(upstreamLDAPIdentityProvider), decodedState: happyActiveDirectoryDecodedState, // correct upstream IDP name, but wrong upstream IDP type formParams: happyUsernamePasswordFormParams, wantErr: "error finding upstream provider: did not find IDP with name \"some-active-directory-idp\"", diff --git a/internal/federationdomain/endpoints/loginurl/login_url.go b/internal/federationdomain/endpoints/loginurl/login_url.go new file mode 100644 index 000000000..f7d1b7911 --- /dev/null +++ b/internal/federationdomain/endpoints/loginurl/login_url.go @@ -0,0 +1,46 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package loginurl + +import ( + "net/url" + + "go.pinniped.dev/internal/federationdomain/oidc" +) + +const ( + UsernameParamName = "username" + PasswordParamName = "password" + StateParamName = "state" + ErrParamName = "err" + + ShowNoError ErrorParamValue = "" + ShowInternalError ErrorParamValue = "internal_error" + ShowBadUserPassErr ErrorParamValue = "login_error" +) + +type ErrorParamValue string + +// URL returns the URL for the GET /login page of the specified issuer. +// The specified issuer should never end with a "/", which is validated by +// provider.FederationDomainIssuer when the issuer string comes from that type. +func URL( + downstreamIssuer string, + encodedStateParamValue string, + errToDisplay ErrorParamValue, +) (string, error) { + loginURL, err := url.Parse(downstreamIssuer + oidc.PinnipedLoginPath) + if err != nil { + return "", err + } + + q := loginURL.Query() + q.Set(StateParamName, encodedStateParamValue) + if errToDisplay != ShowNoError { + q.Set(ErrParamName, string(errToDisplay)) + } + loginURL.RawQuery = q.Encode() + + return loginURL.String(), nil +} diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index a597ec838..418642e46 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -18,10 +18,10 @@ import ( "k8s.io/utils/strings/slices" oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc" - "go.pinniped.dev/internal/federationdomain/downstreamsession" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/federationdomain/oidc" - "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedoidc" "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/httputil/httperr" "go.pinniped.dev/internal/idtransform" @@ -202,7 +202,7 @@ func upstreamOIDCRefresh( // If the claim is found, then use it to update the user's group membership in the session. // If the claim is not found, then we have no new information about groups, so skip updating the group membership // and let any old groups memberships in the session remain. - refreshedUntransformedGroups, err = downstreamsession.GetGroupsFromUpstreamIDToken(p.Provider, mergedClaims) + refreshedUntransformedGroups, err = resolvedoidc.GetGroupsFromUpstreamIDToken(p.Provider, mergedClaims) if err != nil { return errUpstreamRefreshError().WithHintf( "Upstream refresh error while extracting groups claim.").WithTrace(err). @@ -324,14 +324,15 @@ func getString(m map[string]interface{}, key string) (string, bool) { func findOIDCProviderByNameAndValidateUID( s *psession.CustomSessionData, idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, -) (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, error) { - for _, p := range idpLister.GetOIDCIdentityProviders() { - if p.Provider.GetName() == s.ProviderName { - if p.Provider.GetResourceUID() != s.ProviderUID { +) (*resolvedoidc.FederationDomainResolvedOIDCIdentityProvider, error) { + for _, p := range idpLister.GetIdentityProviders() { + if p.GetSessionProviderType() == psession.ProviderTypeOIDC && p.GetProvider().GetName() == s.ProviderName { + if p.GetProvider().GetResourceUID() != s.ProviderUID { return nil, errorsx.WithStack(errUpstreamRefreshError().WithHint( "Provider from upstream session data has changed its resource UID since authentication.")) } - return p, nil + // TODO: make a new interface method for refreshing rather than downcasting here + return p.(*resolvedoidc.FederationDomainResolvedOIDCIdentityProvider), nil } } return nil, errorsx.WithStack(errUpstreamRefreshError(). @@ -457,25 +458,23 @@ func transformRefreshedIdentity( func findLDAPProviderByNameAndValidateUID( s *psession.CustomSessionData, idpLister federationdomainproviders.FederationDomainIdentityProvidersListerI, -) (*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, string, error) { - var providers []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider +) (*resolvedldap.FederationDomainResolvedLDAPIdentityProvider, string, error) { var dn string if s.ProviderType == psession.ProviderTypeLDAP { - providers = idpLister.GetLDAPIdentityProviders() dn = s.LDAP.UserDN } else if s.ProviderType == psession.ProviderTypeActiveDirectory { - providers = idpLister.GetActiveDirectoryIdentityProviders() dn = s.ActiveDirectory.UserDN } - for _, p := range providers { - if p.Provider.GetName() == s.ProviderName { - if p.Provider.GetResourceUID() != s.ProviderUID { + for _, p := range idpLister.GetIdentityProviders() { + if p.GetSessionProviderType() == s.ProviderType && p.GetProvider().GetName() == s.ProviderName { + if p.GetProvider().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 + // TODO: make a new interface method for refreshing rather than downcasting here + return p.(*resolvedldap.FederationDomainResolvedLDAPIdentityProvider), dn, nil } } diff --git a/internal/federationdomain/endpoints/token/token_handler_test.go b/internal/federationdomain/endpoints/token/token_handler_test.go index 2bd515762..a6ed50f49 100644 --- a/internal/federationdomain/endpoints/token/token_handler_test.go +++ b/internal/federationdomain/endpoints/token/token_handler_test.go @@ -61,6 +61,7 @@ import ( "go.pinniped.dev/internal/psession" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" "go.pinniped.dev/internal/testutil/transformtestutil" "go.pinniped.dev/pkg/oidcclient/oidctypes" ) @@ -884,7 +885,7 @@ func TestTokenEndpointAuthcodeExchange(t *testing.T) { // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. exchangeAuthcodeForTokens(t, - test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) + test.authcodeExchange, testidplister.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) }) } } @@ -919,7 +920,7 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) { // First call - should be successful. // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. subject, rsp, authCode, _, secrets, oauthStore := exchangeAuthcodeForTokens(t, - test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) + test.authcodeExchange, testidplister.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) var parsedResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody)) @@ -1574,7 +1575,7 @@ func TestTokenEndpointTokenExchange(t *testing.T) { // tests for grant_type "urn // Authcode exchange doesn't use the upstream provider cache, so just pass an empty cache. subject, rsp, _, _, secrets, storage := exchangeAuthcodeForTokens(t, - test.authcodeExchange, oidctestutil.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) + test.authcodeExchange, testidplister.NewUpstreamIDPListerBuilder().BuildFederationDomainIdentityProvidersListerFinder(), test.kubeResources) var parsedAuthcodeExchangeResponseBody map[string]interface{} require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedAuthcodeExchangeResponseBody)) @@ -2009,7 +2010,7 @@ func TestRefreshGrant(t *testing.T) { tests := []struct { name string - idps *oidctestutil.UpstreamIDPListerBuilder + idps *testidplister.UpstreamIDPListerBuilder kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) authcodeExchange authcodeExchangeInputs refreshRequest refreshRequestInputs @@ -2017,7 +2018,7 @@ func TestRefreshGrant(t *testing.T) { }{ { name: "happy path refresh grant with openid scope granted (id token returned)", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2035,7 +2036,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant with OIDC upstream with identity transformations which modify the username and group names", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2070,7 +2071,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant with OIDC upstream with identity transformations which modify the username and group names when the upstream refresh does not return new username or groups then it reruns the transformations on the old upstream username and groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{}, @@ -2109,7 +2110,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "refresh grant with OIDC upstream with identity transformations which modify the username and group names when the downstream username has changed compared to initial login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2149,7 +2150,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "refresh grant with OIDC upstream with identity transformations which reject the auth", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2189,7 +2190,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant with openid scope granted (id token returned) and additionalClaims", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2252,7 +2253,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2280,7 +2281,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant with openid scope granted (id token returned) using dynamic client with additional claims", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2349,7 +2350,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant with upstream username claim but without downstream username scope granted, using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2396,7 +2397,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "refresh grant with unchanged username claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2416,7 +2417,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "refresh grant when the customsessiondata has a stored access token and no stored refresh token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim"). WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ @@ -2460,7 +2461,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant without openid scope granted (no id token returned)", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{}, @@ -2497,7 +2498,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh does not return a new ID token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{}, @@ -2521,7 +2522,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships (as strings) from the merged ID token and userinfo results, it updates groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2552,7 +2553,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships (as strings) from the merged ID token and userinfo results, it updates groups, using dynamic client - updates groups without outputting warnings", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2590,7 +2591,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships (as interface{} types) from the merged ID token and userinfo results, it updates groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2621,7 +2622,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships as an empty list from the merged ID token and userinfo results, it updates groups to be empty", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2651,7 +2652,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh does not return new group memberships from the merged ID token and userinfo results by omitting claim, it keeps groups from initial login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2678,7 +2679,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships from LDAP, it updates groups", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -2706,7 +2707,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns new group memberships from LDAP, it updates groups, using dynamic client - updates groups without outputting warnings", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -2741,7 +2742,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh returns empty list of group memberships from LDAP, it updates groups to an empty list", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -2768,7 +2769,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "ldap refresh grant when the upstream refresh when username and groups scopes are not requested on original request or refresh", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -2812,7 +2813,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "oidc refresh grant when the upstream refresh when username and groups scopes are not requested on original request or refresh", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2859,7 +2860,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "oidc refresh grant when the upstream refresh when groups scope not requested on original request or refresh when using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2910,7 +2911,7 @@ func TestRefreshGrant(t *testing.T) { // fosite does not look at the scopes provided in refresh requests, although it is a valid parameter. // even if 'groups' is not sent in the refresh request, we will send groups all the same. name: "refresh grant when the upstream refresh when groups scope requested on original request but not refresh refresh", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -2954,7 +2955,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "error from refresh grant when the upstream refresh does not return new group memberships from the merged ID token and userinfo results by returning group claim with illegal nil value", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithGroupsClaim("my-groups-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2975,7 +2976,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "happy path refresh grant when the upstream refresh does not return a new refresh token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -2993,7 +2994,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the refresh request adds a new scope to the list of requested scopes then it is ignored", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3014,7 +3015,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the refresh request removes a scope which was originally granted from the list of requested scopes then it is granted anyway", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3058,7 +3059,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the refresh request does not include a scope param then it gets all the same scopes as the original authorization request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3079,7 +3080,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when a valid refresh token is sent in the refresh request, but the token has already expired", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, modifyRefreshTokenStorage: func(t *testing.T, oauthStore *storage.KubeStorage, secrets v1.SecretInterface, refreshToken string) { // The fosite storage APIs don't offer a way to update a refresh token, so we will instead find the underlying @@ -3128,7 +3129,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when a bad refresh token is sent in the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access username groups") }, @@ -3155,7 +3156,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the access token is sent as if it were a refresh token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access username groups") }, @@ -3182,7 +3183,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the wrong client ID is included in the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: initialUpstreamOIDCRefreshTokenCustomSessionData(), modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "offline_access username groups") }, @@ -3209,7 +3210,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the refresh request uses a different client than the one that was used to get the refresh token", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3230,7 +3231,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the client auth fails on the refresh request using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3261,7 +3262,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "dynamic client uses wrong auth method on the refresh request (must use basic auth)", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3292,7 +3293,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when there is no custom session data found in the session storage during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: nil, // this should not happen in practice modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, @@ -3307,7 +3308,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when there is no provider name in custom session data found in the session storage during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: "", // this should not happen in practice @@ -3334,7 +3335,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when there is no provider UID in custom session data found in the session storage during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: oidcUpstreamName, @@ -3361,7 +3362,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when there is no provider type in custom session data found in the session storage during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: oidcUpstreamName, @@ -3388,7 +3389,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when there is an illegal provider type in custom session data found in the session storage during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: oidcUpstreamName, @@ -3415,7 +3416,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when there is no OIDC-specific data in custom session data found in the session storage during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: oidcUpstreamName, @@ -3442,7 +3443,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when there is no OIDC refresh token nor access token in custom session data found in the session storage during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: oidcUpstreamName, @@ -3475,7 +3476,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the provider in the session storage is not found due to its name during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: "this-name-will-not-be-found", // this could happen if the OIDCIdentityProvider was deleted since original login @@ -3507,7 +3508,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the provider in the session storage is found but has the wrong resource UID during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder().Build()), authcodeExchange: authcodeExchangeInputs{ customSessionData: &psession.CustomSessionData{ ProviderName: oidcUpstreamName, @@ -3539,7 +3540,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the upstream refresh fails during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). WithPerformRefreshError(errors.New("some upstream refresh error")).Build()), authcodeExchange: happyAuthcodeExchangeInputsForOIDCUpstream, refreshRequest: refreshRequestInputs{ @@ -3557,7 +3558,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the upstream refresh returns an invalid ID token during the refresh request", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()). // This is the current format of the errors returned by the production code version of ValidateTokenAndMergeWithUserInfo, see ValidateTokenAndMergeWithUserInfo in upstreamoidc.go WithValidateTokenAndMergeWithUserInfoError(httperr.Wrap(http.StatusBadRequest, "some validate error", errors.New("some validate cause"))). @@ -3579,7 +3580,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "when the upstream refresh returns an ID token with a different subject than the original", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProviderBuilder(). WithRefreshedTokens(refreshedUpstreamTokensWithIDAndRefreshTokens()). // This is the current format of the errors returned by the production code version of ValidateTokenAndMergeWithUserInfo, see ValidateTokenAndMergeWithUserInfo in upstreamoidc.go WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ @@ -3607,7 +3608,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "refresh grant with claims but not the subject claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3632,7 +3633,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "refresh grant with changed username claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3659,7 +3660,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "refresh grant with changed issuer claim", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC( + idps: testidplister.NewUpstreamIDPListerBuilder().WithOIDC( upstreamOIDCIdentityProviderBuilder().WithUsernameClaim("username-claim").WithValidatedAndMergedWithUserInfoTokens(&oidctypes.Token{ IDToken: &oidctypes.IDToken{ Claims: map[string]interface{}{ @@ -3686,7 +3687,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh happy path", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3702,7 +3703,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh happy path with identity transformations which modify the username and group names", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3735,7 +3736,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh with identity transformations which modify the username and group names when the downstream username has changed compared to initial login", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3775,7 +3776,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh with identity transformations which reject the auth", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3815,7 +3816,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh happy path using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3839,7 +3840,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh happy path without downstream username scope granted, using dynamic client", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3882,7 +3883,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh happy path", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(activeDirectoryUpstreamName). WithResourceUID(activeDirectoryUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3904,7 +3905,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh when the LDAP session data is nil", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3941,7 +3942,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh when the ad session data is nil", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(activeDirectoryUpstreamName). WithResourceUID(activeDirectoryUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -3978,7 +3979,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh when the LDAP session data does not contain dn", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -4019,7 +4020,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh when the active directory session data does not contain dn", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(activeDirectoryUpstreamName). WithResourceUID(activeDirectoryUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -4060,7 +4061,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap refresh returns an error", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -4083,7 +4084,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory refresh returns an error", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(activeDirectoryUpstreamName). WithResourceUID(activeDirectoryUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -4112,7 +4113,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream ldap idp not found", - idps: oidctestutil.NewUpstreamIDPListerBuilder(), + idps: testidplister.NewUpstreamIDPListerBuilder(), authcodeExchange: happyAuthcodeExchangeInputsForLDAPUpstream, refreshRequest: refreshRequestInputs{ want: tokenEndpointResponseExpectedValues{ @@ -4128,7 +4129,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "upstream active directory idp not found", - idps: oidctestutil.NewUpstreamIDPListerBuilder(), + idps: testidplister.NewUpstreamIDPListerBuilder(), authcodeExchange: authcodeExchangeInputs{ modifyAuthRequest: func(r *http.Request) { r.Form.Set("scope", "openid offline_access username groups") }, customSessionData: happyActiveDirectoryCustomSessionData, @@ -4150,7 +4151,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "fosite session is empty", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -4182,7 +4183,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "groups not found in extra field when the groups scope was granted", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -4220,7 +4221,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "username in custom session is empty string during refresh", - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). @@ -4258,7 +4259,7 @@ func TestRefreshGrant(t *testing.T) { }, { 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.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID("the-wrong-uid"). WithURL(ldapUpstreamURL). @@ -4279,7 +4280,7 @@ func TestRefreshGrant(t *testing.T) { }, { 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.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(activeDirectoryUpstreamName). WithResourceUID("the-wrong-uid"). WithURL(ldapUpstreamURL). @@ -4306,7 +4307,7 @@ func TestRefreshGrant(t *testing.T) { }, { name: "auth time is the zero value", // time.Times can never be nil, but it is possible that it would be the zero value which would mean something's wrong - idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). + idps: testidplister.NewUpstreamIDPListerBuilder().WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName(ldapUpstreamName). WithResourceUID(ldapUpstreamResourceUID). WithURL(ldapUpstreamURL). diff --git a/internal/federationdomain/endpointsmanager/manager_test.go b/internal/federationdomain/endpointsmanager/manager_test.go index e857e18bd..75534df8f 100644 --- a/internal/federationdomain/endpointsmanager/manager_test.go +++ b/internal/federationdomain/endpointsmanager/manager_test.go @@ -29,6 +29,7 @@ import ( "go.pinniped.dev/internal/secret" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" ) func TestManager(t *testing.T) { @@ -301,7 +302,7 @@ func TestManager(t *testing.T) { }, } - idpLister := oidctestutil.NewUpstreamIDPListerBuilder(). + idpLister := testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName(upstreamIDPName1). WithClientID("test-client-id-1"). diff --git a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go index fc2ac66a3..358d57921 100644 --- a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go +++ b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder.go @@ -1,4 +1,4 @@ -// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package federationdomainproviders @@ -11,6 +11,8 @@ import ( "go.pinniped.dev/internal/federationdomain/idplister" "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedoidc" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/psession" ) @@ -25,26 +27,14 @@ type FederationDomainIdentityProvider struct { } type FederationDomainIdentityProvidersFinderI interface { - FindDefaultIDP() ( - *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, - *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, - error, - ) - - FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) ( - *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, - *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, - error, - ) - + FindDefaultIDP() (resolvedprovider.FederationDomainResolvedIdentityProvider, error) + FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) (resolvedprovider.FederationDomainResolvedIdentityProvider, error) HasDefaultIDP() bool IDPCount() int } type FederationDomainIdentityProvidersListerI interface { - GetOIDCIdentityProviders() []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider - GetLDAPIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider - GetActiveDirectoryIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + GetIdentityProviders() []resolvedprovider.FederationDomainResolvedIdentityProvider } type FederationDomainIdentityProvidersListerFinderI interface { @@ -105,39 +95,28 @@ func NewFederationDomainIdentityProvidersListerFinder( } func (u *FederationDomainIdentityProvidersListerFinder) IDPCount() int { - return len(u.GetOIDCIdentityProviders()) + len(u.GetLDAPIdentityProviders()) + len(u.GetActiveDirectoryIdentityProviders()) + return len(u.GetIdentityProviders()) } // FindUpstreamIDPByDisplayName selects either an OIDC, LDAP, or ActiveDirectory IDP, or returns an error. // It only considers the allowed IDPs while doing the lookup by display name. // Note that ActiveDirectory and LDAP IDPs both return the same type, but with different SessionProviderType values. func (u *FederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) ( - *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, - *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, + resolvedprovider.FederationDomainResolvedIdentityProvider, error, ) { // Given a display name, look up the identity provider's UID for that display name. idpUIDForDisplayName, ok := u.idpDisplayNamesToResourceUIDsMap[upstreamIDPDisplayName] if !ok { - return nil, nil, fmt.Errorf("identity provider not found: %q", upstreamIDPDisplayName) + return nil, fmt.Errorf("identity provider not found: %q", upstreamIDPDisplayName) } // Find the IDP with that UID. It could be any type, so look at all types to find it. - for _, p := range u.GetOIDCIdentityProviders() { - if p.Provider.GetResourceUID() == idpUIDForDisplayName { - return p, nil, nil + for _, p := range u.GetIdentityProviders() { + if p.GetProvider().GetResourceUID() == idpUIDForDisplayName { + return p, nil } } - for _, p := range u.GetLDAPIdentityProviders() { - if p.Provider.GetResourceUID() == idpUIDForDisplayName { - return nil, p, nil - } - } - for _, p := range u.GetActiveDirectoryIdentityProviders() { - if p.Provider.GetResourceUID() == idpUIDForDisplayName { - return nil, p, nil - } - } - return nil, nil, fmt.Errorf("identity provider not available: %q", upstreamIDPDisplayName) + return nil, fmt.Errorf("identity provider not available: %q", upstreamIDPDisplayName) } func (u *FederationDomainIdentityProvidersListerFinder) HasDefaultIDP() bool { @@ -150,29 +129,30 @@ func (u *FederationDomainIdentityProvidersListerFinder) HasDefaultIDP() bool { // without specifying an IDP name, and there are no IDPs explicitly specified on the FederationDomain, and there // is exactly one IDP CR defined in the Supervisor namespace. func (u *FederationDomainIdentityProvidersListerFinder) FindDefaultIDP() ( - *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, - *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, + resolvedprovider.FederationDomainResolvedIdentityProvider, error, ) { if !u.HasDefaultIDP() { - return nil, nil, fmt.Errorf("identity provider not found: this federation domain does not have a default identity provider") + return nil, fmt.Errorf("identity provider not found: this federation domain does not have a default identity provider") } return u.FindUpstreamIDPByDisplayName(u.defaultIdentityProvider.DisplayName) } -// GetOIDCIdentityProviders lists only the OIDC providers for this FederationDomain. -func (u *FederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders() []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider { +// GetIdentityProviders list all identity providers for this FederationDomain. +func (u *FederationDomainIdentityProvidersListerFinder) GetIdentityProviders() []resolvedprovider.FederationDomainResolvedIdentityProvider { // Get the cached providers once at the start in case they change during the rest of this function. - cachedProviders := u.wrappedLister.GetOIDCIdentityProviders() - providers := []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{} + cachedOIDCProviders := u.wrappedLister.GetOIDCIdentityProviders() + cachedLDAPProviders := u.wrappedLister.GetLDAPIdentityProviders() + cachedADProviders := u.wrappedLister.GetActiveDirectoryIdentityProviders() + providers := []resolvedprovider.FederationDomainResolvedIdentityProvider{} // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... for _, idp := range u.configuredIdentityProviders { // Check if the IDP used by that displayName is in the cached available OIDC providers. - for _, p := range cachedProviders { + for _, p := range cachedOIDCProviders { if idp.UID == p.GetResourceUID() { // Found it, so append it to the result. - providers = append(providers, &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + providers = append(providers, &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: idp.DisplayName, Provider: p, SessionProviderType: psession.ProviderTypeOIDC, @@ -180,23 +160,11 @@ func (u *FederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders }) } } - } - return providers -} - -// GetLDAPIdentityProviders lists only the LDAP providers for this FederationDomain. -func (u *FederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { - // Get the cached providers once at the start in case they change during the rest of this function. - cachedProviders := u.wrappedLister.GetLDAPIdentityProviders() - providers := []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{} - // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might - // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... - for _, idp := range u.configuredIdentityProviders { // Check if the IDP used by that displayName is in the cached available LDAP providers. - for _, p := range cachedProviders { + for _, p := range cachedLDAPProviders { if idp.UID == p.GetResourceUID() { // Found it, so append it to the result. - providers = append(providers, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + providers = append(providers, &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: idp.DisplayName, Provider: p, SessionProviderType: psession.ProviderTypeLDAP, @@ -204,23 +172,11 @@ func (u *FederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProviders }) } } - } - return providers -} - -// GetActiveDirectoryIdentityProviders lists only the ActiveDirectory providers for this FederationDomain. -func (u *FederationDomainIdentityProvidersListerFinder) GetActiveDirectoryIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { - // Get the cached providers once at the start in case they change during the rest of this function. - cachedProviders := u.wrappedLister.GetActiveDirectoryIdentityProviders() - providers := []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{} - // Every configured identityProvider on the FederationDomain uses an objetRef to an underlying IDP CR that might - // be available as a provider in the wrapped cache. For each configured identityProvider/displayName... - for _, idp := range u.configuredIdentityProviders { - // Check if the IDP used by that displayName is in the cached available ActiveDirectory providers. - for _, p := range cachedProviders { + // Check if the IDP used by that displayName is in the cached available AD providers. + for _, p := range cachedADProviders { if idp.UID == p.GetResourceUID() { // Found it, so append it to the result. - providers = append(providers, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + providers = append(providers, &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: idp.DisplayName, Provider: p, SessionProviderType: psession.ProviderTypeActiveDirectory, diff --git a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go index 3c55b5fba..7e4d7a5b9 100644 --- a/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go +++ b/internal/federationdomain/federationdomainproviders/federation_domain_identity_providers_lister_finder_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package federationdomainproviders @@ -10,7 +10,10 @@ import ( "go.pinniped.dev/internal/federationdomain/idplister" "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedoidc" "go.pinniped.dev/internal/testutil/oidctestutil" + "go.pinniped.dev/internal/testutil/testidplister" ) func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { @@ -105,43 +108,38 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { require.NoError(t, err) // Resolved IdPs - myOIDCIDP1Resolved := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + myOIDCIDP1Resolved := &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: "my-oidc-idp1", Provider: myOIDCIDP1, SessionProviderType: "oidc", } - myOIDCIDP2Resolved := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + myOIDCIDP2Resolved := &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: "my-oidc-idp2", Provider: myOIDCIDP2, SessionProviderType: "oidc", } - myLDAPIDP1Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myLDAPIDP1Resolved := &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: "my-ldap-idp1", Provider: myLDAPIDP1, SessionProviderType: "ldap", } - myLDAPIDP2Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myLDAPIDP2Resolved := &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: "my-ldap-idp2", Provider: myLDAPIDP2, SessionProviderType: "ldap", } - myADIDP1Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myADIDP1Resolved := &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: "my-ad-idp1", Provider: myADIDP1, SessionProviderType: "activedirectory", } - myADIDP2Resolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ - DisplayName: "my-ad-idp2", - Provider: myADIDP2, - SessionProviderType: "activedirectory", - } - myDefaultOIDCIDPResolved := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + myDefaultOIDCIDPResolved := &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ DisplayName: "my-default-oidc-idp", Provider: myDefaultOIDCIDP, SessionProviderType: "oidc", } - myDefaultLDAPIDPResolved := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ + myDefaultLDAPIDPResolved := &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ DisplayName: "my-default-ldap-idp", Provider: myDefaultLDAPIDP, SessionProviderType: "ldap", @@ -152,14 +150,14 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { wrappedLister idplister.UpstreamIdentityProvidersLister federationDomainIssuer *FederationDomainIssuer findIDPByDisplayName string - wantOIDCIDPByDisplayName *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider - wantLDAPIDPByDisplayName *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + wantOIDCIDPByDisplayName *resolvedoidc.FederationDomainResolvedOIDCIdentityProvider + wantLDAPIDPByDisplayName *resolvedldap.FederationDomainResolvedLDAPIdentityProvider wantError string }{ { name: "FindUpstreamIDPByDisplayName will find an upstream IdP by display name with one IDP configured", findIDPByDisplayName: "my-oidc-idp1", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithLDAP(myLDAPIDP1). BuildDynamicUpstreamIDPProvider(), @@ -169,7 +167,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { { name: "FindUpstreamIDPByDisplayName will find an upstream IDP by display name if multiple IDPs configured of the same type", findIDPByDisplayName: "my-oidc-idp1", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). BuildDynamicUpstreamIDPProvider(), @@ -179,7 +177,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { { name: "FindUpstreamIDPByDisplayName will find an upstream IDP by display name if multiple IDPs configured of different types", findIDPByDisplayName: "my-oidc-idp1", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithLDAP(myLDAPIDP1). @@ -191,7 +189,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { { name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type OIDC by display name", findIDPByDisplayName: "my-oidc-idp1", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithLDAP(myLDAPIDP1). @@ -204,7 +202,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { { name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type LDAP by display name", findIDPByDisplayName: "my-ldap-idp1", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithLDAP(myLDAPIDP1). @@ -216,7 +214,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { { name: "FindUpstreamIDPByDisplayName will find an upstream IDP of type AD (LDAP) by display name", findIDPByDisplayName: "my-ad-idp1", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithLDAP(myLDAPIDP1). @@ -229,7 +227,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { { name: "FindUpstreamIDPByDisplayName will error if IDP by display name is not found - no such display name", findIDPByDisplayName: "i-cant-find-my-idp", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithLDAP(myLDAPIDP1). @@ -242,7 +240,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { { name: "FindUpstreamIDPByDisplayName will error if IDP by display name is not found - display name was found, but IDP it points at does not exist", findIDPByDisplayName: "my-idp", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithIDPWithLostUID, wantError: `identity provider not available: "my-idp"`, @@ -255,7 +253,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { t.Parallel() subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) - foundOIDCIDP, foundLDAPIDP, err := subject.FindUpstreamIDPByDisplayName(tt.findIDPByDisplayName) + foundIDP, err := subject.FindUpstreamIDPByDisplayName(tt.findIDPByDisplayName) if tt.wantError != "" { require.EqualError(t, err, tt.wantError) @@ -263,10 +261,10 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { require.NoError(t, err) } if tt.wantOIDCIDPByDisplayName != nil { - require.Equal(t, tt.wantOIDCIDPByDisplayName, foundOIDCIDP) + require.Equal(t, tt.wantOIDCIDPByDisplayName, foundIDP) } if tt.wantLDAPIDPByDisplayName != nil { - require.Equal(t, tt.wantLDAPIDPByDisplayName, foundLDAPIDP) + require.Equal(t, tt.wantLDAPIDPByDisplayName, foundIDP) } }) } @@ -275,13 +273,13 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { name string wrappedLister idplister.UpstreamIdentityProvidersLister federationDomainIssuer *FederationDomainIssuer - wantDefaultOIDCIDP *resolvedprovider.FederationDomainResolvedOIDCIdentityProvider - wantDefaultLDAPIDP *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider + wantDefaultOIDCIDP *resolvedoidc.FederationDomainResolvedOIDCIdentityProvider + wantDefaultLDAPIDP *resolvedldap.FederationDomainResolvedLDAPIdentityProvider wantError string }{ { name: "FindDefaultIDP returns an OIDCIdentityProvider if there is an OIDCIdentityProvider defined as the default IDP", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myDefaultOIDCIDP). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithDefaultOIDCIDP, @@ -289,7 +287,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }, { name: "FindDefaultIDP returns an LDAPIdentityProvider if there is an LDAPIdentityProvider defined as the default IDP", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(myDefaultLDAPIDP). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithDefaultLDAPIDP, @@ -297,7 +295,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }, { name: "FindDefaultIDP returns an error if there is no default IDP to return", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(myDefaultLDAPIDP). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithoutIDP, @@ -305,7 +303,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }, { name: "FindDefaultIDP returns an error if there are multiple IDPs configured", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithLDAP(myLDAPIDP1). BuildDynamicUpstreamIDPProvider(), @@ -314,7 +312,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }, { name: "FindDefaultIDP returns an error if the wrapped lister does not contain the default IDP (not available)", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). WithName("my-default-ldap-idp"). WithResourceUID("my-ldap-idp-resource-uid-does-not-match"). @@ -331,7 +329,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { t.Parallel() subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) - foundOIDCIDP, foundLDAPIDP, err := subject.FindDefaultIDP() + foundIDP, err := subject.FindDefaultIDP() if tt.wantError != "" { require.EqualError(t, err, tt.wantError) @@ -339,23 +337,23 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { require.NoError(t, err) } if tt.wantDefaultOIDCIDP != nil { - require.Equal(t, tt.wantDefaultOIDCIDP, foundOIDCIDP) + require.Equal(t, tt.wantDefaultOIDCIDP, foundIDP) } if tt.wantDefaultLDAPIDP != nil { - require.Equal(t, tt.wantDefaultLDAPIDP, foundLDAPIDP) + require.Equal(t, tt.wantDefaultLDAPIDP, foundIDP) } }) } - testGetOIDCIdentityProviders := []struct { + testGetIdentityProviders := []struct { name string wrappedLister idplister.UpstreamIdentityProvidersLister federationDomainIssuer *FederationDomainIssuer - wantIDPs []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider + wantIDPs []resolvedprovider.FederationDomainResolvedIdentityProvider }{ { - name: "GetOIDCIdentityProviders will list all OIDCIdentityProviders", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + name: "GetIdentityProviders will list all identity providers that can be resolved", + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithLDAP(myLDAPIDP1). @@ -363,14 +361,17 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithActiveDirectory(myADIDP1). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + wantIDPs: []resolvedprovider.FederationDomainResolvedIdentityProvider{ myOIDCIDP1Resolved, myOIDCIDP2Resolved, + myLDAPIDP1Resolved, + myLDAPIDP2Resolved, + myADIDP1Resolved, }, }, { - name: "GetLDAPIdentityProviders will return a list of LDAP IDPs if there are LDAPIdentityProviders configured but exclude LDAP IDPs that do not have matching UIDs", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + name: "GetIdentityProviders will return a list of IDPs if there are IDPs configured but exclude IDPs that do not have matching UIDs", + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). @@ -381,153 +382,29 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { WithActiveDirectory(myADIDP1). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithLotsOfIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ + wantIDPs: []resolvedprovider.FederationDomainResolvedIdentityProvider{ myOIDCIDP1Resolved, myOIDCIDP2Resolved, - }, - }, - { - name: "GetOIDCIdentityProviders will return nil of no OIDCIDentityProviders are found", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). - WithLDAP(myLDAPIDP1). - WithLDAP(myLDAPIDP2). - WithActiveDirectory(myADIDP1). - BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{}, - }, - } - - for _, tt := range testGetOIDCIdentityProviders { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) - idps := subject.GetOIDCIdentityProviders() - - require.Equal(t, tt.wantIDPs, idps) - }) - } - - testGetLDAPIdentityProviders := []struct { - name string - wrappedLister idplister.UpstreamIdentityProvidersLister - federationDomainIssuer *FederationDomainIssuer - wantIDPs []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider - }{ - { - name: "GetLDAPIdentityProviders will list all LDAPIdentityProviders", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(myOIDCIDP1). - WithOIDC(myOIDCIDP2). - WithLDAP(myLDAPIDP1). - WithLDAP(myLDAPIDP2). - WithActiveDirectory(myADIDP1). - BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ myLDAPIDP1Resolved, - myLDAPIDP2Resolved, - }, - }, - { - name: "GetLDAPIdentityProviders will return a list of LDAP IDPs if there are LDAPIdentityProviders configured but exclude LDAP IDPs that do not have matching UIDs", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(myOIDCIDP1). - WithOIDC(myOIDCIDP2). - WithLDAP(myLDAPIDP1). - WithLDAP(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). - WithName("my-ldap-idp-that-isnt-in-fd-issuer"). - WithResourceUID("my-ldap-idp-that-isnt-in-fd-issuer"). - Build()). - WithActiveDirectory(myADIDP1). - BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithLotsOfIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ - myLDAPIDP1Resolved, - }, - }, - { - name: "GetLDAPIdentityProviders will return an empty list of IDPs if no LDAPIdentityProviders are found", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(myOIDCIDP1). - WithOIDC(myOIDCIDP2). - WithActiveDirectory(myADIDP1). - BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{}, - }, - } - for _, tt := range testGetLDAPIdentityProviders { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) - idps := subject.GetLDAPIdentityProviders() - - require.Equal(t, tt.wantIDPs, idps) - }) - } - - testGetActiveDirectoryIdentityProviders := []struct { - name string - wrappedLister idplister.UpstreamIdentityProvidersLister - federationDomainIssuer *FederationDomainIssuer - wantIDPs []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider - }{ - { - name: "GetActiveDirectoryIdentityProviders will return a list of LDAP IDPs if there are ActiveDirectoryIdentityProviders configured", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(myOIDCIDP1). - WithOIDC(myOIDCIDP2). - WithLDAP(myLDAPIDP1). - WithActiveDirectory(myADIDP1). - WithActiveDirectory(myADIDP2). - BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ - myADIDP1Resolved, - myADIDP2Resolved, - }, - }, - { - name: "GetActiveDirectoryIdentityProviders will return a list of LDAP IDPs if there are ActiveDirectoryIdentityProviders configured but exclude AD IDPs that do not have matching UIDs", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(myOIDCIDP1). - WithOIDC(myOIDCIDP2). - WithLDAP(myLDAPIDP1). - WithActiveDirectory(myADIDP1). - WithActiveDirectory(oidctestutil.NewTestUpstreamLDAPIdentityProviderBuilder(). - WithName("my-ad-idp-that-isnt-in-fd-issuer"). - WithResourceUID("my-ad-idp-that-isnt-in-fd-issuer"). - Build()). - BuildDynamicUpstreamIDPProvider(), - federationDomainIssuer: fdIssuerWithLotsOfIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ myADIDP1Resolved, }, }, { - name: "GetActiveDirectoryIdentityProviders will return an empty list of LDAP IDPs if no ActiveDirectoryIdentityProviders are found", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(myOIDCIDP1). - WithOIDC(myOIDCIDP2). - WithLDAP(myLDAPIDP1). + name: "GetIdentityProviders will return empty list if no IDPs are found", + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, - wantIDPs: []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{}, + wantIDPs: []resolvedprovider.FederationDomainResolvedIdentityProvider{}, }, } - for _, tt := range testGetActiveDirectoryIdentityProviders { + for _, tt := range testGetIdentityProviders { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() subject := NewFederationDomainIdentityProvidersListerFinder(tt.federationDomainIssuer, tt.wrappedLister) - idps := subject.GetActiveDirectoryIdentityProviders() + idps := subject.GetIdentityProviders() require.Equal(t, tt.wantIDPs, idps) }) @@ -541,14 +418,14 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }{ { name: "IDPCount when there are none to be found", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, wantCount: 0, }, { name: "IDPCount when there are various types of IDP to be found", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myOIDCIDP1). WithOIDC(myOIDCIDP2). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). @@ -591,7 +468,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }{ { name: "HasDefaultIDP when there is an OIDC provider set as default", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(myDefaultOIDCIDP). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithDefaultOIDCIDP, @@ -599,7 +476,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }, { name: "HasDefaultIDP when there is an LDAP provider set as default", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithLDAP(myDefaultLDAPIDP). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithDefaultLDAPIDP, @@ -607,7 +484,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }, { name: "HasDefaultIDP when there is one set even if it cannot be found", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). WithOIDC(oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). WithName("my-oidc-idp-that-isnt-in-fd-issuer"). WithResourceUID("my-oidc-idp-that-isnt-in-fd-issuer"). @@ -618,7 +495,7 @@ func TestFederationDomainIdentityProvidersListerFinder(t *testing.T) { }, { name: "HasDefaultIDP when there is none set", - wrappedLister: oidctestutil.NewUpstreamIDPListerBuilder(). + wrappedLister: testidplister.NewUpstreamIDPListerBuilder(). BuildDynamicUpstreamIDPProvider(), federationDomainIssuer: fdIssuerWithOIDCAndLDAPAndADIDPs, wantHasDefaultIDP: false, diff --git a/internal/federationdomain/resolvedprovider/resolved_provider.go b/internal/federationdomain/resolvedprovider/resolved_provider.go index e2b43a869..9fdb221dd 100644 --- a/internal/federationdomain/resolvedprovider/resolved_provider.go +++ b/internal/federationdomain/resolvedprovider/resolved_provider.go @@ -1,30 +1,61 @@ -// Copyright 2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package resolvedprovider import ( + "context" + + "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/psession" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" ) -// FederationDomainResolvedOIDCIdentityProvider represents a FederationDomainIdentityProvider which has -// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamOIDCIdentityProviderI -// and other metadata about the provider. -type FederationDomainResolvedOIDCIdentityProvider struct { - DisplayName string - Provider upstreamprovider.UpstreamOIDCIdentityProviderI - SessionProviderType psession.ProviderType - Transforms *idtransform.TransformationPipeline +type Identity struct { + // Note that the username is stored in SessionData.Username. + SessionData *psession.CustomSessionData + Groups []string + Subject string + AdditionalClaims map[string]interface{} } -// FederationDomainResolvedLDAPIdentityProvider represents a FederationDomainIdentityProvider which has -// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamLDAPIdentityProviderI -// and other metadata about the provider. -type FederationDomainResolvedLDAPIdentityProvider struct { - DisplayName string - Provider upstreamprovider.UpstreamLDAPIdentityProviderI - SessionProviderType psession.ProviderType - Transforms *idtransform.TransformationPipeline +type UpstreamAuthorizeRequestState struct { + EncodedStateParam string + PKCE pkce.Code + Nonce nonce.Nonce +} + +type FederationDomainResolvedIdentityProvider interface { + GetDisplayName() string + + GetProvider() upstreamprovider.UpstreamIdentityProviderI + + GetSessionProviderType() psession.ProviderType + + GetIDPDiscoveryType() v1alpha1.IDPType + + GetIDPDiscoveryFlows() []v1alpha1.IDPFlow + + GetTransforms() *idtransform.TransformationPipeline + + // UpstreamAuthorizeRedirectURL returns the URL to which the user's browser can be redirected to continue + // the downstream browser-based authorization flow. Returned errors should be of type fosite.RFC6749Error. + UpstreamAuthorizeRedirectURL(state *UpstreamAuthorizeRequestState, downstreamIssuerURL string) (string, error) + + // Login performs auth using a username and password that was submitted by the client, without a web browser. + // This function should authenticate the user with the upstream identity provider, extract their upstream + // identity, and transform it into their downstream identity. + // The groupsWillBeIgnored parameter will be true when the returned groups are going to be ignored by the caller, + // in which case this function may be able to save some effort by avoiding getting the user's upstream groups. + // Returned errors should be of type fosite.RFC6749Error. + Login(ctx context.Context, submittedUsername string, submittedPassword string, groupsWillBeIgnored bool) (*Identity, error) + + // HandleCallback handles an OAuth-style callback in a browser-based flow. This function should complete + // the authorization with the upstream identity provider using the authCode, extract their upstream + // identity, and transform it into their downstream identity. + // Returned errors should be from the httperr package. + HandleCallback(ctx context.Context, authCode string, pkce pkce.Code, nonce nonce.Nonce, redirectURI string) (*Identity, error) } diff --git a/internal/federationdomain/resolvedprovider/resolvedldap/resolved_ldap_provider.go b/internal/federationdomain/resolvedprovider/resolvedldap/resolved_ldap_provider.go new file mode 100644 index 000000000..0867e46d0 --- /dev/null +++ b/internal/federationdomain/resolvedprovider/resolvedldap/resolved_ldap_provider.go @@ -0,0 +1,181 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package resolvedldap + +import ( + "context" + "net/http" + + "github.com/ory/fosite" + + "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" + "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/internal/federationdomain/downstreamsession" + "go.pinniped.dev/internal/federationdomain/downstreamsubject" + "go.pinniped.dev/internal/federationdomain/endpoints/loginurl" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/idtransform" + "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/pkce" +) + +// FederationDomainResolvedLDAPIdentityProvider represents a FederationDomainIdentityProvider which has +// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamLDAPIdentityProviderI +// and other metadata about the provider. +type FederationDomainResolvedLDAPIdentityProvider struct { + DisplayName string + Provider upstreamprovider.UpstreamLDAPIdentityProviderI + SessionProviderType psession.ProviderType + Transforms *idtransform.TransformationPipeline +} + +var _ resolvedprovider.FederationDomainResolvedIdentityProvider = (*FederationDomainResolvedLDAPIdentityProvider)(nil) + +func (p *FederationDomainResolvedLDAPIdentityProvider) GetDisplayName() string { + return p.DisplayName +} + +func (p *FederationDomainResolvedLDAPIdentityProvider) GetProvider() upstreamprovider.UpstreamIdentityProviderI { + return p.Provider +} + +func (p *FederationDomainResolvedLDAPIdentityProvider) GetSessionProviderType() psession.ProviderType { + return p.SessionProviderType +} + +func (p *FederationDomainResolvedLDAPIdentityProvider) GetIDPDiscoveryType() v1alpha1.IDPType { + if p.GetSessionProviderType() == psession.ProviderTypeActiveDirectory { + return v1alpha1.IDPTypeActiveDirectory + } + return v1alpha1.IDPTypeLDAP +} + +func (p *FederationDomainResolvedLDAPIdentityProvider) GetIDPDiscoveryFlows() []v1alpha1.IDPFlow { + return []v1alpha1.IDPFlow{v1alpha1.IDPFlowCLIPassword, v1alpha1.IDPFlowBrowserAuthcode} +} + +func (p *FederationDomainResolvedLDAPIdentityProvider) GetTransforms() *idtransform.TransformationPipeline { + return p.Transforms +} + +func (p *FederationDomainResolvedLDAPIdentityProvider) UpstreamAuthorizeRedirectURL(state *resolvedprovider.UpstreamAuthorizeRequestState, downstreamIssuerURL string) (string, error) { + loginURL, err := loginurl.URL(downstreamIssuerURL, state.EncodedStateParam, loginurl.ShowNoError) + if err != nil { + return "", fosite.ErrServerError.WithHint("Server could not formulate login UI URL for redirect.").WithWrap(err) + } + + return loginURL, nil +} + +// These are special errors that can be returned by Login for a FederationDomainResolvedLDAPIdentityProvider. +var ( + // ErrUnexpectedUpstreamLDAPError is returned by Login when there was an unexpected error during LDAP auth. + // The error returned from Login() should be compared to this using errors.Is(). + ErrUnexpectedUpstreamLDAPError = &fosite.RFC6749Error{ + ErrorField: "error", // this string matches what fosite uses for generic errors + DescriptionField: "Unexpected error during upstream LDAP authentication.", + CodeField: http.StatusBadGateway, + } + + // ErrAccessDeniedDueToUsernamePasswordNotAccepted is returned by Login when the LDAP auth failed due to a + // bad username or password. Due to the way that fosite implements RFC6749Error.Is(), you must use "==" + // to compare this error to an error returned from Login(). + ErrAccessDeniedDueToUsernamePasswordNotAccepted = &fosite.RFC6749Error{ + ErrorField: "access_denied", // this string matches what fosite uses for access denied errors + DescriptionField: "The resource owner or authorization server denied the request.", + HintField: "Username/password not accepted by LDAP provider.", + CodeField: http.StatusForbidden, + } +) + +func (p *FederationDomainResolvedLDAPIdentityProvider) Login( + ctx context.Context, + submittedUsername string, + submittedPassword string, + groupsWillBeIgnored bool, +) (*resolvedprovider.Identity, error) { + authenticateResponse, authenticated, err := p.Provider.AuthenticateUser(ctx, submittedUsername, submittedPassword, groupsWillBeIgnored) + if err != nil { + plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", p.Provider.GetName()) + return nil, ErrUnexpectedUpstreamLDAPError.WithWrap(err) + } + if !authenticated { + return nil, ErrAccessDeniedDueToUsernamePasswordNotAccepted + } + + subject := downstreamSubjectFromUpstreamLDAP(p.Provider, authenticateResponse, p.DisplayName) + upstreamUsername := authenticateResponse.User.GetName() + upstreamGroups := authenticateResponse.User.GetGroups() + + username, groups, err := downstreamsession.ApplyIdentityTransformations(ctx, p.Transforms, upstreamUsername, upstreamGroups) + if err != nil { + return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) + } + + customSessionData := makeDownstreamLDAPOrADCustomSessionData( + p.Provider, p.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups) + + return &resolvedprovider.Identity{ + SessionData: customSessionData, + Groups: groups, + Subject: subject, + }, nil +} + +func (p *FederationDomainResolvedLDAPIdentityProvider) HandleCallback( + _ctx context.Context, + _authCode string, + _pkce pkce.Code, + _nonce nonce.Nonce, + _redirectURI string, +) (*resolvedprovider.Identity, error) { + return nil, httperr.New(http.StatusInternalServerError, "not supported for this type of identity provider") +} + +func makeDownstreamLDAPOrADCustomSessionData( + ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, + idpType psession.ProviderType, + authenticateResponse *authenticators.Response, + username string, + untransformedUpstreamUsername string, + untransformedUpstreamGroups []string, +) *psession.CustomSessionData { + customSessionData := &psession.CustomSessionData{ + Username: username, + UpstreamUsername: untransformedUpstreamUsername, + UpstreamGroups: untransformedUpstreamGroups, + ProviderUID: ldapUpstream.GetResourceUID(), + ProviderName: ldapUpstream.GetName(), + ProviderType: idpType, + } + + if idpType == psession.ProviderTypeLDAP { + customSessionData.LDAP = &psession.LDAPSessionData{ + UserDN: authenticateResponse.DN, + ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, + } + } + + if idpType == psession.ProviderTypeActiveDirectory { + customSessionData.ActiveDirectory = &psession.ActiveDirectorySessionData{ + UserDN: authenticateResponse.DN, + ExtraRefreshAttributes: authenticateResponse.ExtraRefreshAttributes, + } + } + + return customSessionData +} + +func downstreamSubjectFromUpstreamLDAP( + ldapUpstream upstreamprovider.UpstreamLDAPIdentityProviderI, + authenticateResponse *authenticators.Response, + idpDisplayName string, +) string { + ldapURL := *ldapUpstream.GetURL() + return downstreamsubject.LDAP(authenticateResponse.User.GetUID(), ldapURL, idpDisplayName) +} diff --git a/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider.go b/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider.go new file mode 100644 index 000000000..b76166f4c --- /dev/null +++ b/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider.go @@ -0,0 +1,479 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package resolvedoidc + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/ory/fosite" + "golang.org/x/oauth2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1" + "go.pinniped.dev/generated/latest/apis/supervisor/oidc" + "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/federationdomain/downstreamsession" + "go.pinniped.dev/internal/federationdomain/downstreamsubject" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/idtransform" + "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/oidctypes" + "go.pinniped.dev/pkg/oidcclient/pkce" +) + +const ( + // The name of the email claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + emailClaimName = oidc.ScopeEmail + + // The name of the email_verified claim from https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + emailVerifiedClaimName = "email_verified" + + requiredClaimMissingErr = constable.Error("required claim in upstream ID token missing") + requiredClaimInvalidFormatErr = constable.Error("required claim in upstream ID token has invalid format") + requiredClaimEmptyErr = constable.Error("required claim in upstream ID token is empty") + emailVerifiedClaimInvalidFormatErr = constable.Error("email_verified claim in upstream ID token has invalid format") + emailVerifiedClaimFalseErr = constable.Error("email_verified claim in upstream ID token has false value") +) + +// FederationDomainResolvedOIDCIdentityProvider represents a FederationDomainIdentityProvider which has +// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamOIDCIdentityProviderI +// and other metadata about the provider. +type FederationDomainResolvedOIDCIdentityProvider struct { + DisplayName string + Provider upstreamprovider.UpstreamOIDCIdentityProviderI + SessionProviderType psession.ProviderType + Transforms *idtransform.TransformationPipeline +} + +var _ resolvedprovider.FederationDomainResolvedIdentityProvider = (*FederationDomainResolvedOIDCIdentityProvider)(nil) + +func (p *FederationDomainResolvedOIDCIdentityProvider) GetDisplayName() string { + return p.DisplayName +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) GetProvider() upstreamprovider.UpstreamIdentityProviderI { + return p.Provider +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) GetSessionProviderType() psession.ProviderType { + return p.SessionProviderType +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) GetIDPDiscoveryType() v1alpha1.IDPType { + return v1alpha1.IDPTypeOIDC +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) GetIDPDiscoveryFlows() []v1alpha1.IDPFlow { + flows := []v1alpha1.IDPFlow{v1alpha1.IDPFlowBrowserAuthcode} + if p.Provider.AllowsPasswordGrant() { + flows = append(flows, v1alpha1.IDPFlowCLIPassword) + } + return flows +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) GetTransforms() *idtransform.TransformationPipeline { + return p.Transforms +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) UpstreamAuthorizeRedirectURL(state *resolvedprovider.UpstreamAuthorizeRequestState, downstreamIssuerURL string) (string, error) { + upstreamOAuthConfig := oauth2.Config{ + ClientID: p.Provider.GetClientID(), + Endpoint: oauth2.Endpoint{ + AuthURL: p.Provider.GetAuthorizationURL().String(), + }, + RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuerURL), + Scopes: p.Provider.GetScopes(), + } + + authCodeOptions := []oauth2.AuthCodeOption{ + state.Nonce.Param(), + state.PKCE.Challenge(), + state.PKCE.Method(), + } + + for key, val := range p.Provider.GetAdditionalAuthcodeParams() { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam(key, val)) + } + + redirectURL := upstreamOAuthConfig.AuthCodeURL( + state.EncodedStateParam, + authCodeOptions..., + ) + + return redirectURL, nil +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) Login( + ctx context.Context, + submittedUsername string, + submittedPassword string, + _groupsWillBeIgnored bool, // ignored because we always compute the user's group memberships for OIDC, if possible +) (*resolvedprovider.Identity, error) { + if !p.Provider.AllowsPasswordGrant() { + // Return a user-friendly error for this case which is entirely within our control. + return nil, fosite.ErrAccessDenied.WithHint( + "Resource owner password credentials grant is not allowed for this upstream provider according to its configuration.") + } + + token, err := p.Provider.PasswordCredentialsGrantAndValidateTokens(ctx, submittedUsername, submittedPassword) + if err != nil { + // Upstream password grant errors can be generic errors (e.g. a network failure) or can be oauth2.RetrieveError errors + // which represent the http response from the upstream server. These could be a 5XX or some other unexpected error, + // or could be a 400 with a JSON body as described by https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + // which notes that wrong resource owner credentials should result in an "invalid_grant" error. + // However, the exact response is undefined in the sense that there is no such thing as a password grant in + // the OIDC spec, so we don't try too hard to read the upstream errors in this case. (E.g. Dex departs from the + // spec and returns something other than an "invalid_grant" error for bad resource owner credentials.) + return nil, fosite.ErrAccessDenied.WithDebug(err.Error()) // WithDebug hides the error from the client + } + + subject, upstreamUsername, upstreamGroups, err := getDownstreamIdentityFromUpstreamIDToken( + p.Provider, token.IDToken.Claims, p.DisplayName, + ) + if err != nil { + // Return a user-friendly error for this case which is entirely within our control. + return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) + } + + username, groups, err := downstreamsession.ApplyIdentityTransformations(ctx, p.Transforms, upstreamUsername, upstreamGroups) + if err != nil { + return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) + } + + additionalClaims := mapAdditionalClaimsFromUpstreamIDToken(p.Provider, token.IDToken.Claims) + + customSessionData, err := makeDownstreamOIDCCustomSessionData(p.Provider, token, username, upstreamUsername, upstreamGroups) + if err != nil { + return nil, fosite.ErrAccessDenied.WithHintf("Reason: %s.", err.Error()) + } + + return &resolvedprovider.Identity{ + SessionData: customSessionData, + Groups: groups, + Subject: subject, + AdditionalClaims: additionalClaims, + }, nil +} + +func (p *FederationDomainResolvedOIDCIdentityProvider) HandleCallback( + ctx context.Context, + authCode string, + pkce pkce.Code, + nonce nonce.Nonce, + redirectURI string, +) (*resolvedprovider.Identity, error) { + token, err := p.Provider.ExchangeAuthcodeAndValidateTokens( + ctx, + authCode, + pkce, + nonce, + redirectURI, + ) + if err != nil { + plog.WarningErr("error exchanging and validating upstream tokens", err, "upstreamName", p.Provider.GetName()) + return nil, httperr.New(http.StatusBadGateway, "error exchanging and validating upstream tokens") + } + + subject, upstreamUsername, upstreamGroups, err := getDownstreamIdentityFromUpstreamIDToken( + p.Provider, token.IDToken.Claims, p.DisplayName, + ) + if err != nil { + return nil, httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) + } + + username, groups, err := downstreamsession.ApplyIdentityTransformations( + ctx, p.Transforms, upstreamUsername, upstreamGroups, + ) + if err != nil { + return nil, httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) + } + + additionalClaims := mapAdditionalClaimsFromUpstreamIDToken(p.Provider, token.IDToken.Claims) + + customSessionData, err := makeDownstreamOIDCCustomSessionData( + p.Provider, token, username, upstreamUsername, upstreamGroups, + ) + if err != nil { + return nil, httperr.Wrap(http.StatusUnprocessableEntity, err.Error(), err) + } + + return &resolvedprovider.Identity{ + SessionData: customSessionData, + Groups: groups, + Subject: subject, + AdditionalClaims: additionalClaims, + }, nil +} + +func makeDownstreamOIDCCustomSessionData( + oidcUpstream upstreamprovider.UpstreamOIDCIdentityProviderI, + token *oidctypes.Token, + username string, + untransformedUpstreamUsername string, + untransformedUpstreamGroups []string, +) (*psession.CustomSessionData, error) { + upstreamSubject, err := extractStringClaimValue(oidc.IDTokenClaimSubject, oidcUpstream.GetName(), token.IDToken.Claims) + if err != nil { + return nil, err + } + upstreamIssuer, err := extractStringClaimValue(oidc.IDTokenClaimIssuer, oidcUpstream.GetName(), token.IDToken.Claims) + if err != nil { + return nil, err + } + + customSessionData := &psession.CustomSessionData{ + Username: username, + UpstreamUsername: untransformedUpstreamUsername, + UpstreamGroups: untransformedUpstreamGroups, + ProviderUID: oidcUpstream.GetResourceUID(), + ProviderName: oidcUpstream.GetName(), + ProviderType: psession.ProviderTypeOIDC, + OIDC: &psession.OIDCSessionData{ + UpstreamIssuer: upstreamIssuer, + UpstreamSubject: upstreamSubject, + }, + } + + const pleaseCheck = "please check configuration of OIDCIdentityProvider and the client in the " + + "upstream provider's API/UI and try to get a refresh token if possible" + logKV := []interface{}{ + "upstreamName", oidcUpstream.GetName(), + "scopes", oidcUpstream.GetScopes(), + "additionalParams", oidcUpstream.GetAdditionalAuthcodeParams(), + } + + hasRefreshToken := token.RefreshToken != nil && token.RefreshToken.Token != "" + hasAccessToken := token.AccessToken != nil && token.AccessToken.Token != "" + switch { + case hasRefreshToken: // we prefer refresh tokens, so check for this first + customSessionData.OIDC.UpstreamRefreshToken = token.RefreshToken.Token + case hasAccessToken: // as a fallback, we can use the access token as long as there is a userinfo endpoint + if !oidcUpstream.HasUserInfoURL() { + plog.Warning("access token was returned by upstream provider during login without a refresh token "+ + "and there was no userinfo endpoint available on the provider. "+pleaseCheck, logKV...) + return nil, errors.New("access token was returned by upstream provider but there was no userinfo endpoint") + } + plog.Info("refresh token not returned by upstream provider during login, using access token instead. "+pleaseCheck, logKV...) + customSessionData.OIDC.UpstreamAccessToken = token.AccessToken.Token + // When we are in a flow where we will be performing access token based refresh, issue a warning to the client if the access + // token lifetime is very short, since that would mean that the user's session is very short. + // The warnings are stored here and will be processed by the token handler. + threeHoursFromNow := metav1.NewTime(time.Now().Add(3 * time.Hour)) + if !token.AccessToken.Expiry.IsZero() && token.AccessToken.Expiry.Before(&threeHoursFromNow) { + customSessionData.Warnings = append(customSessionData.Warnings, "Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in.") + } + default: + plog.Warning("refresh token and access token not returned by upstream provider during login. "+pleaseCheck, logKV...) + return nil, errors.New("neither access token nor refresh token returned by upstream provider") + } + + return customSessionData, nil +} + +// getDownstreamIdentityFromUpstreamIDToken returns the mapped subject, username, and group names, in that order. +func getDownstreamIdentityFromUpstreamIDToken( + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, + idTokenClaims map[string]interface{}, + idpDisplayName string, +) (string, string, []string, error) { + subject, username, err := getSubjectAndUsernameFromUpstreamIDToken(upstreamIDPConfig, idTokenClaims, idpDisplayName) + if err != nil { + return "", "", nil, err + } + + groups, err := GetGroupsFromUpstreamIDToken(upstreamIDPConfig, idTokenClaims) + if err != nil { + return "", "", nil, err + } + + return subject, username, groups, err +} + +// mapAdditionalClaimsFromUpstreamIDToken returns the additionalClaims mapped from the upstream token, if any. +func mapAdditionalClaimsFromUpstreamIDToken( + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, + idTokenClaims map[string]interface{}, +) map[string]interface{} { + mapped := make(map[string]interface{}, len(upstreamIDPConfig.GetAdditionalClaimMappings())) + for downstreamClaimName, upstreamClaimName := range upstreamIDPConfig.GetAdditionalClaimMappings() { + upstreamClaimValue, ok := idTokenClaims[upstreamClaimName] + if !ok { + plog.Warning( + "additionalClaims mapping claim in upstream ID token missing", + "upstreamName", upstreamIDPConfig.GetName(), + "claimName", upstreamClaimName, + ) + } else { + mapped[downstreamClaimName] = upstreamClaimValue + } + } + return mapped +} + +func getSubjectAndUsernameFromUpstreamIDToken( + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, + idTokenClaims map[string]interface{}, + idpDisplayName string, +) (string, string, error) { + // The spec says the "sub" claim is only unique per issuer, + // so we will prepend the issuer string to make it globally unique. + upstreamIssuer, err := extractStringClaimValue(oidc.IDTokenClaimIssuer, upstreamIDPConfig.GetName(), idTokenClaims) + if err != nil { + return "", "", err + } + upstreamSubject, err := extractStringClaimValue(oidc.IDTokenClaimSubject, upstreamIDPConfig.GetName(), idTokenClaims) + if err != nil { + return "", "", err + } + subject := downstreamsubject.OIDC(upstreamIssuer, upstreamSubject, idpDisplayName) + + usernameClaimName := upstreamIDPConfig.GetUsernameClaim() + if usernameClaimName == "" { + return subject, downstreamUsernameFromUpstreamOIDCSubject(upstreamIssuer, upstreamSubject), nil + } + + // If the upstream username claim is configured to be the special "email" claim and the upstream "email_verified" + // claim is present, then validate that the "email_verified" claim is true. + emailVerifiedAsInterface, ok := idTokenClaims[emailVerifiedClaimName] + if usernameClaimName == emailClaimName && ok { + emailVerified, ok := emailVerifiedAsInterface.(bool) + if !ok { + plog.Warning( + "username claim configured as \"email\" and upstream email_verified claim is not a boolean", + "upstreamName", upstreamIDPConfig.GetName(), + "configuredUsernameClaim", usernameClaimName, + "emailVerifiedClaim", emailVerifiedAsInterface, + ) + return "", "", emailVerifiedClaimInvalidFormatErr + } + if !emailVerified { + plog.Warning( + "username claim configured as \"email\" and upstream email_verified claim has false value", + "upstreamName", upstreamIDPConfig.GetName(), + "configuredUsernameClaim", usernameClaimName, + ) + return "", "", emailVerifiedClaimFalseErr + } + } + + username, err := extractStringClaimValue(usernameClaimName, upstreamIDPConfig.GetName(), idTokenClaims) + if err != nil { + return "", "", err + } + + return subject, username, nil +} + +func extractStringClaimValue(claimName string, upstreamIDPName string, idTokenClaims map[string]interface{}) (string, error) { + value, ok := idTokenClaims[claimName] + if !ok { + plog.Warning( + "required claim in upstream ID token missing", + "upstreamName", upstreamIDPName, + "claimName", claimName, + ) + return "", requiredClaimMissingErr + } + + valueAsString, ok := value.(string) + if !ok { + plog.Warning( + "required claim in upstream ID token is not a string value", + "upstreamName", upstreamIDPName, + "claimName", claimName, + ) + return "", requiredClaimInvalidFormatErr + } + + if valueAsString == "" { + plog.Warning( + "required claim in upstream ID token has an empty string value", + "upstreamName", upstreamIDPName, + "claimName", claimName, + ) + return "", requiredClaimEmptyErr + } + + return valueAsString, nil +} + +func downstreamUsernameFromUpstreamOIDCSubject(upstreamIssuerAsString string, upstreamSubject string) string { + return fmt.Sprintf("%s?%s=%s", upstreamIssuerAsString, + oidc.IDTokenClaimSubject, url.QueryEscape(upstreamSubject), + ) +} + +// GetGroupsFromUpstreamIDToken returns mapped group names coerced into a slice of strings. +// It returns nil when there is no configured groups claim name, or then when the configured claim name is not found +// in the provided map of claims. It returns an error when the claim exists but its value cannot be parsed. +func GetGroupsFromUpstreamIDToken( + upstreamIDPConfig upstreamprovider.UpstreamOIDCIdentityProviderI, + idTokenClaims map[string]interface{}, +) ([]string, error) { + groupsClaimName := upstreamIDPConfig.GetGroupsClaim() + if groupsClaimName == "" { + return nil, nil + } + + groupsAsInterface, ok := idTokenClaims[groupsClaimName] + if !ok { + plog.Warning( + "no groups claim in upstream ID token", + "upstreamName", upstreamIDPConfig.GetName(), + "configuredGroupsClaim", groupsClaimName, + ) + return nil, nil // the upstream IDP may have omitted the claim if the user has no groups + } + + groupsAsArray, okAsArray := extractGroups(groupsAsInterface) + if !okAsArray { + plog.Warning( + "groups claim in upstream ID token has invalid format", + "upstreamName", upstreamIDPConfig.GetName(), + "configuredGroupsClaim", groupsClaimName, + ) + return nil, requiredClaimInvalidFormatErr + } + + return groupsAsArray, nil +} + +func extractGroups(groupsAsInterface interface{}) ([]string, bool) { + groupsAsString, okAsString := groupsAsInterface.(string) + if okAsString { + return []string{groupsAsString}, true + } + + groupsAsStringArray, okAsStringArray := groupsAsInterface.([]string) + if okAsStringArray { + return groupsAsStringArray, true + } + + groupsAsInterfaceArray, okAsArray := groupsAsInterface.([]interface{}) + if !okAsArray { + return nil, false + } + + var groupsAsStrings []string + for _, groupAsInterface := range groupsAsInterfaceArray { + groupAsString, okAsString := groupAsInterface.(string) + if !okAsString { + return nil, false + } + if groupAsString != "" { + groupsAsStrings = append(groupsAsStrings, groupAsString) + } + } + + return groupsAsStrings, true +} diff --git a/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider_test.go b/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider_test.go new file mode 100644 index 000000000..25c2c565a --- /dev/null +++ b/internal/federationdomain/resolvedprovider/resolvedoidc/resolved_oidc_provider_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package resolvedoidc + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/testutil/oidctestutil" +) + +func TestMapAdditionalClaimsFromUpstreamIDToken(t *testing.T) { + tests := []struct { + name string + additionalClaimMappings map[string]string + upstreamClaims map[string]interface{} + wantClaims map[string]interface{} + }{ + { + name: "happy path", + additionalClaimMappings: map[string]string{ + "email": "notification_email", + }, + upstreamClaims: map[string]interface{}{ + "notification_email": "test@example.com", + }, + wantClaims: map[string]interface{}{ + "email": "test@example.com", + }, + }, + { + name: "missing", + additionalClaimMappings: map[string]string{ + "email": "email", + }, + upstreamClaims: map[string]interface{}{}, + wantClaims: map[string]interface{}{}, + }, + { + name: "complex", + additionalClaimMappings: map[string]string{ + "complex": "complex", + }, + upstreamClaims: map[string]interface{}{ + "complex": map[string]string{ + "subClaim": "subValue", + }, + }, + wantClaims: map[string]interface{}{ + "complex": map[string]string{ + "subClaim": "subValue", + }, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + idp := oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). + WithAdditionalClaimMappings(test.additionalClaimMappings). + Build() + actual := mapAdditionalClaimsFromUpstreamIDToken(idp, test.upstreamClaims) + + require.Equal(t, test.wantClaims, actual) + }) + } +} diff --git a/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go index e2e15cba5..74d548668 100644 --- a/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go +++ b/internal/federationdomain/strategy/dynamic_open_id_connect_ecdsa_strategy_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package strategy diff --git a/internal/federationdomain/upstreamprovider/upsteam_provider.go b/internal/federationdomain/upstreamprovider/upsteam_provider.go index 330276107..37bb27a5d 100644 --- a/internal/federationdomain/upstreamprovider/upsteam_provider.go +++ b/internal/federationdomain/upstreamprovider/upsteam_provider.go @@ -38,18 +38,24 @@ type RefreshAttributes struct { SkipGroups bool } -type UpstreamOIDCIdentityProviderI interface { - // GetName returns a name for this upstream provider. The controller watching the OIDCIdentityProviders will +// UpstreamIdentityProviderI includes the interface functions that are common to all upstream identity provider types. +// These represent the identity provider resources, i.e. OIDCIdentityProvider, etc. +type UpstreamIdentityProviderI interface { + // GetName returns a name for this upstream provider. The controller watching the identity provider resources will // set this to be the Name of the CR from its metadata. Note that this is different from the DisplayName configured // in each FederationDomain that uses this provider, so this name is for internal use only, not for interacting // with clients. Clients should not expect to see this name or send this name. GetName() string - // GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow. - GetClientID() string - // GetResourceUID returns the Kubernetes resource ID GetResourceUID() types.UID +} + +type UpstreamOIDCIdentityProviderI interface { + UpstreamIdentityProviderI + + // GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow. + GetClientID() string // GetAuthorizationURL returns the Authorization Endpoint fetched from discovery. GetAuthorizationURL() *url.URL @@ -110,17 +116,13 @@ type UpstreamOIDCIdentityProviderI interface { } type UpstreamLDAPIdentityProviderI interface { - // GetName returns a name for this upstream provider. - GetName() string + UpstreamIdentityProviderI // GetURL returns a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234". // This URL is not used for connecting to the provider, but rather is used for creating a globally unique user // identifier by being combined with the user's UID, since user UIDs are only unique within one provider. GetURL() *url.URL - // GetResourceUID returns the Kubernetes resource ID - GetResourceUID() types.UID - // UserAuthenticator adds an interface method for performing user authentication against the upstream LDAP provider. authenticators.UserAuthenticator diff --git a/internal/testutil/oidctestutil/expected_upstream_state_param.go b/internal/testutil/oidctestutil/expected_upstream_state_param.go new file mode 100644 index 000000000..f3a0bcb96 --- /dev/null +++ b/internal/testutil/oidctestutil/expected_upstream_state_param.go @@ -0,0 +1,63 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidctestutil + +import ( + "testing" + + "github.com/gorilla/securecookie" + "github.com/stretchr/testify/require" +) + +// ExpectedUpstreamStateParamFormat is a separate type from the production code to ensure that the state +// param's contents was serialized in the format that we expect, with the json keys that we expect, etc. +// This also ensure that the order of the serialized fields is the same, which doesn't really matter +// except that we can make simpler equality assertions about the redirect URL in tests. +type ExpectedUpstreamStateParamFormat struct { + P string `json:"p"` + U string `json:"u"` + T string `json:"t"` + N string `json:"n"` + C string `json:"c"` + K string `json:"k"` + V string `json:"v"` +} + +type UpstreamStateParamBuilder ExpectedUpstreamStateParamFormat + +func (b *UpstreamStateParamBuilder) Build(t *testing.T, stateEncoder *securecookie.SecureCookie) string { + state, err := stateEncoder.Encode("s", b) + require.NoError(t, err) + return state +} + +func (b *UpstreamStateParamBuilder) WithAuthorizeRequestParams(params string) *UpstreamStateParamBuilder { + b.P = params + return b +} + +func (b *UpstreamStateParamBuilder) WithNonce(nonce string) *UpstreamStateParamBuilder { + b.N = nonce + return b +} + +func (b *UpstreamStateParamBuilder) WithCSRF(csrf string) *UpstreamStateParamBuilder { + b.C = csrf + return b +} + +func (b *UpstreamStateParamBuilder) WithPKCE(pkce string) *UpstreamStateParamBuilder { + b.K = pkce + return b +} + +func (b *UpstreamStateParamBuilder) WithUpstreamIDPType(upstreamIDPType string) *UpstreamStateParamBuilder { + b.T = upstreamIDPType + return b +} + +func (b *UpstreamStateParamBuilder) WithStateVersion(version string) *UpstreamStateParamBuilder { + b.V = version + return b +} diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go deleted file mode 100644 index c1dc574e4..000000000 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ /dev/null @@ -1,1484 +0,0 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package oidctestutil - -import ( - "context" - "crypto" - "crypto/ecdsa" - "fmt" - "net/url" - "regexp" - "strings" - "testing" - "time" - - coreosoidc "github.com/coreos/go-oidc/v3/oidc" - "github.com/go-jose/go-jose/v3" - "github.com/gorilla/securecookie" - "github.com/ory/fosite" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/fake" - v1 "k8s.io/client-go/kubernetes/typed/core/v1" - kubetesting "k8s.io/client-go/testing" - "k8s.io/utils/strings/slices" - - "go.pinniped.dev/internal/authenticators" - "go.pinniped.dev/internal/crud" - "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" - "go.pinniped.dev/internal/federationdomain/resolvedprovider" - "go.pinniped.dev/internal/federationdomain/upstreamprovider" - "go.pinniped.dev/internal/fositestorage/authorizationcode" - "go.pinniped.dev/internal/fositestorage/openidconnect" - "go.pinniped.dev/internal/fositestorage/pkce" - "go.pinniped.dev/internal/fositestoragei" - "go.pinniped.dev/internal/idtransform" - "go.pinniped.dev/internal/psession" - "go.pinniped.dev/internal/testutil" - "go.pinniped.dev/pkg/oidcclient/nonce" - "go.pinniped.dev/pkg/oidcclient/oidctypes" - oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce" -) - -// Test helpers for the OIDC package. - -// ExchangeAuthcodeAndValidateTokenArgs is used to spy on calls to -// TestUpstreamOIDCIdentityProvider.ExchangeAuthcodeAndValidateTokensFunc(). -type ExchangeAuthcodeAndValidateTokenArgs struct { - Ctx context.Context - Authcode string - PKCECodeVerifier oidcpkce.Code - ExpectedIDTokenNonce nonce.Nonce - RedirectURI string -} - -// PasswordCredentialsGrantAndValidateTokensArgs is used to spy on calls to -// TestUpstreamOIDCIdentityProvider.PasswordCredentialsGrantAndValidateTokensFunc(). -type PasswordCredentialsGrantAndValidateTokensArgs struct { - Ctx context.Context - Username string - Password string -} - -// PerformRefreshArgs is used to spy on calls to -// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc(). -type PerformRefreshArgs struct { - Ctx context.Context - RefreshToken string - DN string - ExpectedUsername string - ExpectedSubject string -} - -// RevokeTokenArgs is used to spy on calls to -// TestUpstreamOIDCIdentityProvider.RevokeTokenArgsFunc(). -type RevokeTokenArgs struct { - Ctx context.Context - Token string - TokenType upstreamprovider.RevocableTokenType -} - -// ValidateTokenAndMergeWithUserInfoArgs is used to spy on calls to -// TestUpstreamOIDCIdentityProvider.ValidateTokenAndMergeWithUserInfoFunc(). -type ValidateTokenAndMergeWithUserInfoArgs struct { - Ctx context.Context - Tok *oauth2.Token - ExpectedIDTokenNonce nonce.Nonce - RequireIDToken bool - RequireUserInfo bool -} - -type ValidateRefreshArgs struct { - Ctx context.Context - Tok *oauth2.Token - StoredAttributes upstreamprovider.RefreshAttributes -} - -func NewTestUpstreamLDAPIdentityProviderBuilder() *TestUpstreamLDAPIdentityProviderBuilder { - return &TestUpstreamLDAPIdentityProviderBuilder{} -} - -type TestUpstreamLDAPIdentityProviderBuilder struct { - name string - resourceUID types.UID - url *url.URL - authenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) - performRefreshCallCount int - performRefreshArgs []*PerformRefreshArgs - performRefreshErr error - performRefreshGroups []string - displayNameForFederationDomain string - transformsForFederationDomain *idtransform.TransformationPipeline -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithName(name string) *TestUpstreamLDAPIdentityProviderBuilder { - t.name = name - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithResourceUID(uid types.UID) *TestUpstreamLDAPIdentityProviderBuilder { - t.resourceUID = uid - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithURL(url *url.URL) *TestUpstreamLDAPIdentityProviderBuilder { - t.url = url - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithAuthenticateFunc(f func(ctx context.Context, username, password string) (*authenticators.Response, bool, error)) *TestUpstreamLDAPIdentityProviderBuilder { - t.authenticateFunc = f - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshCallCount(count int) *TestUpstreamLDAPIdentityProviderBuilder { - t.performRefreshCallCount = count - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshArgs(args []*PerformRefreshArgs) *TestUpstreamLDAPIdentityProviderBuilder { - t.performRefreshArgs = args - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshErr(err error) *TestUpstreamLDAPIdentityProviderBuilder { - t.performRefreshErr = err - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshGroups(groups []string) *TestUpstreamLDAPIdentityProviderBuilder { - t.performRefreshGroups = groups - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithDisplayNameForFederationDomain(displayName string) *TestUpstreamLDAPIdentityProviderBuilder { - t.displayNameForFederationDomain = displayName - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) WithTransformsForFederationDomain(transforms *idtransform.TransformationPipeline) *TestUpstreamLDAPIdentityProviderBuilder { - t.transformsForFederationDomain = transforms - return t -} - -func (t *TestUpstreamLDAPIdentityProviderBuilder) Build() *TestUpstreamLDAPIdentityProvider { - if t.displayNameForFederationDomain == "" { - // default it to the CR name - t.displayNameForFederationDomain = t.name - } - if t.transformsForFederationDomain == nil { - // default to an empty pipeline - t.transformsForFederationDomain = idtransform.NewTransformationPipeline() - } - return &TestUpstreamLDAPIdentityProvider{ - Name: t.name, - ResourceUID: t.resourceUID, - URL: t.url, - AuthenticateFunc: t.authenticateFunc, - performRefreshCallCount: t.performRefreshCallCount, - performRefreshArgs: t.performRefreshArgs, - PerformRefreshErr: t.performRefreshErr, - PerformRefreshGroups: t.performRefreshGroups, - DisplayNameForFederationDomain: t.displayNameForFederationDomain, - TransformsForFederationDomain: t.transformsForFederationDomain, - } -} - -type TestUpstreamLDAPIdentityProvider struct { - Name string - ResourceUID types.UID - URL *url.URL - AuthenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) - performRefreshCallCount int - performRefreshArgs []*PerformRefreshArgs - PerformRefreshErr error - PerformRefreshGroups []string - DisplayNameForFederationDomain string - TransformsForFederationDomain *idtransform.TransformationPipeline -} - -var _ upstreamprovider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} - -func (u *TestUpstreamLDAPIdentityProvider) GetResourceUID() types.UID { - return u.ResourceUID -} - -func (u *TestUpstreamLDAPIdentityProvider) GetName() string { - return u.Name -} - -func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string, _skipGroups bool) (*authenticators.Response, bool, error) { - return u.AuthenticateFunc(ctx, username, password) -} - -func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL { - return u.URL -} - -func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, _idpDisplayName string) ([]string, error) { - if u.performRefreshArgs == nil { - u.performRefreshArgs = make([]*PerformRefreshArgs, 0) - } - u.performRefreshCallCount++ - u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{ - Ctx: ctx, - DN: storedRefreshAttributes.DN, - ExpectedUsername: storedRefreshAttributes.Username, - ExpectedSubject: storedRefreshAttributes.Subject, - }) - if u.PerformRefreshErr != nil { - return nil, u.PerformRefreshErr - } - return u.PerformRefreshGroups, 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 { - Name string - ClientID string - ResourceUID types.UID - AuthorizationURL url.URL - UserInfoURL bool - RevocationURL *url.URL - UsernameClaim string - GroupsClaim string - Scopes []string - AdditionalAuthcodeParams map[string]string - AdditionalClaimMappings map[string]string - AllowPasswordGrant bool - DisplayNameForFederationDomain string - TransformsForFederationDomain *idtransform.TransformationPipeline - - ExchangeAuthcodeAndValidateTokensFunc func( - ctx context.Context, - authcode string, - pkceCodeVerifier oidcpkce.Code, - expectedIDTokenNonce nonce.Nonce, - ) (*oidctypes.Token, error) - - PasswordCredentialsGrantAndValidateTokensFunc func( - ctx context.Context, - username string, - password string, - ) (*oidctypes.Token, error) - - PerformRefreshFunc func(ctx context.Context, refreshToken string) (*oauth2.Token, error) - - RevokeTokenFunc func(ctx context.Context, refreshToken string, tokenType upstreamprovider.RevocableTokenType) error - - ValidateTokenAndMergeWithUserInfoFunc func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) - - exchangeAuthcodeAndValidateTokensCallCount int - exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs - passwordCredentialsGrantAndValidateTokensCallCount int - passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs - performRefreshCallCount int - performRefreshArgs []*PerformRefreshArgs - revokeTokenCallCount int - revokeTokenArgs []*RevokeTokenArgs - validateTokenAndMergeWithUserInfoCallCount int - validateTokenAndMergeWithUserInfoArgs []*ValidateTokenAndMergeWithUserInfoArgs -} - -var _ upstreamprovider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{} - -func (u *TestUpstreamOIDCIdentityProvider) GetResourceUID() types.UID { - return u.ResourceUID -} - -func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalAuthcodeParams() map[string]string { - return u.AdditionalAuthcodeParams -} - -func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalClaimMappings() map[string]string { - return u.AdditionalClaimMappings -} - -func (u *TestUpstreamOIDCIdentityProvider) GetName() string { - return u.Name -} - -func (u *TestUpstreamOIDCIdentityProvider) GetClientID() string { - return u.ClientID -} - -func (u *TestUpstreamOIDCIdentityProvider) GetAuthorizationURL() *url.URL { - return &u.AuthorizationURL -} - -func (u *TestUpstreamOIDCIdentityProvider) HasUserInfoURL() bool { - return u.UserInfoURL -} - -func (u *TestUpstreamOIDCIdentityProvider) GetRevocationURL() *url.URL { - return u.RevocationURL -} - -func (u *TestUpstreamOIDCIdentityProvider) GetScopes() []string { - return u.Scopes -} - -func (u *TestUpstreamOIDCIdentityProvider) GetUsernameClaim() string { - return u.UsernameClaim -} - -func (u *TestUpstreamOIDCIdentityProvider) GetGroupsClaim() string { - return u.GroupsClaim -} - -func (u *TestUpstreamOIDCIdentityProvider) AllowsPasswordGrant() bool { - return u.AllowPasswordGrant -} - -func (u *TestUpstreamOIDCIdentityProvider) PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error) { - u.passwordCredentialsGrantAndValidateTokensCallCount++ - u.passwordCredentialsGrantAndValidateTokensArgs = append(u.passwordCredentialsGrantAndValidateTokensArgs, &PasswordCredentialsGrantAndValidateTokensArgs{ - Ctx: ctx, - Username: username, - Password: password, - }) - return u.PasswordCredentialsGrantAndValidateTokensFunc(ctx, username, password) -} - -func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokens( - ctx context.Context, - authcode string, - pkceCodeVerifier oidcpkce.Code, - expectedIDTokenNonce nonce.Nonce, - redirectURI string, -) (*oidctypes.Token, error) { - if u.exchangeAuthcodeAndValidateTokensArgs == nil { - u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) - } - u.exchangeAuthcodeAndValidateTokensCallCount++ - u.exchangeAuthcodeAndValidateTokensArgs = append(u.exchangeAuthcodeAndValidateTokensArgs, &ExchangeAuthcodeAndValidateTokenArgs{ - Ctx: ctx, - Authcode: authcode, - PKCECodeVerifier: pkceCodeVerifier, - ExpectedIDTokenNonce: expectedIDTokenNonce, - RedirectURI: redirectURI, - }) - return u.ExchangeAuthcodeAndValidateTokensFunc(ctx, authcode, pkceCodeVerifier, expectedIDTokenNonce) -} - -func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensCallCount() int { - return u.exchangeAuthcodeAndValidateTokensCallCount -} - -func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensArgs(call int) *ExchangeAuthcodeAndValidateTokenArgs { - if u.exchangeAuthcodeAndValidateTokensArgs == nil { - u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) - } - return u.exchangeAuthcodeAndValidateTokensArgs[call] -} - -func (u *TestUpstreamOIDCIdentityProvider) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) { - if u.performRefreshArgs == nil { - u.performRefreshArgs = make([]*PerformRefreshArgs, 0) - } - u.performRefreshCallCount++ - u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{ - Ctx: ctx, - RefreshToken: refreshToken, - }) - return u.PerformRefreshFunc(ctx, refreshToken) -} - -func (u *TestUpstreamOIDCIdentityProvider) RevokeToken(ctx context.Context, token string, tokenType upstreamprovider.RevocableTokenType) error { - if u.revokeTokenArgs == nil { - u.revokeTokenArgs = make([]*RevokeTokenArgs, 0) - } - u.revokeTokenCallCount++ - u.revokeTokenArgs = append(u.revokeTokenArgs, &RevokeTokenArgs{ - Ctx: ctx, - Token: token, - TokenType: tokenType, - }) - return u.RevokeTokenFunc(ctx, token, tokenType) -} - -func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshCallCount() int { - return u.performRefreshCallCount -} - -func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs { - if u.performRefreshArgs == nil { - u.performRefreshArgs = make([]*PerformRefreshArgs, 0) - } - return u.performRefreshArgs[call] -} - -func (u *TestUpstreamOIDCIdentityProvider) RevokeTokenCallCount() int { - return u.performRefreshCallCount -} - -func (u *TestUpstreamOIDCIdentityProvider) RevokeTokenArgs(call int) *RevokeTokenArgs { - if u.revokeTokenArgs == nil { - u.revokeTokenArgs = make([]*RevokeTokenArgs, 0) - } - return u.revokeTokenArgs[call] -} - -func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfo(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, requireIDToken bool, requireUserInfo bool) (*oidctypes.Token, error) { - if u.validateTokenAndMergeWithUserInfoArgs == nil { - u.validateTokenAndMergeWithUserInfoArgs = make([]*ValidateTokenAndMergeWithUserInfoArgs, 0) - } - u.validateTokenAndMergeWithUserInfoCallCount++ - u.validateTokenAndMergeWithUserInfoArgs = append(u.validateTokenAndMergeWithUserInfoArgs, &ValidateTokenAndMergeWithUserInfoArgs{ - Ctx: ctx, - Tok: tok, - ExpectedIDTokenNonce: expectedIDTokenNonce, - RequireIDToken: requireIDToken, - RequireUserInfo: requireUserInfo, - }) - return u.ValidateTokenAndMergeWithUserInfoFunc(ctx, tok, expectedIDTokenNonce) -} - -func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfoCallCount() int { - return u.validateTokenAndMergeWithUserInfoCallCount -} - -func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfoArgs(call int) *ValidateTokenAndMergeWithUserInfoArgs { - if u.validateTokenAndMergeWithUserInfoArgs == nil { - u.validateTokenAndMergeWithUserInfoArgs = make([]*ValidateTokenAndMergeWithUserInfoArgs, 0) - } - return u.validateTokenAndMergeWithUserInfoArgs[call] -} - -type TestFederationDomainIdentityProvidersListerFinder struct { - upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider - upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider - upstreamActiveDirectoryIdentityProviders []*TestUpstreamLDAPIdentityProvider - defaultIDPDisplayName string -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) HasDefaultIDP() bool { - return t.defaultIDPDisplayName != "" -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) IDPCount() int { - return len(t.upstreamOIDCIdentityProviders) + len(t.upstreamLDAPIdentityProviders) + len(t.upstreamActiveDirectoryIdentityProviders) -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) GetOIDCIdentityProviders() []*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider { - fdIDPs := make([]*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, len(t.upstreamOIDCIdentityProviders)) - for i, testIDP := range t.upstreamOIDCIdentityProviders { - fdIDP := &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ - DisplayName: testIDP.DisplayNameForFederationDomain, - Provider: testIDP, - SessionProviderType: psession.ProviderTypeOIDC, - Transforms: testIDP.TransformsForFederationDomain, - } - fdIDPs[i] = fdIDP - } - return fdIDPs -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) GetLDAPIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { - fdIDPs := make([]*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamLDAPIdentityProviders)) - for i, testIDP := range t.upstreamLDAPIdentityProviders { - fdIDP := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ - DisplayName: testIDP.DisplayNameForFederationDomain, - Provider: testIDP, - SessionProviderType: psession.ProviderTypeLDAP, - Transforms: testIDP.TransformsForFederationDomain, - } - fdIDPs[i] = fdIDP - } - return fdIDPs -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) GetActiveDirectoryIdentityProviders() []*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider { - fdIDPs := make([]*resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, len(t.upstreamActiveDirectoryIdentityProviders)) - for i, testIDP := range t.upstreamActiveDirectoryIdentityProviders { - fdIDP := &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ - DisplayName: testIDP.DisplayNameForFederationDomain, - Provider: testIDP, - SessionProviderType: psession.ProviderTypeActiveDirectory, - Transforms: testIDP.TransformsForFederationDomain, - } - fdIDPs[i] = fdIDP - } - return fdIDPs -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) FindDefaultIDP() (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error) { - if t.defaultIDPDisplayName == "" { - return nil, nil, fmt.Errorf("identity provider not found: this federation domain does not have a default identity provider") - } - return t.FindUpstreamIDPByDisplayName(t.defaultIDPDisplayName) -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) (*resolvedprovider.FederationDomainResolvedOIDCIdentityProvider, *resolvedprovider.FederationDomainResolvedLDAPIdentityProvider, error) { - for _, testIDP := range t.upstreamOIDCIdentityProviders { - if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { - return &resolvedprovider.FederationDomainResolvedOIDCIdentityProvider{ - DisplayName: testIDP.DisplayNameForFederationDomain, - Provider: testIDP, - SessionProviderType: psession.ProviderTypeOIDC, - Transforms: testIDP.TransformsForFederationDomain, - }, nil, nil - } - } - for _, testIDP := range t.upstreamLDAPIdentityProviders { - if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { - return nil, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ - DisplayName: testIDP.DisplayNameForFederationDomain, - Provider: testIDP, - SessionProviderType: psession.ProviderTypeLDAP, - Transforms: testIDP.TransformsForFederationDomain, - }, nil - } - } - for _, testIDP := range t.upstreamActiveDirectoryIdentityProviders { - if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { - return nil, &resolvedprovider.FederationDomainResolvedLDAPIdentityProvider{ - DisplayName: testIDP.DisplayNameForFederationDomain, - Provider: testIDP, - SessionProviderType: psession.ProviderTypeActiveDirectory, - Transforms: testIDP.TransformsForFederationDomain, - }, nil - } - } - return nil, nil, fmt.Errorf("did not find IDP with name %q", upstreamIDPDisplayName) -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) SetOIDCIdentityProviders(providers []*TestUpstreamOIDCIdentityProvider) { - t.upstreamOIDCIdentityProviders = providers -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) SetLDAPIdentityProviders(providers []*TestUpstreamLDAPIdentityProvider) { - t.upstreamLDAPIdentityProviders = providers -} - -func (t *TestFederationDomainIdentityProvidersListerFinder) SetActiveDirectoryIdentityProviders(providers []*TestUpstreamLDAPIdentityProvider) { - t.upstreamActiveDirectoryIdentityProviders = providers -} - -type UpstreamIDPListerBuilder struct { - upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider - upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider - upstreamActiveDirectoryIdentityProviders []*TestUpstreamLDAPIdentityProvider - defaultIDPDisplayName string -} - -func (b *UpstreamIDPListerBuilder) WithOIDC(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) *UpstreamIDPListerBuilder { - b.upstreamOIDCIdentityProviders = append(b.upstreamOIDCIdentityProviders, upstreamOIDCIdentityProviders...) - return b -} - -func (b *UpstreamIDPListerBuilder) WithLDAP(upstreamLDAPIdentityProviders ...*TestUpstreamLDAPIdentityProvider) *UpstreamIDPListerBuilder { - b.upstreamLDAPIdentityProviders = append(b.upstreamLDAPIdentityProviders, upstreamLDAPIdentityProviders...) - return b -} - -func (b *UpstreamIDPListerBuilder) WithActiveDirectory(upstreamActiveDirectoryIdentityProviders ...*TestUpstreamLDAPIdentityProvider) *UpstreamIDPListerBuilder { - b.upstreamActiveDirectoryIdentityProviders = append(b.upstreamActiveDirectoryIdentityProviders, upstreamActiveDirectoryIdentityProviders...) - return b -} - -func (b *UpstreamIDPListerBuilder) WithDefaultIDPDisplayName(defaultIDPDisplayName string) *UpstreamIDPListerBuilder { - b.defaultIDPDisplayName = defaultIDPDisplayName - return b -} - -func (b *UpstreamIDPListerBuilder) BuildFederationDomainIdentityProvidersListerFinder() *TestFederationDomainIdentityProvidersListerFinder { - return &TestFederationDomainIdentityProvidersListerFinder{ - upstreamOIDCIdentityProviders: b.upstreamOIDCIdentityProviders, - upstreamLDAPIdentityProviders: b.upstreamLDAPIdentityProviders, - upstreamActiveDirectoryIdentityProviders: b.upstreamActiveDirectoryIdentityProviders, - defaultIDPDisplayName: b.defaultIDPDisplayName, - } -} - -func (b *UpstreamIDPListerBuilder) BuildDynamicUpstreamIDPProvider() dynamicupstreamprovider.DynamicUpstreamIDPProvider { - idpProvider := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() - - oidcUpstreams := make([]upstreamprovider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) - for i := range b.upstreamOIDCIdentityProviders { - oidcUpstreams[i] = upstreamprovider.UpstreamOIDCIdentityProviderI(b.upstreamOIDCIdentityProviders[i]) - } - idpProvider.SetOIDCIdentityProviders(oidcUpstreams) - - ldapUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, len(b.upstreamLDAPIdentityProviders)) - for i := range b.upstreamLDAPIdentityProviders { - ldapUpstreams[i] = upstreamprovider.UpstreamLDAPIdentityProviderI(b.upstreamLDAPIdentityProviders[i]) - } - idpProvider.SetLDAPIdentityProviders(ldapUpstreams) - - adUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, len(b.upstreamActiveDirectoryIdentityProviders)) - for i := range b.upstreamActiveDirectoryIdentityProviders { - adUpstreams[i] = upstreamprovider.UpstreamLDAPIdentityProviderI(b.upstreamActiveDirectoryIdentityProviders[i]) - } - idpProvider.SetActiveDirectoryIdentityProviders(adUpstreams) - - return idpProvider -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPasswordCredentialsGrantAndValidateTokens( - t *testing.T, - expectedPerformedByUpstreamName string, - expectedArgs *PasswordCredentialsGrantAndValidateTokensArgs, -) { - t.Helper() - var actualArgs *PasswordCredentialsGrantAndValidateTokensArgs - var actualNameOfUpstreamWhichMadeCall string - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - callCountOnThisUpstream := upstreamOIDC.passwordCredentialsGrantAndValidateTokensCallCount - actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream - if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name - actualArgs = upstreamOIDC.passwordCredentialsGrantAndValidateTokensArgs[0] - } - } - require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, - "should have been exactly one call to PasswordCredentialsGrantAndValidateTokens() by all OIDC upstreams", - ) - require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, - "PasswordCredentialsGrantAndValidateTokens() was called on the wrong OIDC upstream", - ) - require.Equal(t, expectedArgs, actualArgs) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPasswordCredentialsGrantAndValidateTokens(t *testing.T) { - t.Helper() - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.passwordCredentialsGrantAndValidateTokensCallCount - } - require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, - "expected exactly zero calls to PasswordCredentialsGrantAndValidateTokens()", - ) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToExchangeAuthcodeAndValidateTokens( - t *testing.T, - expectedPerformedByUpstreamName string, - expectedArgs *ExchangeAuthcodeAndValidateTokenArgs, -) { - t.Helper() - var actualArgs *ExchangeAuthcodeAndValidateTokenArgs - var actualNameOfUpstreamWhichMadeCall string - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - callCountOnThisUpstream := upstreamOIDC.exchangeAuthcodeAndValidateTokensCallCount - actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream - if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name - actualArgs = upstreamOIDC.exchangeAuthcodeAndValidateTokensArgs[0] - } - } - require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, - "should have been exactly one call to ExchangeAuthcodeAndValidateTokens() by all OIDC upstreams", - ) - require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, - "ExchangeAuthcodeAndValidateTokens() was called on the wrong OIDC upstream", - ) - require.Equal(t, expectedArgs, actualArgs) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToExchangeAuthcodeAndValidateTokens(t *testing.T) { - t.Helper() - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.exchangeAuthcodeAndValidateTokensCallCount - } - require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, - "expected exactly zero calls to ExchangeAuthcodeAndValidateTokens()", - ) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh( - t *testing.T, - expectedPerformedByUpstreamName string, - expectedArgs *PerformRefreshArgs, -) { - t.Helper() - var actualArgs *PerformRefreshArgs - var actualNameOfUpstreamWhichMadeCall string - actualCallCountAcrossAllUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - callCountOnThisUpstream := upstreamOIDC.performRefreshCallCount - actualCallCountAcrossAllUpstreams += callCountOnThisUpstream - if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name - actualArgs = upstreamOIDC.performRefreshArgs[0] - } - } - for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders { - 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, - "PerformRefresh() was called on the wrong upstream", - ) - require.Equal(t, expectedArgs, actualArgs) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) { - t.Helper() - actualCallCountAcrossAllUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - actualCallCountAcrossAllUpstreams += upstreamOIDC.performRefreshCallCount - } - 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()", - ) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToValidateToken( - t *testing.T, - expectedPerformedByUpstreamName string, - expectedArgs *ValidateTokenAndMergeWithUserInfoArgs, -) { - t.Helper() - var actualArgs *ValidateTokenAndMergeWithUserInfoArgs - var actualNameOfUpstreamWhichMadeCall string - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - callCountOnThisUpstream := upstreamOIDC.validateTokenAndMergeWithUserInfoCallCount - actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream - if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name - actualArgs = upstreamOIDC.validateTokenAndMergeWithUserInfoArgs[0] - } - } - require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, - "should have been exactly one call to ValidateTokenAndMergeWithUserInfo() by all OIDC upstreams", - ) - require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, - "ValidateTokenAndMergeWithUserInfo() was called on the wrong OIDC upstream", - ) - require.Equal(t, expectedArgs, actualArgs) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToValidateToken(t *testing.T) { - t.Helper() - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.validateTokenAndMergeWithUserInfoCallCount - } - require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, - "expected exactly zero calls to ValidateTokenAndMergeWithUserInfo()", - ) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToRevokeToken( - t *testing.T, - expectedPerformedByUpstreamName string, - expectedArgs *RevokeTokenArgs, -) { - t.Helper() - var actualArgs *RevokeTokenArgs - var actualNameOfUpstreamWhichMadeCall string - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - callCountOnThisUpstream := upstreamOIDC.revokeTokenCallCount - actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream - if callCountOnThisUpstream == 1 { - actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name - actualArgs = upstreamOIDC.revokeTokenArgs[0] - } - } - require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, - "should have been exactly one call to RevokeToken() by all OIDC upstreams", - ) - require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, - "RevokeToken() was called on the wrong OIDC upstream", - ) - require.Equal(t, expectedArgs, actualArgs) -} - -func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToRevokeToken(t *testing.T) { - t.Helper() - actualCallCountAcrossAllOIDCUpstreams := 0 - for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { - actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.revokeTokenCallCount - } - require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, - "expected exactly zero calls to RevokeToken()", - ) -} - -func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { - return &UpstreamIDPListerBuilder{} -} - -type TestUpstreamOIDCIdentityProviderBuilder struct { - name string - resourceUID types.UID - clientID string - scopes []string - idToken map[string]interface{} - refreshToken *oidctypes.RefreshToken - accessToken *oidctypes.AccessToken - usernameClaim string - groupsClaim string - refreshedTokens *oauth2.Token - validatedAndMergedWithUserInfoTokens *oidctypes.Token - authorizationURL url.URL - hasUserInfoURL bool - additionalAuthcodeParams map[string]string - additionalClaimMappings map[string]string - allowPasswordGrant bool - authcodeExchangeErr error - passwordGrantErr error - performRefreshErr error - revokeTokenErr error - validateTokenAndMergeWithUserInfoErr error - displayNameForFederationDomain string - transformsForFederationDomain *idtransform.TransformationPipeline -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder { - u.name = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithResourceUID(value types.UID) *TestUpstreamOIDCIdentityProviderBuilder { - u.resourceUID = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithClientID(value string) *TestUpstreamOIDCIdentityProviderBuilder { - u.clientID = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAuthorizationURL(value url.URL) *TestUpstreamOIDCIdentityProviderBuilder { - u.authorizationURL = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUserInfoURL() *TestUpstreamOIDCIdentityProviderBuilder { - u.hasUserInfoURL = true - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutUserInfoURL() *TestUpstreamOIDCIdentityProviderBuilder { - u.hasUserInfoURL = false - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAllowPasswordGrant(value bool) *TestUpstreamOIDCIdentityProviderBuilder { - u.allowPasswordGrant = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithScopes(values []string) *TestUpstreamOIDCIdentityProviderBuilder { - u.scopes = values - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUsernameClaim(value string) *TestUpstreamOIDCIdentityProviderBuilder { - u.usernameClaim = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutUsernameClaim() *TestUpstreamOIDCIdentityProviderBuilder { - u.usernameClaim = "" - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithGroupsClaim(value string) *TestUpstreamOIDCIdentityProviderBuilder { - u.groupsClaim = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutGroupsClaim() *TestUpstreamOIDCIdentityProviderBuilder { - u.groupsClaim = "" - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithIDTokenClaim(name string, value interface{}) *TestUpstreamOIDCIdentityProviderBuilder { - if u.idToken == nil { - u.idToken = map[string]interface{}{} - } - u.idToken[name] = value - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutIDTokenClaim(claim string) *TestUpstreamOIDCIdentityProviderBuilder { - delete(u.idToken, claim) - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalAuthcodeParams(params map[string]string) *TestUpstreamOIDCIdentityProviderBuilder { - u.additionalAuthcodeParams = params - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalClaimMappings(m map[string]string) *TestUpstreamOIDCIdentityProviderBuilder { - u.additionalClaimMappings = m - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshToken(token string) *TestUpstreamOIDCIdentityProviderBuilder { - u.refreshToken = &oidctypes.RefreshToken{Token: token} - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithEmptyRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder { - u.refreshToken = &oidctypes.RefreshToken{Token: ""} - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder { - u.refreshToken = nil - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAccessToken(token string, expiry metav1.Time) *TestUpstreamOIDCIdentityProviderBuilder { - u.accessToken = &oidctypes.AccessToken{Token: token, Expiry: expiry} - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithEmptyAccessToken() *TestUpstreamOIDCIdentityProviderBuilder { - u.accessToken = &oidctypes.AccessToken{Token: ""} - return u -} -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutAccessToken() *TestUpstreamOIDCIdentityProviderBuilder { - u.accessToken = nil - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder { - u.authcodeExchangeErr = err - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPasswordGrantError(err error) *TestUpstreamOIDCIdentityProviderBuilder { - u.passwordGrantErr = err - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshedTokens(tokens *oauth2.Token) *TestUpstreamOIDCIdentityProviderBuilder { - u.refreshedTokens = tokens - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPerformRefreshError(err error) *TestUpstreamOIDCIdentityProviderBuilder { - u.performRefreshErr = err - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidatedAndMergedWithUserInfoTokens(tokens *oidctypes.Token) *TestUpstreamOIDCIdentityProviderBuilder { - u.validatedAndMergedWithUserInfoTokens = tokens - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidateTokenAndMergeWithUserInfoError(err error) *TestUpstreamOIDCIdentityProviderBuilder { - u.validateTokenAndMergeWithUserInfoErr = err - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRevokeTokenError(err error) *TestUpstreamOIDCIdentityProviderBuilder { - u.revokeTokenErr = err - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithDisplayNameForFederationDomain(displayName string) *TestUpstreamOIDCIdentityProviderBuilder { - u.displayNameForFederationDomain = displayName - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) WithTransformsForFederationDomain(transforms *idtransform.TransformationPipeline) *TestUpstreamOIDCIdentityProviderBuilder { - u.transformsForFederationDomain = transforms - return u -} - -func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdentityProvider { - if u.displayNameForFederationDomain == "" { - // default it to the CR name - u.displayNameForFederationDomain = u.name - } - if u.transformsForFederationDomain == nil { - // default to an empty pipeline - u.transformsForFederationDomain = idtransform.NewTransformationPipeline() - } - - return &TestUpstreamOIDCIdentityProvider{ - Name: u.name, - ClientID: u.clientID, - ResourceUID: u.resourceUID, - UsernameClaim: u.usernameClaim, - GroupsClaim: u.groupsClaim, - Scopes: u.scopes, - AllowPasswordGrant: u.allowPasswordGrant, - AuthorizationURL: u.authorizationURL, - UserInfoURL: u.hasUserInfoURL, - AdditionalAuthcodeParams: u.additionalAuthcodeParams, - AdditionalClaimMappings: u.additionalClaimMappings, - DisplayNameForFederationDomain: u.displayNameForFederationDomain, - TransformsForFederationDomain: u.transformsForFederationDomain, - ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier oidcpkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { - if u.authcodeExchangeErr != nil { - return nil, u.authcodeExchangeErr - } - return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken, AccessToken: u.accessToken}, nil - }, - PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) { - if u.passwordGrantErr != nil { - return nil, u.passwordGrantErr - } - return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken, AccessToken: u.accessToken}, nil - }, - PerformRefreshFunc: func(ctx context.Context, refreshToken string) (*oauth2.Token, error) { - if u.performRefreshErr != nil { - return nil, u.performRefreshErr - } - return u.refreshedTokens, nil - }, - RevokeTokenFunc: func(ctx context.Context, refreshToken string, tokenType upstreamprovider.RevocableTokenType) error { - return u.revokeTokenErr - }, - ValidateTokenAndMergeWithUserInfoFunc: func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { - if u.validateTokenAndMergeWithUserInfoErr != nil { - return nil, u.validateTokenAndMergeWithUserInfoErr - } - return u.validatedAndMergedWithUserInfoTokens, nil - }, - } -} - -func NewTestUpstreamOIDCIdentityProviderBuilder() *TestUpstreamOIDCIdentityProviderBuilder { - return &TestUpstreamOIDCIdentityProviderBuilder{} -} - -// Declare a separate type from the production code to ensure that the state param's contents was serialized -// in the format that we expect, with the json keys that we expect, etc. This also ensure that the order of -// the serialized fields is the same, which doesn't really matter expect that we can make simpler equality -// assertions about the redirect URL in this test. -type ExpectedUpstreamStateParamFormat struct { - P string `json:"p"` - U string `json:"u"` - T string `json:"t"` - N string `json:"n"` - C string `json:"c"` - K string `json:"k"` - V string `json:"v"` -} - -type UpstreamStateParamBuilder ExpectedUpstreamStateParamFormat - -func (b UpstreamStateParamBuilder) Build(t *testing.T, stateEncoder *securecookie.SecureCookie) string { - state, err := stateEncoder.Encode("s", b) - require.NoError(t, err) - return state -} - -func (b *UpstreamStateParamBuilder) WithAuthorizeRequestParams(params string) *UpstreamStateParamBuilder { - b.P = params - return b -} - -func (b *UpstreamStateParamBuilder) WithNonce(nonce string) *UpstreamStateParamBuilder { - b.N = nonce - return b -} - -func (b *UpstreamStateParamBuilder) WithCSRF(csrf string) *UpstreamStateParamBuilder { - b.C = csrf - return b -} - -func (b *UpstreamStateParamBuilder) WithPKCE(pkce string) *UpstreamStateParamBuilder { - b.K = pkce - return b -} - -func (b *UpstreamStateParamBuilder) WithUpstreamIDPType(upstreamIDPType string) *UpstreamStateParamBuilder { - b.T = upstreamIDPType - return b -} - -func (b *UpstreamStateParamBuilder) WithStateVersion(version string) *UpstreamStateParamBuilder { - b.V = version - return b -} - -type staticKeySet struct { - publicKey crypto.PublicKey -} - -func newStaticKeySet(publicKey crypto.PublicKey) coreosoidc.KeySet { - return &staticKeySet{publicKey} -} - -func (s *staticKeySet) VerifySignature(_ context.Context, jwt string) ([]byte, error) { - jws, err := jose.ParseSigned(jwt) - if err != nil { - return nil, fmt.Errorf("oidc: malformed jwt: %w", err) - } - return jws.Verify(s.publicKey) -} - -// VerifyECDSAIDToken verifies that the provided idToken was issued via the provided jwtSigningKey. -// It also performs some light validation on the claims, i.e., it makes sure the provided idToken -// has the provided issuer and clientID. -// -// Further validation can be done via callers via the returned coreosoidc.IDToken. -func VerifyECDSAIDToken( - t *testing.T, - issuer, clientID string, - jwtSigningKey *ecdsa.PrivateKey, - idToken string, -) *coreosoidc.IDToken { - t.Helper() - - keySet := newStaticKeySet(jwtSigningKey.Public()) - verifyConfig := coreosoidc.Config{ClientID: clientID, SupportedSigningAlgs: []string{coreosoidc.ES256}} - verifier := coreosoidc.NewVerifier(issuer, keySet, &verifyConfig) - token, err := verifier.Verify(context.Background(), idToken) - require.NoError(t, err) - - return token -} - -func RequireAuthCodeRegexpMatch( - t *testing.T, - actualContent string, - wantRegexp string, - kubeClient *fake.Clientset, - secretsClient v1.SecretInterface, - oauthStore fositestoragei.AllFositeStorage, - wantDownstreamGrantedScopes []string, - wantDownstreamIDTokenSubject string, - wantDownstreamIDTokenUsername string, - wantDownstreamIDTokenGroups []string, - wantDownstreamRequestedScopes []string, - wantDownstreamPKCEChallenge string, - wantDownstreamPKCEChallengeMethod string, - wantDownstreamNonce string, - wantDownstreamClientID string, - wantDownstreamRedirectURI string, - wantCustomSessionData *psession.CustomSessionData, - wantDownstreamAdditionalClaims map[string]interface{}, -) { - t.Helper() - - // Assert that Location header matches regular expression. - regex := regexp.MustCompile(wantRegexp) - submatches := regex.FindStringSubmatch(actualContent) - require.Lenf(t, submatches, 2, "no regexp match in actualContent: %", actualContent) - capturedAuthCode := submatches[1] - - // Authcodes should start with the custom prefix "pin_ac_" to make them identifiable as authcodes when seen by a user out of context. - require.True(t, strings.HasPrefix(capturedAuthCode, "pin_ac_"), "token %q did not have expected prefix 'pin_ac_'", capturedAuthCode) - - // fosite authcodes are in the format `data.signature`, so grab the signature part, which is the lookup key in the storage interface - authcodeDataAndSignature := strings.Split(capturedAuthCode, ".") - require.Len(t, authcodeDataAndSignature, 2) - - // Several Secrets should have been created - expectedNumberOfCreatedSecrets := 2 - if includesOpenIDScope(wantDownstreamGrantedScopes) { - expectedNumberOfCreatedSecrets++ - } - require.Len(t, FilterClientSecretCreateActions(kubeClient.Actions()), expectedNumberOfCreatedSecrets) - - // One authcode should have been stored. - testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) - - storedRequestFromAuthcode, storedSessionFromAuthcode := validateAuthcodeStorage( - t, - oauthStore, - authcodeDataAndSignature[1], // Authcode store key is authcode signature - wantDownstreamGrantedScopes, - wantDownstreamIDTokenSubject, - wantDownstreamIDTokenUsername, - wantDownstreamIDTokenGroups, - wantDownstreamRequestedScopes, - wantDownstreamClientID, - wantDownstreamRedirectURI, - wantCustomSessionData, - wantDownstreamAdditionalClaims, - ) - - // One PKCE should have been stored. - testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: pkce.TypeLabelValue}, 1) - - validatePKCEStorage( - t, - oauthStore, - authcodeDataAndSignature[1], // PKCE store key is authcode signature - storedRequestFromAuthcode, - storedSessionFromAuthcode, - wantDownstreamPKCEChallenge, - wantDownstreamPKCEChallengeMethod, - ) - - // One IDSession should have been stored, if the downstream actually requested the "openid" scope - if includesOpenIDScope(wantDownstreamGrantedScopes) { - testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1) - - validateIDSessionStorage( - t, - oauthStore, - capturedAuthCode, // IDSession store key is full authcode - storedRequestFromAuthcode, - storedSessionFromAuthcode, - wantDownstreamNonce, - ) - } -} - -func includesOpenIDScope(scopes []string) bool { - for _, scope := range scopes { - if scope == "openid" { - return true - } - } - return false -} - -//nolint:funlen -func validateAuthcodeStorage( - t *testing.T, - oauthStore fositestoragei.AllFositeStorage, - storeKey string, - wantDownstreamGrantedScopes []string, - wantDownstreamIDTokenSubject string, - wantDownstreamIDTokenUsername string, - wantDownstreamIDTokenGroups []string, - wantDownstreamRequestedScopes []string, - wantDownstreamClientID string, - wantDownstreamRedirectURI string, - wantCustomSessionData *psession.CustomSessionData, - wantDownstreamAdditionalClaims map[string]interface{}, -) (*fosite.Request, *psession.PinnipedSession) { - t.Helper() - - const ( - authCodeExpirationSeconds = 10 * 60 // Currently, we set our auth code expiration to 10 minutes - timeComparisonFudgeFactor = time.Second * 15 - ) - - // Get the authcode session back from storage so we can require that it was stored correctly. - storedAuthorizeRequestFromAuthcode, err := oauthStore.GetAuthorizeCodeSession(context.Background(), storeKey, nil) - require.NoError(t, err) - - // Check that storage returned the expected concrete data types. - storedRequestFromAuthcode, storedSessionFromAuthcode := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromAuthcode) - - // Check which scopes were granted. - require.ElementsMatch(t, wantDownstreamGrantedScopes, storedRequestFromAuthcode.GetGrantedScopes()) - - // Don't care about the order of requested scopes, as long as they match the expected list. - storedRequestedScopes := storedRequestFromAuthcode.Form["scope"] - require.Len(t, storedRequestedScopes, 1) - require.NotEmpty(t, storedRequestedScopes[0]) - storedRequestedScopesSlice := strings.Split(storedRequestedScopes[0], " ") - require.ElementsMatch(t, storedRequestedScopesSlice, wantDownstreamRequestedScopes) - - // Check all the other fields of the stored request. - require.NotEmpty(t, storedRequestFromAuthcode.ID) - require.Equal(t, wantDownstreamClientID, storedRequestFromAuthcode.Client.GetID()) - require.ElementsMatch(t, wantDownstreamRequestedScopes, storedRequestFromAuthcode.RequestedScope) - require.Nil(t, storedRequestFromAuthcode.RequestedAudience) - require.Empty(t, storedRequestFromAuthcode.GrantedAudience) - require.Equal(t, - url.Values{ - "client_id": []string{wantDownstreamClientID}, - "redirect_uri": []string{wantDownstreamRedirectURI}, - "response_type": []string{"code"}, - "scope": storedRequestedScopes, // already asserted about this actual value above - }, - storedRequestFromAuthcode.Form, - ) - testutil.RequireTimeInDelta(t, time.Now(), storedRequestFromAuthcode.RequestedAt, timeComparisonFudgeFactor) - - // We're not using these fields yet, so confirm that we did not set them (for now). - require.Empty(t, storedSessionFromAuthcode.Fosite.Subject) - require.Empty(t, storedSessionFromAuthcode.Fosite.Username) - require.Empty(t, storedSessionFromAuthcode.Fosite.Headers) - - // The authcode that we are issuing should be good for the length of time that we declare in the fosite config. - testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.Fosite.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor) - require.Len(t, storedSessionFromAuthcode.Fosite.ExpiresAt, 1) - - // Now confirm the ID token claims. - actualClaims := storedSessionFromAuthcode.Fosite.Claims - - // Should always have an azp claim. - require.Equal(t, wantDownstreamClientID, actualClaims.Extra["azp"]) - wantDownstreamIDTokenExtraClaimsCount := 1 // should always have azp claim - - if len(wantDownstreamAdditionalClaims) > 0 { - wantDownstreamIDTokenExtraClaimsCount++ - } - - // Check the user's identity, which are put into the downstream ID token's subject, username and groups claims. - require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject) - if wantDownstreamIDTokenUsername == "" { - require.NotContains(t, actualClaims.Extra, "username") - } else { - wantDownstreamIDTokenExtraClaimsCount++ // should also have username claim - require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"]) - } - if slices.Contains(wantDownstreamGrantedScopes, "groups") { - wantDownstreamIDTokenExtraClaimsCount++ // should also have groups claim - actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] - require.NotNil(t, actualDownstreamIDTokenGroups) - require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups) - } else { - require.Emptyf(t, wantDownstreamIDTokenGroups, "test case did not want the groups scope to be granted, "+ - "but wanted something in the groups claim, which doesn't make sense. please review the test case's expectations.") - actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] - require.Nil(t, actualDownstreamIDTokenGroups) - } - if len(wantDownstreamAdditionalClaims) > 0 { - actualAdditionalClaims, ok := actualClaims.Get("additionalClaims").(map[string]interface{}) - require.True(t, ok, "expected additionalClaims to be a map[string]interface{}") - require.Equal(t, wantDownstreamAdditionalClaims, actualAdditionalClaims) - } else { - require.NotContains(t, actualClaims.Extra, "additionalClaims", "additionalClaims must not be present when there are no wanted additional claims") - } - - // Make sure that we asserted on every extra claim. - require.Len(t, actualClaims.Extra, wantDownstreamIDTokenExtraClaimsCount) - - // Check the rest of the downstream ID token's claims. Fosite wants us to set these (in UTC time). - testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.RequestedAt, timeComparisonFudgeFactor) - testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.AuthTime, timeComparisonFudgeFactor) - requestedAtZone, _ := actualClaims.RequestedAt.Zone() - require.Equal(t, "UTC", requestedAtZone) - authTimeZone, _ := actualClaims.AuthTime.Zone() - require.Equal(t, "UTC", authTimeZone) - - // Fosite will set these fields for us in the token endpoint based on the store session - // information. Therefore, we assert that they are empty because we want the library to do the - // lifting for us. - require.Empty(t, actualClaims.Issuer) - require.Nil(t, actualClaims.Audience) - require.Empty(t, actualClaims.Nonce) - require.Zero(t, actualClaims.ExpiresAt) - require.Zero(t, actualClaims.IssuedAt) - - // These are not needed yet. - require.Empty(t, actualClaims.JTI) - require.Empty(t, actualClaims.CodeHash) - require.Empty(t, actualClaims.AccessTokenHash) - require.Empty(t, actualClaims.AuthenticationContextClassReference) - require.Empty(t, actualClaims.AuthenticationMethodsReferences) - - // Check that the custom Pinniped session data matches. - require.Equal(t, wantCustomSessionData, storedSessionFromAuthcode.Custom) - - return storedRequestFromAuthcode, storedSessionFromAuthcode -} - -func validatePKCEStorage( - t *testing.T, - oauthStore fositestoragei.AllFositeStorage, - storeKey string, - storedRequestFromAuthcode *fosite.Request, - storedSessionFromAuthcode *psession.PinnipedSession, - wantDownstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod string, -) { - t.Helper() - - storedAuthorizeRequestFromPKCE, err := oauthStore.GetPKCERequestSession(context.Background(), storeKey, nil) - require.NoError(t, err) - - // Check that storage returned the expected concrete data types. - storedRequestFromPKCE, storedSessionFromPKCE := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromPKCE) - - // The stored PKCE request should be the same as the stored authcode request. - require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromPKCE.ID) - require.Equal(t, storedSessionFromAuthcode, storedSessionFromPKCE) - - // The stored PKCE request should also contain the PKCE challenge that the downstream sent us. - require.Equal(t, wantDownstreamPKCEChallenge, storedRequestFromPKCE.Form.Get("code_challenge")) - require.Equal(t, wantDownstreamPKCEChallengeMethod, storedRequestFromPKCE.Form.Get("code_challenge_method")) -} - -func validateIDSessionStorage( - t *testing.T, - oauthStore fositestoragei.AllFositeStorage, - storeKey string, - storedRequestFromAuthcode *fosite.Request, - storedSessionFromAuthcode *psession.PinnipedSession, - wantDownstreamNonce string, -) { - t.Helper() - - storedAuthorizeRequestFromIDSession, err := oauthStore.GetOpenIDConnectSession(context.Background(), storeKey, nil) - require.NoError(t, err) - - // Check that storage returned the expected concrete data types. - storedRequestFromIDSession, storedSessionFromIDSession := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromIDSession) - - // The stored IDSession request should be the same as the stored authcode request. - require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromIDSession.ID) - require.Equal(t, storedSessionFromAuthcode, storedSessionFromIDSession) - - // The stored IDSession request should also contain the nonce that the downstream sent us. - require.Equal(t, wantDownstreamNonce, storedRequestFromIDSession.Form.Get("nonce")) -} - -func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *psession.PinnipedSession) { - t.Helper() - - storedRequest, ok := storedAuthorizeRequest.(*fosite.Request) - require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest, &fosite.Request{}) - storedSession, ok := storedAuthorizeRequest.GetSession().(*psession.PinnipedSession) - require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &psession.PinnipedSession{}) - - return storedRequest, storedSession -} - -// FilterClientSecretCreateActions ignores any reads made to get a storage secret corresponding to an OIDCClient, since these -// are normal actions when the request is using a dynamic client's client_id, and we don't need to make assertions -// about these Secrets since they are not related to session storage. -func FilterClientSecretCreateActions(actions []kubetesting.Action) []kubetesting.Action { - filtered := make([]kubetesting.Action, 0, len(actions)) - for _, action := range actions { - if action.Matches("get", "secrets") { - getAction := action.(kubetesting.GetAction) - if strings.HasPrefix(getAction.GetName(), "pinniped-storage-oidc-client-secret-") { - continue // filter out OIDCClient's storage secret reads - } - } - filtered = append(filtered, action) // otherwise include the action - } - return filtered -} diff --git a/internal/testutil/oidctestutil/session_storage_assertions.go b/internal/testutil/oidctestutil/session_storage_assertions.go new file mode 100644 index 000000000..79260d597 --- /dev/null +++ b/internal/testutil/oidctestutil/session_storage_assertions.go @@ -0,0 +1,339 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidctestutil + +import ( + "context" + "net/url" + "regexp" + "strings" + "testing" + "time" + + "github.com/ory/fosite" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes/fake" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + kubetesting "k8s.io/client-go/testing" + "k8s.io/utils/strings/slices" + + "go.pinniped.dev/internal/crud" + "go.pinniped.dev/internal/fositestorage/authorizationcode" + "go.pinniped.dev/internal/fositestorage/openidconnect" + "go.pinniped.dev/internal/fositestorage/pkce" + "go.pinniped.dev/internal/fositestoragei" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil" +) + +func RequireAuthCodeRegexpMatch( + t *testing.T, + actualContent string, + wantRegexp string, + kubeClient *fake.Clientset, + secretsClient v1.SecretInterface, + oauthStore fositestoragei.AllFositeStorage, + wantDownstreamGrantedScopes []string, + wantDownstreamIDTokenSubject string, + wantDownstreamIDTokenUsername string, + wantDownstreamIDTokenGroups []string, + wantDownstreamRequestedScopes []string, + wantDownstreamPKCEChallenge string, + wantDownstreamPKCEChallengeMethod string, + wantDownstreamNonce string, + wantDownstreamClientID string, + wantDownstreamRedirectURI string, + wantCustomSessionData *psession.CustomSessionData, + wantDownstreamAdditionalClaims map[string]interface{}, +) { + t.Helper() + + // Assert that Location header matches regular expression. + regex := regexp.MustCompile(wantRegexp) + submatches := regex.FindStringSubmatch(actualContent) + require.Lenf(t, submatches, 2, "no regexp match in actualContent: %", actualContent) + capturedAuthCode := submatches[1] + + // Authcodes should start with the custom prefix "pin_ac_" to make them identifiable as authcodes when seen by a user out of context. + require.True(t, strings.HasPrefix(capturedAuthCode, "pin_ac_"), "token %q did not have expected prefix 'pin_ac_'", capturedAuthCode) + + // fosite authcodes are in the format `data.signature`, so grab the signature part, which is the lookup key in the storage interface + authcodeDataAndSignature := strings.Split(capturedAuthCode, ".") + require.Len(t, authcodeDataAndSignature, 2) + + // Several Secrets should have been created + expectedNumberOfCreatedSecrets := 2 + if includesOpenIDScope(wantDownstreamGrantedScopes) { + expectedNumberOfCreatedSecrets++ + } + require.Len(t, FilterClientSecretCreateActions(kubeClient.Actions()), expectedNumberOfCreatedSecrets) + + // One authcode should have been stored. + testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) + + storedRequestFromAuthcode, storedSessionFromAuthcode := validateAuthcodeStorage( + t, + oauthStore, + authcodeDataAndSignature[1], // Authcode store key is authcode signature + wantDownstreamGrantedScopes, + wantDownstreamIDTokenSubject, + wantDownstreamIDTokenUsername, + wantDownstreamIDTokenGroups, + wantDownstreamRequestedScopes, + wantDownstreamClientID, + wantDownstreamRedirectURI, + wantCustomSessionData, + wantDownstreamAdditionalClaims, + ) + + // One PKCE should have been stored. + testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: pkce.TypeLabelValue}, 1) + + validatePKCEStorage( + t, + oauthStore, + authcodeDataAndSignature[1], // PKCE store key is authcode signature + storedRequestFromAuthcode, + storedSessionFromAuthcode, + wantDownstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod, + ) + + // One IDSession should have been stored, if the downstream actually requested the "openid" scope + if includesOpenIDScope(wantDownstreamGrantedScopes) { + testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1) + + validateIDSessionStorage( + t, + oauthStore, + capturedAuthCode, // IDSession store key is full authcode + storedRequestFromAuthcode, + storedSessionFromAuthcode, + wantDownstreamNonce, + ) + } +} + +func includesOpenIDScope(scopes []string) bool { + for _, scope := range scopes { + if scope == "openid" { + return true + } + } + return false +} + +//nolint:funlen +func validateAuthcodeStorage( + t *testing.T, + oauthStore fositestoragei.AllFositeStorage, + storeKey string, + wantDownstreamGrantedScopes []string, + wantDownstreamIDTokenSubject string, + wantDownstreamIDTokenUsername string, + wantDownstreamIDTokenGroups []string, + wantDownstreamRequestedScopes []string, + wantDownstreamClientID string, + wantDownstreamRedirectURI string, + wantCustomSessionData *psession.CustomSessionData, + wantDownstreamAdditionalClaims map[string]interface{}, +) (*fosite.Request, *psession.PinnipedSession) { + t.Helper() + + const ( + authCodeExpirationSeconds = 10 * 60 // Currently, we set our auth code expiration to 10 minutes + timeComparisonFudgeFactor = time.Second * 15 + ) + + // Get the authcode session back from storage so we can require that it was stored correctly. + storedAuthorizeRequestFromAuthcode, err := oauthStore.GetAuthorizeCodeSession(context.Background(), storeKey, nil) + require.NoError(t, err) + + // Check that storage returned the expected concrete data types. + storedRequestFromAuthcode, storedSessionFromAuthcode := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromAuthcode) + + // Check which scopes were granted. + require.ElementsMatch(t, wantDownstreamGrantedScopes, storedRequestFromAuthcode.GetGrantedScopes()) + + // Don't care about the order of requested scopes, as long as they match the expected list. + storedRequestedScopes := storedRequestFromAuthcode.Form["scope"] + require.Len(t, storedRequestedScopes, 1) + require.NotEmpty(t, storedRequestedScopes[0]) + storedRequestedScopesSlice := strings.Split(storedRequestedScopes[0], " ") + require.ElementsMatch(t, storedRequestedScopesSlice, wantDownstreamRequestedScopes) + + // Check all the other fields of the stored request. + require.NotEmpty(t, storedRequestFromAuthcode.ID) + require.Equal(t, wantDownstreamClientID, storedRequestFromAuthcode.Client.GetID()) + require.ElementsMatch(t, wantDownstreamRequestedScopes, storedRequestFromAuthcode.RequestedScope) + require.Nil(t, storedRequestFromAuthcode.RequestedAudience) + require.Empty(t, storedRequestFromAuthcode.GrantedAudience) + require.Equal(t, + url.Values{ + "client_id": []string{wantDownstreamClientID}, + "redirect_uri": []string{wantDownstreamRedirectURI}, + "response_type": []string{"code"}, + "scope": storedRequestedScopes, // already asserted about this actual value above + }, + storedRequestFromAuthcode.Form, + ) + testutil.RequireTimeInDelta(t, time.Now(), storedRequestFromAuthcode.RequestedAt, timeComparisonFudgeFactor) + + // We're not using these fields yet, so confirm that we did not set them (for now). + require.Empty(t, storedSessionFromAuthcode.Fosite.Subject) + require.Empty(t, storedSessionFromAuthcode.Fosite.Username) + require.Empty(t, storedSessionFromAuthcode.Fosite.Headers) + + // The authcode that we are issuing should be good for the length of time that we declare in the fosite config. + testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.Fosite.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor) + require.Len(t, storedSessionFromAuthcode.Fosite.ExpiresAt, 1) + + // Now confirm the ID token claims. + actualClaims := storedSessionFromAuthcode.Fosite.Claims + + // Should always have an azp claim. + require.Equal(t, wantDownstreamClientID, actualClaims.Extra["azp"]) + wantDownstreamIDTokenExtraClaimsCount := 1 // should always have azp claim + + if len(wantDownstreamAdditionalClaims) > 0 { + wantDownstreamIDTokenExtraClaimsCount++ + } + + // Check the user's identity, which are put into the downstream ID token's subject, username and groups claims. + require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject) + if wantDownstreamIDTokenUsername == "" { + require.NotContains(t, actualClaims.Extra, "username") + } else { + wantDownstreamIDTokenExtraClaimsCount++ // should also have username claim + require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"]) + } + if slices.Contains(wantDownstreamGrantedScopes, "groups") { + wantDownstreamIDTokenExtraClaimsCount++ // should also have groups claim + actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] + require.NotNil(t, actualDownstreamIDTokenGroups) + require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups) + } else { + require.Emptyf(t, wantDownstreamIDTokenGroups, "test case did not want the groups scope to be granted, "+ + "but wanted something in the groups claim, which doesn't make sense. please review the test case's expectations.") + actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] + require.Nil(t, actualDownstreamIDTokenGroups) + } + if len(wantDownstreamAdditionalClaims) > 0 { + actualAdditionalClaims, ok := actualClaims.Get("additionalClaims").(map[string]interface{}) + require.True(t, ok, "expected additionalClaims to be a map[string]interface{}") + require.Equal(t, wantDownstreamAdditionalClaims, actualAdditionalClaims) + } else { + require.NotContains(t, actualClaims.Extra, "additionalClaims", "additionalClaims must not be present when there are no wanted additional claims") + } + + // Make sure that we asserted on every extra claim. + require.Len(t, actualClaims.Extra, wantDownstreamIDTokenExtraClaimsCount) + + // Check the rest of the downstream ID token's claims. Fosite wants us to set these (in UTC time). + testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.RequestedAt, timeComparisonFudgeFactor) + testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.AuthTime, timeComparisonFudgeFactor) + requestedAtZone, _ := actualClaims.RequestedAt.Zone() + require.Equal(t, "UTC", requestedAtZone) + authTimeZone, _ := actualClaims.AuthTime.Zone() + require.Equal(t, "UTC", authTimeZone) + + // Fosite will set these fields for us in the token endpoint based on the store session + // information. Therefore, we assert that they are empty because we want the library to do the + // lifting for us. + require.Empty(t, actualClaims.Issuer) + require.Nil(t, actualClaims.Audience) + require.Empty(t, actualClaims.Nonce) + require.Zero(t, actualClaims.ExpiresAt) + require.Zero(t, actualClaims.IssuedAt) + + // These are not needed yet. + require.Empty(t, actualClaims.JTI) + require.Empty(t, actualClaims.CodeHash) + require.Empty(t, actualClaims.AccessTokenHash) + require.Empty(t, actualClaims.AuthenticationContextClassReference) + require.Empty(t, actualClaims.AuthenticationMethodsReferences) + + // Check that the custom Pinniped session data matches. + require.Equal(t, wantCustomSessionData, storedSessionFromAuthcode.Custom) + + return storedRequestFromAuthcode, storedSessionFromAuthcode +} + +func validatePKCEStorage( + t *testing.T, + oauthStore fositestoragei.AllFositeStorage, + storeKey string, + storedRequestFromAuthcode *fosite.Request, + storedSessionFromAuthcode *psession.PinnipedSession, + wantDownstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod string, +) { + t.Helper() + + storedAuthorizeRequestFromPKCE, err := oauthStore.GetPKCERequestSession(context.Background(), storeKey, nil) + require.NoError(t, err) + + // Check that storage returned the expected concrete data types. + storedRequestFromPKCE, storedSessionFromPKCE := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromPKCE) + + // The stored PKCE request should be the same as the stored authcode request. + require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromPKCE.ID) + require.Equal(t, storedSessionFromAuthcode, storedSessionFromPKCE) + + // The stored PKCE request should also contain the PKCE challenge that the downstream sent us. + require.Equal(t, wantDownstreamPKCEChallenge, storedRequestFromPKCE.Form.Get("code_challenge")) + require.Equal(t, wantDownstreamPKCEChallengeMethod, storedRequestFromPKCE.Form.Get("code_challenge_method")) +} + +func validateIDSessionStorage( + t *testing.T, + oauthStore fositestoragei.AllFositeStorage, + storeKey string, + storedRequestFromAuthcode *fosite.Request, + storedSessionFromAuthcode *psession.PinnipedSession, + wantDownstreamNonce string, +) { + t.Helper() + + storedAuthorizeRequestFromIDSession, err := oauthStore.GetOpenIDConnectSession(context.Background(), storeKey, nil) + require.NoError(t, err) + + // Check that storage returned the expected concrete data types. + storedRequestFromIDSession, storedSessionFromIDSession := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromIDSession) + + // The stored IDSession request should be the same as the stored authcode request. + require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromIDSession.ID) + require.Equal(t, storedSessionFromAuthcode, storedSessionFromIDSession) + + // The stored IDSession request should also contain the nonce that the downstream sent us. + require.Equal(t, wantDownstreamNonce, storedRequestFromIDSession.Form.Get("nonce")) +} + +func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *psession.PinnipedSession) { + t.Helper() + + storedRequest, ok := storedAuthorizeRequest.(*fosite.Request) + require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest, &fosite.Request{}) + storedSession, ok := storedAuthorizeRequest.GetSession().(*psession.PinnipedSession) + require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &psession.PinnipedSession{}) + + return storedRequest, storedSession +} + +// FilterClientSecretCreateActions ignores any reads made to get a storage secret corresponding to an OIDCClient, since these +// are normal actions when the request is using a dynamic client's client_id, and we don't need to make assertions +// about these Secrets since they are not related to session storage. +func FilterClientSecretCreateActions(actions []kubetesting.Action) []kubetesting.Action { + filtered := make([]kubetesting.Action, 0, len(actions)) + for _, action := range actions { + if action.Matches("get", "secrets") { + getAction := action.(kubetesting.GetAction) + if strings.HasPrefix(getAction.GetName(), "pinniped-storage-oidc-client-secret-") { + continue // filter out OIDCClient's storage secret reads + } + } + filtered = append(filtered, action) // otherwise include the action + } + return filtered +} diff --git a/internal/testutil/oidctestutil/testldapprovider.go b/internal/testutil/oidctestutil/testldapprovider.go new file mode 100644 index 000000000..55a4eae52 --- /dev/null +++ b/internal/testutil/oidctestutil/testldapprovider.go @@ -0,0 +1,152 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidctestutil + +import ( + "context" + "net/url" + + "k8s.io/apimachinery/pkg/types" + + "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/idtransform" +) + +func NewTestUpstreamLDAPIdentityProviderBuilder() *TestUpstreamLDAPIdentityProviderBuilder { + return &TestUpstreamLDAPIdentityProviderBuilder{} +} + +type TestUpstreamLDAPIdentityProviderBuilder struct { + name string + resourceUID types.UID + url *url.URL + authenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) + performRefreshErr error + performRefreshGroups []string + displayNameForFederationDomain string + transformsForFederationDomain *idtransform.TransformationPipeline +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithName(name string) *TestUpstreamLDAPIdentityProviderBuilder { + t.name = name + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithResourceUID(uid types.UID) *TestUpstreamLDAPIdentityProviderBuilder { + t.resourceUID = uid + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithURL(url *url.URL) *TestUpstreamLDAPIdentityProviderBuilder { + t.url = url + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithAuthenticateFunc(f func(ctx context.Context, username, password string) (*authenticators.Response, bool, error)) *TestUpstreamLDAPIdentityProviderBuilder { + t.authenticateFunc = f + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshErr(err error) *TestUpstreamLDAPIdentityProviderBuilder { + t.performRefreshErr = err + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithPerformRefreshGroups(groups []string) *TestUpstreamLDAPIdentityProviderBuilder { + t.performRefreshGroups = groups + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithDisplayNameForFederationDomain(displayName string) *TestUpstreamLDAPIdentityProviderBuilder { + t.displayNameForFederationDomain = displayName + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) WithTransformsForFederationDomain(transforms *idtransform.TransformationPipeline) *TestUpstreamLDAPIdentityProviderBuilder { + t.transformsForFederationDomain = transforms + return t +} + +func (t *TestUpstreamLDAPIdentityProviderBuilder) Build() *TestUpstreamLDAPIdentityProvider { + if t.displayNameForFederationDomain == "" { + // default it to the CR name + t.displayNameForFederationDomain = t.name + } + if t.transformsForFederationDomain == nil { + // default to an empty pipeline + t.transformsForFederationDomain = idtransform.NewTransformationPipeline() + } + return &TestUpstreamLDAPIdentityProvider{ + Name: t.name, + ResourceUID: t.resourceUID, + URL: t.url, + AuthenticateFunc: t.authenticateFunc, + PerformRefreshErr: t.performRefreshErr, + PerformRefreshGroups: t.performRefreshGroups, + DisplayNameForFederationDomain: t.displayNameForFederationDomain, + TransformsForFederationDomain: t.transformsForFederationDomain, + } +} + +type TestUpstreamLDAPIdentityProvider struct { + Name string + ResourceUID types.UID + URL *url.URL + AuthenticateFunc func(ctx context.Context, username, password string) (*authenticators.Response, bool, error) + PerformRefreshErr error + PerformRefreshGroups []string + DisplayNameForFederationDomain string + TransformsForFederationDomain *idtransform.TransformationPipeline + + // Fields for tracking actual calls make to mock functions. + performRefreshCallCount int + performRefreshArgs []*PerformRefreshArgs +} + +var _ upstreamprovider.UpstreamLDAPIdentityProviderI = &TestUpstreamLDAPIdentityProvider{} + +func (u *TestUpstreamLDAPIdentityProvider) GetResourceUID() types.UID { + return u.ResourceUID +} + +func (u *TestUpstreamLDAPIdentityProvider) GetName() string { + return u.Name +} + +func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string, _skipGroups bool) (*authenticators.Response, bool, error) { + return u.AuthenticateFunc(ctx, username, password) +} + +func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL { + return u.URL +} + +func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, _idpDisplayName string) ([]string, error) { + if u.performRefreshArgs == nil { + u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + } + u.performRefreshCallCount++ + u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{ + Ctx: ctx, + DN: storedRefreshAttributes.DN, + ExpectedUsername: storedRefreshAttributes.Username, + ExpectedSubject: storedRefreshAttributes.Subject, + }) + if u.PerformRefreshErr != nil { + return nil, u.PerformRefreshErr + } + return u.PerformRefreshGroups, 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] +} diff --git a/internal/testutil/oidctestutil/testoidcprovider.go b/internal/testutil/oidctestutil/testoidcprovider.go new file mode 100644 index 000000000..489f6304c --- /dev/null +++ b/internal/testutil/oidctestutil/testoidcprovider.go @@ -0,0 +1,530 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidctestutil + +import ( + "context" + "net/url" + + "golang.org/x/oauth2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/idtransform" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/oidctypes" + oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce" +) + +// ExchangeAuthcodeAndValidateTokenArgs is used to spy on calls to +// TestUpstreamOIDCIdentityProvider.ExchangeAuthcodeAndValidateTokensFunc(). +type ExchangeAuthcodeAndValidateTokenArgs struct { + Ctx context.Context + Authcode string + PKCECodeVerifier oidcpkce.Code + ExpectedIDTokenNonce nonce.Nonce + RedirectURI string +} + +// PasswordCredentialsGrantAndValidateTokensArgs is used to spy on calls to +// TestUpstreamOIDCIdentityProvider.PasswordCredentialsGrantAndValidateTokensFunc(). +type PasswordCredentialsGrantAndValidateTokensArgs struct { + Ctx context.Context + Username string + Password string +} + +// PerformRefreshArgs is used to spy on calls to +// TestUpstreamOIDCIdentityProvider.PerformRefreshFunc(). +type PerformRefreshArgs struct { + Ctx context.Context + RefreshToken string + DN string + ExpectedUsername string + ExpectedSubject string +} + +// RevokeTokenArgs is used to spy on calls to +// TestUpstreamOIDCIdentityProvider.RevokeTokenArgsFunc(). +type RevokeTokenArgs struct { + Ctx context.Context + Token string + TokenType upstreamprovider.RevocableTokenType +} + +// ValidateTokenAndMergeWithUserInfoArgs is used to spy on calls to +// TestUpstreamOIDCIdentityProvider.ValidateTokenAndMergeWithUserInfoFunc(). +type ValidateTokenAndMergeWithUserInfoArgs struct { + Ctx context.Context + Tok *oauth2.Token + ExpectedIDTokenNonce nonce.Nonce + RequireIDToken bool + RequireUserInfo bool +} + +type TestUpstreamOIDCIdentityProvider struct { + Name string + ClientID string + ResourceUID types.UID + AuthorizationURL url.URL + UserInfoURL bool + RevocationURL *url.URL + UsernameClaim string + GroupsClaim string + Scopes []string + AdditionalAuthcodeParams map[string]string + AdditionalClaimMappings map[string]string + AllowPasswordGrant bool + DisplayNameForFederationDomain string + TransformsForFederationDomain *idtransform.TransformationPipeline + + ExchangeAuthcodeAndValidateTokensFunc func( + ctx context.Context, + authcode string, + pkceCodeVerifier oidcpkce.Code, + expectedIDTokenNonce nonce.Nonce, + ) (*oidctypes.Token, error) + + PasswordCredentialsGrantAndValidateTokensFunc func( + ctx context.Context, + username string, + password string, + ) (*oidctypes.Token, error) + + PerformRefreshFunc func(ctx context.Context, refreshToken string) (*oauth2.Token, error) + + RevokeTokenFunc func(ctx context.Context, refreshToken string, tokenType upstreamprovider.RevocableTokenType) error + + ValidateTokenAndMergeWithUserInfoFunc func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) + + // Fields for tracking actual calls make to mock functions. + exchangeAuthcodeAndValidateTokensCallCount int + exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs + passwordCredentialsGrantAndValidateTokensCallCount int + passwordCredentialsGrantAndValidateTokensArgs []*PasswordCredentialsGrantAndValidateTokensArgs + performRefreshCallCount int + performRefreshArgs []*PerformRefreshArgs + revokeTokenCallCount int + revokeTokenArgs []*RevokeTokenArgs + validateTokenAndMergeWithUserInfoCallCount int + validateTokenAndMergeWithUserInfoArgs []*ValidateTokenAndMergeWithUserInfoArgs +} + +var _ upstreamprovider.UpstreamOIDCIdentityProviderI = &TestUpstreamOIDCIdentityProvider{} + +func (u *TestUpstreamOIDCIdentityProvider) GetResourceUID() types.UID { + return u.ResourceUID +} + +func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalAuthcodeParams() map[string]string { + return u.AdditionalAuthcodeParams +} + +func (u *TestUpstreamOIDCIdentityProvider) GetAdditionalClaimMappings() map[string]string { + return u.AdditionalClaimMappings +} + +func (u *TestUpstreamOIDCIdentityProvider) GetName() string { + return u.Name +} + +func (u *TestUpstreamOIDCIdentityProvider) GetClientID() string { + return u.ClientID +} + +func (u *TestUpstreamOIDCIdentityProvider) GetAuthorizationURL() *url.URL { + return &u.AuthorizationURL +} + +func (u *TestUpstreamOIDCIdentityProvider) HasUserInfoURL() bool { + return u.UserInfoURL +} + +func (u *TestUpstreamOIDCIdentityProvider) GetRevocationURL() *url.URL { + return u.RevocationURL +} + +func (u *TestUpstreamOIDCIdentityProvider) GetScopes() []string { + return u.Scopes +} + +func (u *TestUpstreamOIDCIdentityProvider) GetUsernameClaim() string { + return u.UsernameClaim +} + +func (u *TestUpstreamOIDCIdentityProvider) GetGroupsClaim() string { + return u.GroupsClaim +} + +func (u *TestUpstreamOIDCIdentityProvider) AllowsPasswordGrant() bool { + return u.AllowPasswordGrant +} + +func (u *TestUpstreamOIDCIdentityProvider) PasswordCredentialsGrantAndValidateTokens(ctx context.Context, username, password string) (*oidctypes.Token, error) { + u.passwordCredentialsGrantAndValidateTokensCallCount++ + u.passwordCredentialsGrantAndValidateTokensArgs = append(u.passwordCredentialsGrantAndValidateTokensArgs, &PasswordCredentialsGrantAndValidateTokensArgs{ + Ctx: ctx, + Username: username, + Password: password, + }) + return u.PasswordCredentialsGrantAndValidateTokensFunc(ctx, username, password) +} + +func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokens( + ctx context.Context, + authcode string, + pkceCodeVerifier oidcpkce.Code, + expectedIDTokenNonce nonce.Nonce, + redirectURI string, +) (*oidctypes.Token, error) { + if u.exchangeAuthcodeAndValidateTokensArgs == nil { + u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) + } + u.exchangeAuthcodeAndValidateTokensCallCount++ + u.exchangeAuthcodeAndValidateTokensArgs = append(u.exchangeAuthcodeAndValidateTokensArgs, &ExchangeAuthcodeAndValidateTokenArgs{ + Ctx: ctx, + Authcode: authcode, + PKCECodeVerifier: pkceCodeVerifier, + ExpectedIDTokenNonce: expectedIDTokenNonce, + RedirectURI: redirectURI, + }) + return u.ExchangeAuthcodeAndValidateTokensFunc(ctx, authcode, pkceCodeVerifier, expectedIDTokenNonce) +} + +func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensCallCount() int { + return u.exchangeAuthcodeAndValidateTokensCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensArgs(call int) *ExchangeAuthcodeAndValidateTokenArgs { + if u.exchangeAuthcodeAndValidateTokensArgs == nil { + u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) + } + return u.exchangeAuthcodeAndValidateTokensArgs[call] +} + +func (u *TestUpstreamOIDCIdentityProvider) PasswordCredentialsGrantAndValidateTokensCallCount() int { + return u.passwordCredentialsGrantAndValidateTokensCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) PasswordCredentialsGrantAndValidateTokensArgs(call int) *PasswordCredentialsGrantAndValidateTokensArgs { + if u.passwordCredentialsGrantAndValidateTokensArgs == nil { + u.passwordCredentialsGrantAndValidateTokensArgs = make([]*PasswordCredentialsGrantAndValidateTokensArgs, 0) + } + return u.passwordCredentialsGrantAndValidateTokensArgs[call] +} + +func (u *TestUpstreamOIDCIdentityProvider) PerformRefresh(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + if u.performRefreshArgs == nil { + u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + } + u.performRefreshCallCount++ + u.performRefreshArgs = append(u.performRefreshArgs, &PerformRefreshArgs{ + Ctx: ctx, + RefreshToken: refreshToken, + }) + return u.PerformRefreshFunc(ctx, refreshToken) +} + +func (u *TestUpstreamOIDCIdentityProvider) RevokeToken(ctx context.Context, token string, tokenType upstreamprovider.RevocableTokenType) error { + if u.revokeTokenArgs == nil { + u.revokeTokenArgs = make([]*RevokeTokenArgs, 0) + } + u.revokeTokenCallCount++ + u.revokeTokenArgs = append(u.revokeTokenArgs, &RevokeTokenArgs{ + Ctx: ctx, + Token: token, + TokenType: tokenType, + }) + return u.RevokeTokenFunc(ctx, token, tokenType) +} + +func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshCallCount() int { + return u.performRefreshCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) PerformRefreshArgs(call int) *PerformRefreshArgs { + if u.performRefreshArgs == nil { + u.performRefreshArgs = make([]*PerformRefreshArgs, 0) + } + return u.performRefreshArgs[call] +} + +func (u *TestUpstreamOIDCIdentityProvider) RevokeTokenCallCount() int { + return u.revokeTokenCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) RevokeTokenArgs(call int) *RevokeTokenArgs { + if u.revokeTokenArgs == nil { + u.revokeTokenArgs = make([]*RevokeTokenArgs, 0) + } + return u.revokeTokenArgs[call] +} + +func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfo(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce, requireIDToken bool, requireUserInfo bool) (*oidctypes.Token, error) { + if u.validateTokenAndMergeWithUserInfoArgs == nil { + u.validateTokenAndMergeWithUserInfoArgs = make([]*ValidateTokenAndMergeWithUserInfoArgs, 0) + } + u.validateTokenAndMergeWithUserInfoCallCount++ + u.validateTokenAndMergeWithUserInfoArgs = append(u.validateTokenAndMergeWithUserInfoArgs, &ValidateTokenAndMergeWithUserInfoArgs{ + Ctx: ctx, + Tok: tok, + ExpectedIDTokenNonce: expectedIDTokenNonce, + RequireIDToken: requireIDToken, + RequireUserInfo: requireUserInfo, + }) + return u.ValidateTokenAndMergeWithUserInfoFunc(ctx, tok, expectedIDTokenNonce) +} + +func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfoCallCount() int { + return u.validateTokenAndMergeWithUserInfoCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) ValidateTokenAndMergeWithUserInfoArgs(call int) *ValidateTokenAndMergeWithUserInfoArgs { + if u.validateTokenAndMergeWithUserInfoArgs == nil { + u.validateTokenAndMergeWithUserInfoArgs = make([]*ValidateTokenAndMergeWithUserInfoArgs, 0) + } + return u.validateTokenAndMergeWithUserInfoArgs[call] +} + +type TestUpstreamOIDCIdentityProviderBuilder struct { + name string + resourceUID types.UID + clientID string + scopes []string + idToken map[string]interface{} + refreshToken *oidctypes.RefreshToken + accessToken *oidctypes.AccessToken + usernameClaim string + groupsClaim string + refreshedTokens *oauth2.Token + validatedAndMergedWithUserInfoTokens *oidctypes.Token + authorizationURL url.URL + hasUserInfoURL bool + additionalAuthcodeParams map[string]string + additionalClaimMappings map[string]string + allowPasswordGrant bool + authcodeExchangeErr error + passwordGrantErr error + performRefreshErr error + revokeTokenErr error + validateTokenAndMergeWithUserInfoErr error + displayNameForFederationDomain string + transformsForFederationDomain *idtransform.TransformationPipeline +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithName(value string) *TestUpstreamOIDCIdentityProviderBuilder { + u.name = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithResourceUID(value types.UID) *TestUpstreamOIDCIdentityProviderBuilder { + u.resourceUID = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithClientID(value string) *TestUpstreamOIDCIdentityProviderBuilder { + u.clientID = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAuthorizationURL(value url.URL) *TestUpstreamOIDCIdentityProviderBuilder { + u.authorizationURL = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUserInfoURL() *TestUpstreamOIDCIdentityProviderBuilder { + u.hasUserInfoURL = true + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutUserInfoURL() *TestUpstreamOIDCIdentityProviderBuilder { + u.hasUserInfoURL = false + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAllowPasswordGrant(value bool) *TestUpstreamOIDCIdentityProviderBuilder { + u.allowPasswordGrant = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithScopes(values []string) *TestUpstreamOIDCIdentityProviderBuilder { + u.scopes = values + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUsernameClaim(value string) *TestUpstreamOIDCIdentityProviderBuilder { + u.usernameClaim = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutUsernameClaim() *TestUpstreamOIDCIdentityProviderBuilder { + u.usernameClaim = "" + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithGroupsClaim(value string) *TestUpstreamOIDCIdentityProviderBuilder { + u.groupsClaim = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutGroupsClaim() *TestUpstreamOIDCIdentityProviderBuilder { + u.groupsClaim = "" + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithIDTokenClaim(name string, value interface{}) *TestUpstreamOIDCIdentityProviderBuilder { + if u.idToken == nil { + u.idToken = map[string]interface{}{} + } + u.idToken[name] = value + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutIDTokenClaim(claim string) *TestUpstreamOIDCIdentityProviderBuilder { + delete(u.idToken, claim) + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalAuthcodeParams(params map[string]string) *TestUpstreamOIDCIdentityProviderBuilder { + u.additionalAuthcodeParams = params + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAdditionalClaimMappings(m map[string]string) *TestUpstreamOIDCIdentityProviderBuilder { + u.additionalClaimMappings = m + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshToken(token string) *TestUpstreamOIDCIdentityProviderBuilder { + u.refreshToken = &oidctypes.RefreshToken{Token: token} + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithEmptyRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder { + u.refreshToken = &oidctypes.RefreshToken{Token: ""} + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutRefreshToken() *TestUpstreamOIDCIdentityProviderBuilder { + u.refreshToken = nil + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithAccessToken(token string, expiry metav1.Time) *TestUpstreamOIDCIdentityProviderBuilder { + u.accessToken = &oidctypes.AccessToken{Token: token, Expiry: expiry} + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithEmptyAccessToken() *TestUpstreamOIDCIdentityProviderBuilder { + u.accessToken = &oidctypes.AccessToken{Token: ""} + return u +} +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithoutAccessToken() *TestUpstreamOIDCIdentityProviderBuilder { + u.accessToken = nil + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithUpstreamAuthcodeExchangeError(err error) *TestUpstreamOIDCIdentityProviderBuilder { + u.authcodeExchangeErr = err + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPasswordGrantError(err error) *TestUpstreamOIDCIdentityProviderBuilder { + u.passwordGrantErr = err + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRefreshedTokens(tokens *oauth2.Token) *TestUpstreamOIDCIdentityProviderBuilder { + u.refreshedTokens = tokens + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithPerformRefreshError(err error) *TestUpstreamOIDCIdentityProviderBuilder { + u.performRefreshErr = err + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidatedAndMergedWithUserInfoTokens(tokens *oidctypes.Token) *TestUpstreamOIDCIdentityProviderBuilder { + u.validatedAndMergedWithUserInfoTokens = tokens + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithValidateTokenAndMergeWithUserInfoError(err error) *TestUpstreamOIDCIdentityProviderBuilder { + u.validateTokenAndMergeWithUserInfoErr = err + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithRevokeTokenError(err error) *TestUpstreamOIDCIdentityProviderBuilder { + u.revokeTokenErr = err + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithDisplayNameForFederationDomain(displayName string) *TestUpstreamOIDCIdentityProviderBuilder { + u.displayNameForFederationDomain = displayName + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) WithTransformsForFederationDomain(transforms *idtransform.TransformationPipeline) *TestUpstreamOIDCIdentityProviderBuilder { + u.transformsForFederationDomain = transforms + return u +} + +func (u *TestUpstreamOIDCIdentityProviderBuilder) Build() *TestUpstreamOIDCIdentityProvider { + if u.displayNameForFederationDomain == "" { + // default it to the CR name + u.displayNameForFederationDomain = u.name + } + if u.transformsForFederationDomain == nil { + // default to an empty pipeline + u.transformsForFederationDomain = idtransform.NewTransformationPipeline() + } + + return &TestUpstreamOIDCIdentityProvider{ + Name: u.name, + ClientID: u.clientID, + ResourceUID: u.resourceUID, + UsernameClaim: u.usernameClaim, + GroupsClaim: u.groupsClaim, + Scopes: u.scopes, + AllowPasswordGrant: u.allowPasswordGrant, + AuthorizationURL: u.authorizationURL, + UserInfoURL: u.hasUserInfoURL, + AdditionalAuthcodeParams: u.additionalAuthcodeParams, + AdditionalClaimMappings: u.additionalClaimMappings, + DisplayNameForFederationDomain: u.displayNameForFederationDomain, + TransformsForFederationDomain: u.transformsForFederationDomain, + ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier oidcpkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { + if u.authcodeExchangeErr != nil { + return nil, u.authcodeExchangeErr + } + return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken, AccessToken: u.accessToken}, nil + }, + PasswordCredentialsGrantAndValidateTokensFunc: func(ctx context.Context, username, password string) (*oidctypes.Token, error) { + if u.passwordGrantErr != nil { + return nil, u.passwordGrantErr + } + return &oidctypes.Token{IDToken: &oidctypes.IDToken{Claims: u.idToken}, RefreshToken: u.refreshToken, AccessToken: u.accessToken}, nil + }, + PerformRefreshFunc: func(ctx context.Context, refreshToken string) (*oauth2.Token, error) { + if u.performRefreshErr != nil { + return nil, u.performRefreshErr + } + return u.refreshedTokens, nil + }, + RevokeTokenFunc: func(ctx context.Context, refreshToken string, tokenType upstreamprovider.RevocableTokenType) error { + return u.revokeTokenErr + }, + ValidateTokenAndMergeWithUserInfoFunc: func(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) { + if u.validateTokenAndMergeWithUserInfoErr != nil { + return nil, u.validateTokenAndMergeWithUserInfoErr + } + return u.validatedAndMergedWithUserInfoTokens, nil + }, + } +} + +func NewTestUpstreamOIDCIdentityProviderBuilder() *TestUpstreamOIDCIdentityProviderBuilder { + return &TestUpstreamOIDCIdentityProviderBuilder{} +} diff --git a/internal/testutil/oidctestutil/verify_id_token.go b/internal/testutil/oidctestutil/verify_id_token.go new file mode 100644 index 000000000..5eff6a8b2 --- /dev/null +++ b/internal/testutil/oidctestutil/verify_id_token.go @@ -0,0 +1,54 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidctestutil + +import ( + "context" + "crypto" + "crypto/ecdsa" + "fmt" + "testing" + + coreosoidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-jose/go-jose/v3" + "github.com/stretchr/testify/require" +) + +type staticKeySet struct { + publicKey crypto.PublicKey +} + +func newStaticKeySet(publicKey crypto.PublicKey) coreosoidc.KeySet { + return &staticKeySet{publicKey} +} + +func (s *staticKeySet) VerifySignature(_ context.Context, jwt string) ([]byte, error) { + jws, err := jose.ParseSigned(jwt) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt: %w", err) + } + return jws.Verify(s.publicKey) +} + +// VerifyECDSAIDToken verifies that the provided idToken was issued via the provided jwtSigningKey. +// It also performs some light validation on the claims, i.e., it makes sure the provided idToken +// has the provided issuer and clientID. +// +// Further validation can be done via callers via the returned coreosoidc.IDToken. +func VerifyECDSAIDToken( + t *testing.T, + issuer, clientID string, + jwtSigningKey *ecdsa.PrivateKey, + idToken string, +) *coreosoidc.IDToken { + t.Helper() + + keySet := newStaticKeySet(jwtSigningKey.Public()) + verifyConfig := coreosoidc.Config{ClientID: clientID, SupportedSigningAlgs: []string{coreosoidc.ES256}} + verifier := coreosoidc.NewVerifier(issuer, keySet, &verifyConfig) + token, err := verifier.Verify(context.Background(), idToken) + require.NoError(t, err) + + return token +} diff --git a/internal/testutil/testidplister/testidplister.go b/internal/testutil/testidplister/testidplister.go new file mode 100644 index 000000000..ee0f9ae82 --- /dev/null +++ b/internal/testutil/testidplister/testidplister.go @@ -0,0 +1,400 @@ +// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package testidplister + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/federationdomain/dynamicupstreamprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedldap" + "go.pinniped.dev/internal/federationdomain/resolvedprovider/resolvedoidc" + "go.pinniped.dev/internal/federationdomain/upstreamprovider" + "go.pinniped.dev/internal/psession" + "go.pinniped.dev/internal/testutil/oidctestutil" +) + +// TestFederationDomainIdentityProvidersListerFinder implements FederationDomainIdentityProvidersListerFinderI +// for testing purposes. +type TestFederationDomainIdentityProvidersListerFinder struct { + upstreamOIDCIdentityProviders []*oidctestutil.TestUpstreamOIDCIdentityProvider + upstreamLDAPIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider + upstreamActiveDirectoryIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider + defaultIDPDisplayName string +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) HasDefaultIDP() bool { + return t.defaultIDPDisplayName != "" +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) IDPCount() int { + return len(t.upstreamOIDCIdentityProviders) + len(t.upstreamLDAPIdentityProviders) + len(t.upstreamActiveDirectoryIdentityProviders) +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) GetIdentityProviders() []resolvedprovider.FederationDomainResolvedIdentityProvider { + fdIDPs := make([]resolvedprovider.FederationDomainResolvedIdentityProvider, + len(t.upstreamOIDCIdentityProviders)+len(t.upstreamLDAPIdentityProviders)+len(t.upstreamActiveDirectoryIdentityProviders)) + i := 0 + for _, testIDP := range t.upstreamOIDCIdentityProviders { + fdIDP := &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeOIDC, + Transforms: testIDP.TransformsForFederationDomain, + } + fdIDPs[i] = fdIDP + i++ + } + for _, testIDP := range t.upstreamLDAPIdentityProviders { + fdIDP := &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeLDAP, + Transforms: testIDP.TransformsForFederationDomain, + } + fdIDPs[i] = fdIDP + i++ + } + for _, testIDP := range t.upstreamActiveDirectoryIdentityProviders { + fdIDP := &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeActiveDirectory, + Transforms: testIDP.TransformsForFederationDomain, + } + fdIDPs[i] = fdIDP + i++ + } + return fdIDPs +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) FindDefaultIDP() (resolvedprovider.FederationDomainResolvedIdentityProvider, error) { + if t.defaultIDPDisplayName == "" { + return nil, fmt.Errorf("identity provider not found: this federation domain does not have a default identity provider") + } + return t.FindUpstreamIDPByDisplayName(t.defaultIDPDisplayName) +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) FindUpstreamIDPByDisplayName(upstreamIDPDisplayName string) (resolvedprovider.FederationDomainResolvedIdentityProvider, error) { + for _, testIDP := range t.upstreamOIDCIdentityProviders { + if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { + return &resolvedoidc.FederationDomainResolvedOIDCIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeOIDC, + Transforms: testIDP.TransformsForFederationDomain, + }, nil + } + } + for _, testIDP := range t.upstreamLDAPIdentityProviders { + if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { + return &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeLDAP, + Transforms: testIDP.TransformsForFederationDomain, + }, nil + } + } + for _, testIDP := range t.upstreamActiveDirectoryIdentityProviders { + if upstreamIDPDisplayName == testIDP.DisplayNameForFederationDomain { + return &resolvedldap.FederationDomainResolvedLDAPIdentityProvider{ + DisplayName: testIDP.DisplayNameForFederationDomain, + Provider: testIDP, + SessionProviderType: psession.ProviderTypeActiveDirectory, + Transforms: testIDP.TransformsForFederationDomain, + }, nil + } + } + return nil, fmt.Errorf("did not find IDP with name %q", upstreamIDPDisplayName) +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) SetOIDCIdentityProviders(providers []*oidctestutil.TestUpstreamOIDCIdentityProvider) { + t.upstreamOIDCIdentityProviders = providers +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) SetLDAPIdentityProviders(providers []*oidctestutil.TestUpstreamLDAPIdentityProvider) { + t.upstreamLDAPIdentityProviders = providers +} + +func (t *TestFederationDomainIdentityProvidersListerFinder) SetActiveDirectoryIdentityProviders(providers []*oidctestutil.TestUpstreamLDAPIdentityProvider) { + t.upstreamActiveDirectoryIdentityProviders = providers +} + +// UpstreamIDPListerBuilder can be used to build either a dynamicupstreamprovider.DynamicUpstreamIDPProvider +// or a FederationDomainIdentityProvidersListerFinderI for testing. +type UpstreamIDPListerBuilder struct { + upstreamOIDCIdentityProviders []*oidctestutil.TestUpstreamOIDCIdentityProvider + upstreamLDAPIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider + upstreamActiveDirectoryIdentityProviders []*oidctestutil.TestUpstreamLDAPIdentityProvider + defaultIDPDisplayName string +} + +func (b *UpstreamIDPListerBuilder) WithOIDC(upstreamOIDCIdentityProviders ...*oidctestutil.TestUpstreamOIDCIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamOIDCIdentityProviders = append(b.upstreamOIDCIdentityProviders, upstreamOIDCIdentityProviders...) + return b +} + +func (b *UpstreamIDPListerBuilder) WithLDAP(upstreamLDAPIdentityProviders ...*oidctestutil.TestUpstreamLDAPIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamLDAPIdentityProviders = append(b.upstreamLDAPIdentityProviders, upstreamLDAPIdentityProviders...) + return b +} + +func (b *UpstreamIDPListerBuilder) WithActiveDirectory(upstreamActiveDirectoryIdentityProviders ...*oidctestutil.TestUpstreamLDAPIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamActiveDirectoryIdentityProviders = append(b.upstreamActiveDirectoryIdentityProviders, upstreamActiveDirectoryIdentityProviders...) + return b +} + +func (b *UpstreamIDPListerBuilder) WithDefaultIDPDisplayName(defaultIDPDisplayName string) *UpstreamIDPListerBuilder { + b.defaultIDPDisplayName = defaultIDPDisplayName + return b +} + +func (b *UpstreamIDPListerBuilder) BuildFederationDomainIdentityProvidersListerFinder() *TestFederationDomainIdentityProvidersListerFinder { + return &TestFederationDomainIdentityProvidersListerFinder{ + upstreamOIDCIdentityProviders: b.upstreamOIDCIdentityProviders, + upstreamLDAPIdentityProviders: b.upstreamLDAPIdentityProviders, + upstreamActiveDirectoryIdentityProviders: b.upstreamActiveDirectoryIdentityProviders, + defaultIDPDisplayName: b.defaultIDPDisplayName, + } +} + +func (b *UpstreamIDPListerBuilder) BuildDynamicUpstreamIDPProvider() dynamicupstreamprovider.DynamicUpstreamIDPProvider { + idpProvider := dynamicupstreamprovider.NewDynamicUpstreamIDPProvider() + + oidcUpstreams := make([]upstreamprovider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) + for i := range b.upstreamOIDCIdentityProviders { + oidcUpstreams[i] = upstreamprovider.UpstreamOIDCIdentityProviderI(b.upstreamOIDCIdentityProviders[i]) + } + idpProvider.SetOIDCIdentityProviders(oidcUpstreams) + + ldapUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, len(b.upstreamLDAPIdentityProviders)) + for i := range b.upstreamLDAPIdentityProviders { + ldapUpstreams[i] = upstreamprovider.UpstreamLDAPIdentityProviderI(b.upstreamLDAPIdentityProviders[i]) + } + idpProvider.SetLDAPIdentityProviders(ldapUpstreams) + + adUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, len(b.upstreamActiveDirectoryIdentityProviders)) + for i := range b.upstreamActiveDirectoryIdentityProviders { + adUpstreams[i] = upstreamprovider.UpstreamLDAPIdentityProviderI(b.upstreamActiveDirectoryIdentityProviders[i]) + } + idpProvider.SetActiveDirectoryIdentityProviders(adUpstreams) + + return idpProvider +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPasswordCredentialsGrantAndValidateTokens( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs, +) { + t.Helper() + var actualArgs *oidctestutil.PasswordCredentialsGrantAndValidateTokensArgs + var actualNameOfUpstreamWhichMadeCall string + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstreamOIDC.PasswordCredentialsGrantAndValidateTokensCallCount() + actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name + actualArgs = upstreamOIDC.PasswordCredentialsGrantAndValidateTokensArgs(0) + } + } + require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, + "should have been exactly one call to PasswordCredentialsGrantAndValidateTokens() by all OIDC upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "PasswordCredentialsGrantAndValidateTokens() was called on the wrong OIDC upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPasswordCredentialsGrantAndValidateTokens(t *testing.T) { + t.Helper() + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.PasswordCredentialsGrantAndValidateTokensCallCount() + } + require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, + "expected exactly zero calls to PasswordCredentialsGrantAndValidateTokens()", + ) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToExchangeAuthcodeAndValidateTokens( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.ExchangeAuthcodeAndValidateTokenArgs, +) { + t.Helper() + var actualArgs *oidctestutil.ExchangeAuthcodeAndValidateTokenArgs + var actualNameOfUpstreamWhichMadeCall string + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstreamOIDC.ExchangeAuthcodeAndValidateTokensCallCount() + actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name + actualArgs = upstreamOIDC.ExchangeAuthcodeAndValidateTokensArgs(0) + } + } + require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, + "should have been exactly one call to ExchangeAuthcodeAndValidateTokens() by all OIDC upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "ExchangeAuthcodeAndValidateTokens() was called on the wrong OIDC upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToExchangeAuthcodeAndValidateTokens(t *testing.T) { + t.Helper() + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.ExchangeAuthcodeAndValidateTokensCallCount() + } + require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, + "expected exactly zero calls to ExchangeAuthcodeAndValidateTokens()", + ) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToPerformRefresh( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.PerformRefreshArgs, +) { + t.Helper() + var actualArgs *oidctestutil.PerformRefreshArgs + var actualNameOfUpstreamWhichMadeCall string + actualCallCountAcrossAllUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstreamOIDC.PerformRefreshCallCount() + actualCallCountAcrossAllUpstreams += callCountOnThisUpstream + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name + actualArgs = upstreamOIDC.PerformRefreshArgs(0) + } + } + for _, upstreamLDAP := range b.upstreamLDAPIdentityProviders { + 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, + "PerformRefresh() was called on the wrong upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToPerformRefresh(t *testing.T) { + t.Helper() + actualCallCountAcrossAllUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCountAcrossAllUpstreams += upstreamOIDC.PerformRefreshCallCount() + } + 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()", + ) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToValidateToken( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.ValidateTokenAndMergeWithUserInfoArgs, +) { + t.Helper() + var actualArgs *oidctestutil.ValidateTokenAndMergeWithUserInfoArgs + var actualNameOfUpstreamWhichMadeCall string + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstreamOIDC.ValidateTokenAndMergeWithUserInfoCallCount() + actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name + actualArgs = upstreamOIDC.ValidateTokenAndMergeWithUserInfoArgs(0) + } + } + require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, + "should have been exactly one call to ValidateTokenAndMergeWithUserInfo() by all OIDC upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "ValidateTokenAndMergeWithUserInfo() was called on the wrong OIDC upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToValidateToken(t *testing.T) { + t.Helper() + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.ValidateTokenAndMergeWithUserInfoCallCount() + } + require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, + "expected exactly zero calls to ValidateTokenAndMergeWithUserInfo()", + ) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyOneCallToRevokeToken( + t *testing.T, + expectedPerformedByUpstreamName string, + expectedArgs *oidctestutil.RevokeTokenArgs, +) { + t.Helper() + var actualArgs *oidctestutil.RevokeTokenArgs + var actualNameOfUpstreamWhichMadeCall string + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + callCountOnThisUpstream := upstreamOIDC.RevokeTokenCallCount() + actualCallCountAcrossAllOIDCUpstreams += callCountOnThisUpstream + if callCountOnThisUpstream == 1 { + actualNameOfUpstreamWhichMadeCall = upstreamOIDC.Name + actualArgs = upstreamOIDC.RevokeTokenArgs(0) + } + } + require.Equal(t, 1, actualCallCountAcrossAllOIDCUpstreams, + "should have been exactly one call to RevokeToken() by all OIDC upstreams", + ) + require.Equal(t, expectedPerformedByUpstreamName, actualNameOfUpstreamWhichMadeCall, + "RevokeToken() was called on the wrong OIDC upstream", + ) + require.Equal(t, expectedArgs, actualArgs) +} + +func (b *UpstreamIDPListerBuilder) RequireExactlyZeroCallsToRevokeToken(t *testing.T) { + t.Helper() + actualCallCountAcrossAllOIDCUpstreams := 0 + for _, upstreamOIDC := range b.upstreamOIDCIdentityProviders { + actualCallCountAcrossAllOIDCUpstreams += upstreamOIDC.RevokeTokenCallCount() + } + require.Equal(t, 0, actualCallCountAcrossAllOIDCUpstreams, + "expected exactly zero calls to RevokeToken()", + ) +} + +func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { + return &UpstreamIDPListerBuilder{} +} diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 131799cda..39c524460 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -25,7 +25,7 @@ import ( "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/crypto/ptls" "go.pinniped.dev/internal/endpointaddr" - "go.pinniped.dev/internal/federationdomain/downstreamsession" + "go.pinniped.dev/internal/federationdomain/downstreamsubject" "go.pinniped.dev/internal/federationdomain/upstreamprovider" "go.pinniped.dev/internal/plog" ) @@ -235,7 +235,7 @@ func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes u if err != nil { return nil, err } - newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL(), idpDisplayName) + newSubject := downstreamsubject.LDAP(newUID, *p.GetURL(), idpDisplayName) if newSubject != storedRefreshAttributes.Subject { return nil, fmt.Errorf(`searching for user %q produced a different subject than the previous value. expected: %q, actual: %q`, userDN, storedRefreshAttributes.Subject, newSubject) }