Refactor to extract interface for upstream IDP interactions

Create an interface to abstract the upstream IDP from the
authorize, IDP discovery, callback, choose IDP, and login
endpoints. This commit does not refactor the token endpoint,
which will be refactored in a similar way in the next commit.
This commit is contained in:
Ryan Richard
2024-02-14 17:48:52 -08:00
parent 9db87132b1
commit 1bc13e94f7
47 changed files with 3167 additions and 3157 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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),
)
}

View File

@@ -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)
})
}
}

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}

View File

@@ -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(),

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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(),
})
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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\"",

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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).

View File

@@ -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").

View File

@@ -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,

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
})
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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]
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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)
}