From e2bdab9e2df7db60ba933ad82f6fa734cdeab01e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 30 Aug 2023 15:13:42 -0700 Subject: [PATCH] add the IDP display name to the downstream ID token's `sub` claim To make the subject of the downstream ID token more unique when there are multiple IDPs. It is possible to define two IDPs in a FederationDomain using the same identity provider CR, in which case the only thing that would make the subject claim different is adding the IDP display name into the values of the subject claim. --- hack/prepare-supervisor-on-kind.sh | 4 +- .../downstreamsession/downstream_session.go | 34 +- .../downstream_session_test.go | 116 ++++++ .../endpoints/auth/auth_handler.go | 10 +- .../endpoints/auth/auth_handler_test.go | 50 +-- .../endpoints/callback/callback_handler.go | 12 +- .../callback/callback_handler_test.go | 46 +-- .../endpoints/login/post_login_handler.go | 15 +- .../login/post_login_handler_test.go | 30 +- .../endpoints/token/token_handler.go | 2 +- .../upstreamprovider/upsteam_provider.go | 2 +- .../testutil/oidctestutil/oidctestutil.go | 2 +- internal/upstreamldap/upstreamldap.go | 4 +- internal/upstreamldap/upstreamldap_test.go | 10 +- test/integration/supervisor_login_test.go | 329 +++++++----------- 15 files changed, 372 insertions(+), 294 deletions(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 0696e17ba..1fdf4a814 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -246,7 +246,7 @@ if [[ "$use_oidc_upstream" == "yes" ]]; then fd_idps="${fd_idps}$( cat <(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -262,7 +262,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "text/html;charset=UTF-8", wantBodyFormResponseRegexp: `(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -290,7 +290,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -315,7 +315,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -339,7 +339,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -373,7 +373,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "text/html;charset=UTF-8", wantBodyFormResponseRegexp: `(.+)`, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamRequestedScopes: []string{"openid"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -398,7 +398,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -437,7 +437,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenGroups: []string{}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -467,7 +467,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe@whitehouse.gov", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -499,7 +499,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe@whitehouse.gov", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -532,7 +532,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, // succeed despite `email_verified=false` because we're not using the email claim for anything wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "joe", wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -667,7 +667,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamSubject, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -697,7 +697,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: []string{"notAnArrayGroup1 notAnArrayGroup2"}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -727,7 +727,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: []string{"group1", "group2"}, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -761,7 +761,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "", // username scope was not requested wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"}, @@ -791,7 +791,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: nil, // groups scope was not requested wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"}, @@ -834,7 +834,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: "", // username scope was not requested wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamRequestedScopes: []string{"openid", "groups", "offline_access"}, @@ -877,7 +877,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: nil, // groups scope was not requested wantDownstreamRequestedScopes: []string{"openid", "username", "offline_access"}, @@ -902,7 +902,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: transformationUsernamePrefix + oidcUpstreamUsername, wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, oidcUpstreamGroupMembership), wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -1158,7 +1158,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"profile", "email", "username", "groups"}, wantDownstreamGrantedScopes: []string{"username", "groups"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -1188,7 +1188,7 @@ func TestCallbackEndpoint(t *testing.T) { wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"profile", "email"}, // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantDownstreamGrantedScopes: []string{"username", "groups"}, @@ -1217,7 +1217,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username\+groups&state=` + happyDownstreamState, wantDownstreamIDTokenUsername: oidcUpstreamUsername, - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "username", "groups"}, wantDownstreamGrantedScopes: []string{"openid", "offline_access", "username", "groups"}, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, @@ -1315,7 +1315,7 @@ func TestCallbackEndpoint(t *testing.T) { wantStatus: http.StatusSeeOther, wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp, wantBody: "", - wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped, + wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?idpName=" + happyUpstreamIDPName + "&sub=" + oidcUpstreamSubjectQueryEscaped, wantDownstreamIDTokenUsername: oidcUpstreamUsername, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamGrantedScopes: happyDownstreamScopesGranted, diff --git a/internal/federationdomain/endpoints/login/post_login_handler.go b/internal/federationdomain/endpoints/login/post_login_handler.go index 018ac4ffa..bdd1cb11c 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler.go +++ b/internal/federationdomain/endpoints/login/post_login_handler.go @@ -63,7 +63,9 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed } // Attempt to authenticate the user with the upstream IDP. - authenticateResponse, authenticated, err := ldapUpstream.Provider.AuthenticateUser(r.Context(), submittedUsername, submittedPassword, authorizeRequester.GetGrantedScopes()) + authenticateResponse, authenticated, err := ldapUpstream.Provider.AuthenticateUser( + r.Context(), submittedUsername, submittedPassword, authorizeRequester.GetGrantedScopes(), + ) 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. @@ -80,11 +82,15 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed // 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) + 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) + 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, @@ -92,7 +98,8 @@ func NewPostHandler(issuerURL string, upstreamIDPs federationdomainproviders.Fed return nil } - customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData(ldapUpstream.Provider, ldapUpstream.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups) + customSessionData := downstreamsession.MakeDownstreamLDAPOrADCustomSessionData( + ldapUpstream.Provider, ldapUpstream.SessionProviderType, authenticateResponse, username, upstreamUsername, upstreamGroups) openIDSession := downstreamsession.MakeDownstreamSession(subject, username, groups, authorizeRequester.GetGrantedScopes(), authorizeRequester.GetClient().GetID(), customSessionData, map[string]interface{}{}) oidc.PerformAuthcodeRedirect(r, w, oauthHelper, authorizeRequester, openIDSession, false) diff --git a/internal/federationdomain/endpoints/login/post_login_handler_test.go b/internal/federationdomain/endpoints/login/post_login_handler_test.go index 0a1753336..153cb9ded 100644 --- a/internal/federationdomain/endpoints/login/post_login_handler_test.go +++ b/internal/federationdomain/endpoints/login/post_login_handler_test.go @@ -338,7 +338,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -361,7 +361,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: transformationUsernamePrefix + happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, happyLDAPGroups), wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -390,7 +390,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -413,7 +413,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + activeDirectoryUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -436,7 +436,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + activeDirectoryUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: transformationUsernamePrefix + happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: testutil.AddPrefixToEach(transformationGroupsPrefix, happyLDAPGroups), wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -465,7 +465,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + activeDirectoryUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -490,7 +490,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyFormResponseRegexp: `(?s).*To finish logging in, paste this authorization code` + `.*
.*(.+).*`, // "(?s)" means match "." across newlines - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -515,7 +515,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -541,7 +541,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: happyDownstreamScopesRequested, @@ -567,7 +567,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantBodyString: "", // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, @@ -593,7 +593,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: "", // username scope was not requested, so there should be no username in the ID token wantDownstreamIDTokenGroups: []string{}, // groups scope was not requested, so there should be no groups in the ID token wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, @@ -627,7 +627,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: "", // username scope was not requested, so there should be no username in the ID token wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"openid", "offline_access", "groups"}, @@ -661,7 +661,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "", wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+username&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: []string{}, // groups scope was not requested, so there should be no groups in the ID token wantDownstreamRequestedScopes: []string{"openid", "offline_access", "username"}, @@ -691,7 +691,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantBodyString: "", // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"email"}, // only email was requested @@ -719,7 +719,7 @@ func TestPostLoginEndpoint(t *testing.T) { wantBodyString: "", // username and groups scopes were not requested but are granted anyway for the pinniped-cli client for backwards compatibility wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+username\+groups&state=` + happyDownstreamState, - wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "&idpName=" + ldapUpstreamName + "&sub=" + happyLDAPUID, wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, wantDownstreamIDTokenGroups: happyLDAPGroups, wantDownstreamRequestedScopes: []string{"openid"}, diff --git a/internal/federationdomain/endpoints/token/token_handler.go b/internal/federationdomain/endpoints/token/token_handler.go index 19057685f..53187d71a 100644 --- a/internal/federationdomain/endpoints/token/token_handler.go +++ b/internal/federationdomain/endpoints/token/token_handler.go @@ -394,7 +394,7 @@ func upstreamLDAPRefresh( Groups: oldUntransformedGroups, AdditionalAttributes: additionalAttributes, GrantedScopes: grantedScopes, - }) + }, p.DisplayName) if err != nil { return errUpstreamRefreshError().WithHint( "Upstream refresh failed.").WithTrace(err). diff --git a/internal/federationdomain/upstreamprovider/upsteam_provider.go b/internal/federationdomain/upstreamprovider/upsteam_provider.go index 68695b254..7caeafc1f 100644 --- a/internal/federationdomain/upstreamprovider/upsteam_provider.go +++ b/internal/federationdomain/upstreamprovider/upsteam_provider.go @@ -122,5 +122,5 @@ type UpstreamLDAPIdentityProviderI interface { authenticators.UserAuthenticator // PerformRefresh performs a refresh against the upstream LDAP identity provider - PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes) (groups []string, err error) + PerformRefresh(ctx context.Context, storedRefreshAttributes RefreshAttributes, idpDisplayName string) (groups []string, err error) } diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index bffe5ae73..a3c55591a 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -220,7 +220,7 @@ func (u *TestUpstreamLDAPIdentityProvider) GetURL() *url.URL { return u.URL } -func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes) ([]string, error) { +func (u *TestUpstreamLDAPIdentityProvider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, idpDisplayName string) ([]string, error) { if u.performRefreshArgs == nil { u.performRefreshArgs = make([]*PerformRefreshArgs, 0) } diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index b178adf2e..ef1c432a6 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -188,7 +188,7 @@ func closeAndLogError(conn Conn, doingWhat string) { } } -func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes) ([]string, error) { +func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes upstreamprovider.RefreshAttributes, idpDisplayName string) ([]string, error) { t := trace.FromContext(ctx).Nest("slow ldap refresh attempt", trace.Field{Key: "providerName", Value: p.GetName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches userDN := storedRefreshAttributes.DN @@ -237,7 +237,7 @@ func (p *Provider) PerformRefresh(ctx context.Context, storedRefreshAttributes u if err != nil { return nil, err } - newSubject := downstreamsession.DownstreamLDAPSubject(newUID, *p.GetURL()) + newSubject := downstreamsession.DownstreamLDAPSubject(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) } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index d07c2c9e1..e511d66e6 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -36,6 +36,7 @@ const ( testHost = "ldap.example.com:8443" testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev" testBindPassword = "some-bind-password" + testUpstreamName = "some-upstream-idp-name" testUpstreamUsername = "some-upstream-username" testUpstreamPassword = "some-upstream-password" testUserSearchBase = "some-upstream-user-base-dn" @@ -2046,7 +2047,7 @@ func TestUpstreamRefresh(t *testing.T) { }, nil).Times(1) conn.EXPECT().Close().Times(1) }, - wantErr: "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=d3JvbmctdWlk\"", + wantErr: "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&idpName=some-upstream-idp-name&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&idpName=some-upstream-idp-name&sub=d3JvbmctdWlk\"", }, { name: "search result has wrong username", @@ -2279,14 +2280,17 @@ func TestUpstreamRefresh(t *testing.T) { } initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000")) ldapProvider := New(*tt.providerConfig) - subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU" + subject := fmt.Sprintf( + "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&idpName=%s&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU", + testUpstreamName, + ) groups, err := ldapProvider.PerformRefresh(context.Background(), upstreamprovider.RefreshAttributes{ Username: testUserSearchResultUsernameAttributeValue, Subject: subject, DN: tt.refreshUserDN, AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded}, GrantedScopes: tt.grantedScopes, - }) + }, testUpstreamName) if tt.wantErr != "" { require.Error(t, err) require.Equal(t, tt.wantErr, err.Error()) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index e7d25ecf4..0d4f2912c 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -162,6 +162,49 @@ func TestSupervisorLogin_Browser(t *testing.T) { return ldapIDP, secret } + // The downstream ID token Subject should include the upstream user ID after the upstream issuer name + // and IDP display name. + expectedIDTokenSubjectRegexForUpstreamOIDC := "^" + + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?idpName=test-upstream-oidc-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub=") + ".+" + + "$" + + // The downstream ID token Subject should be the Host URL, plus the user search base, plus the IDP display name, + // plus value pulled from the requested UserSearch.Attributes.UID attribute. + expectedIDTokenSubjectRegexForUpstreamLDAP := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ldap-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + + "$" + + // Some of the LDAP tests below use the StartTLS server and port. + expectedIDTokenSubjectRegexForUpstreamLDAPStartTLSHost := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ldap-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + + "$" + + // The downstream ID token Subject should be in the the same format as LDAP above, but with AD-specific values. + expectedIDTokenSubjectRegexForUpstreamAD := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ad-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$" + + // Sometimes the AD tests below use a different search base. + expectedIDTokenSubjectRegexForUpstreamADWithCustomUserSearchBase := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ad-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$" + + // Sometimes the AD tests below create a user dynamically so we can't know the UID up front. + expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID := "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName=test-upstream-ad-idp-") + `[\w]+` + + regexp.QuoteMeta("&sub=") + ".+" + + "$" + // These tests attempt to exercise the entire login and refresh flow of the Supervisor for various cases. // They do not use the Pinniped CLI as the client, which allows them to exercise the Supervisor as an // OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go. @@ -275,8 +318,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { pinnipedSessionData := pinnipedSession.Custom pinnipedSessionData.OIDC.UpstreamIssuer = "wrong-issuer" }, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, }, @@ -299,7 +341,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { customSessionData := pinnipedSession.Custom customSessionData.Username = "some-incorrect-username" }, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string { @@ -346,7 +388,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { customSessionData := pinnipedSession.Custom customSessionData.Username = "some-incorrect-username" }, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, @@ -375,8 +417,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.OIDC.UpstreamRefreshToken) customSessionData.OIDC.UpstreamRefreshToken = "invalid-updated-refresh-token" }, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, }, @@ -406,8 +447,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, wantDownstreamIDTokenAdditionalClaims: wantGroupsInAdditionalClaimsIfGroupsExist(map[string]interface{}{ @@ -431,9 +471,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { } return testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, wantDownstreamIDTokenAdditionalClaims: wantGroupsInAdditionalClaimsIfGroupsExist(map[string]interface{}{ @@ -472,12 +511,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -504,12 +538,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -534,12 +563,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -563,7 +587,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access"}, wantDownstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username", "groups"}, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, @@ -579,13 +603,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -634,13 +653,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -684,12 +698,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -739,12 +748,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -778,12 +782,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.LDAP.UserDN) customSessionData.Username = "not-the-same" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAPStartTLSHost, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs, @@ -852,12 +851,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.LDAP.UserDN) customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -923,12 +917,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.LDAP.UserDN) customSessionData.LDAP.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -957,12 +946,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) customSessionData.Username = "not-the-same" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamAD, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" @@ -1007,12 +991,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { fositeSessionData := pinnipedSession.Fosite fositeSessionData.Claims.Subject = "not-right" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithCustomUserSearchBase, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$" @@ -1055,12 +1034,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { false, ) }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithCustomUserSearchBase, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$" @@ -1112,12 +1086,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamAD, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" @@ -1184,12 +1153,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NotEmpty(t, customSessionData.ActiveDirectory.UserDN) customSessionData.ActiveDirectory.UserDN = "cn=not-a-user,dc=pinniped,dc=dev" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamAD, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" @@ -1218,8 +1182,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) { testlib.ChangeADTestUserPassword(t, env, username) }, - // we can't know the subject ahead of time because we created a new user and don't know their uid, - // so skip wantDownstreamIDTokenSubjectToMatch + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(username string) string { return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$" @@ -1248,8 +1211,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) { testlib.DeactivateADTestUser(t, env, username) }, - // we can't know the subject ahead of time because we created a new user and don't know their uid, - // so skip wantDownstreamIDTokenSubjectToMatch + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(username string) string { return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$" @@ -1278,8 +1240,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { breakRefreshSessionData: func(t *testing.T, sessionData *psession.PinnipedSession, _, username string) { testlib.LockADTestUser(t, env, username) }, - // we can't know the subject ahead of time because we created a new user and don't know their uid, - // so skip wantDownstreamIDTokenSubjectToMatch + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamADWithUnkownUID, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(username string) string { return "^" + regexp.QuoteMeta(username+"@"+env.SupervisorUpstreamActiveDirectory.Domain) + "$" @@ -1337,12 +1298,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { require.NoError(t, err) time.Sleep(10 * time.Second) // wait for controllers to pick up the change }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -1381,12 +1337,7 @@ func TestSupervisorLogin_Browser(t *testing.T) { time.Sleep(10 * time.Second) // wait for controllers to pick up the change return []string{} }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -1399,10 +1350,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { createIDP: func(t *testing.T) string { return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - requestTokenExchangeAud: "contains-disallowed-substring.pinniped.dev-something", // .pinniped.dev substring is not allowed - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + requestTokenExchangeAud: "contains-disallowed-substring.pinniped.dev-something", // .pinniped.dev substring is not allowed + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, wantTokenExchangeResponse: func(t *testing.T, status int, body string) { @@ -1420,10 +1370,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { createIDP: func(t *testing.T) string { return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - requestTokenExchangeAud: "client.oauth.pinniped.dev-client-name", // OIDC dynamic client name is not allowed - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + requestTokenExchangeAud: "client.oauth.pinniped.dev-client-name", // OIDC dynamic client name is not allowed + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, wantTokenExchangeResponse: func(t *testing.T, status int, body string) { @@ -1441,10 +1390,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { createIDP: func(t *testing.T) string { return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - requestTokenExchangeAud: "pinniped-cli", // pinniped-cli is not allowed - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + requestTokenExchangeAud: "pinniped-cli", // pinniped-cli is not allowed + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" }, wantTokenExchangeResponse: func(t *testing.T, status int, body string) { @@ -1477,9 +1425,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, }, configv1alpha1.OIDCClientPhaseReady) }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, @@ -1510,9 +1457,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, }, configv1alpha1.OIDCClientPhaseReady) }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamOIDC, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$" }, wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, wantDownstreamIDTokenAdditionalClaims: wantGroupsInAdditionalClaimsIfGroupsExist(map[string]interface{}{ @@ -1539,13 +1485,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -1691,14 +1632,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username"}, // do not request (or expect) groups - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "username"}, // do not request (or expect) groups + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -1724,14 +1660,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "groups"}, // do not request (or expect) username - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + downstreamScopes: []string{"openid", "pinniped:request-audience", "offline_access", "groups"}, // do not request (or expect) username + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "" // username should not exist as a claim since we did not request it }, @@ -1763,14 +1694,9 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login }, - downstreamScopes: []string{"openid", "offline_access"}, // do not request (or expect) pinniped:request-audience or username or groups - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + downstreamScopes: []string{"openid", "offline_access"}, // do not request (or expect) pinniped:request-audience or username or groups + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamLDAP, wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "" // username should not exist as a claim since we did not request it }, @@ -1802,13 +1728,8 @@ func TestSupervisorLogin_Browser(t *testing.T) { return env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login env.SupervisorUpstreamActiveDirectory.TestUserPassword // password to present to server during login }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowLDAP, + wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamAD, // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" @@ -1878,8 +1799,12 @@ func TestSupervisorLogin_Browser(t *testing.T) { }, }, displayName }, - requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC, + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer) + + regexp.QuoteMeta("?idpName="+url.QueryEscape("my oidc idp")) + + regexp.QuoteMeta("&sub=") + ".+" + + "$", wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamOIDC.Username) + "$" }, @@ -1958,12 +1883,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ldap idp")) + + regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute and then transformed wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -2023,12 +1947,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamLDAP.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ - "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamLDAP.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ldap idp")) + + regexp.QuoteMeta("&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue))) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute and then transformed wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$" @@ -2093,12 +2016,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ad idp")) + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$" @@ -2160,12 +2082,11 @@ func TestSupervisorLogin_Browser(t *testing.T) { // compared to what they chose during the initial login, by changing what they decided during the initial login. customSessionData.Username = "some-different-downstream-username" }, - // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( - "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ - "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ - "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ) + "$", + wantDownstreamIDTokenSubjectToMatch: "^" + + regexp.QuoteMeta("ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+"?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)) + + regexp.QuoteMeta("&idpName="+url.QueryEscape("my ad idp")) + + regexp.QuoteMeta("&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue) + + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta("username-prefix:"+env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$"