More unit tests for dynamic clients

- Add dynamic client unit tests for the upstream OIDC callback and
  POST login endpoints.
- Enhance a few log statements to print the full fosite error messages
  into the logs where they were previously only printing the name of
  the error type.
This commit is contained in:
Ryan Richard 2022-07-21 09:26:00 -07:00
parent 34509e7430
commit e42f5488fa
8 changed files with 400 additions and 109 deletions

View File

@ -25,7 +25,6 @@ import (
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
kubetesting "k8s.io/client-go/testing"
"k8s.io/utils/pointer" "k8s.io/utils/pointer"
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
@ -3088,7 +3087,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
// OIDC validations are checked in fosite after the OAuth authcode (and sometimes the OIDC session) // OIDC validations are checked in fosite after the OAuth authcode (and sometimes the OIDC session)
// is stored, so it is possible with an LDAP upstream to store objects and then return an error to // is stored, so it is possible with an LDAP upstream to store objects and then return an error to
// the client anyway (which makes the stored objects useless, but oh well). // the client anyway (which makes the stored objects useless, but oh well).
require.Len(t, filterActions(kubeClient.Actions()), test.wantUnnecessaryStoredRecords) require.Len(t, oidctestutil.FilterClientSecretCreateActions(kubeClient.Actions()), test.wantUnnecessaryStoredRecords)
case test.wantRedirectLocationRegexp != "": case test.wantRedirectLocationRegexp != "":
if test.wantDownstreamClientID == "" { if test.wantDownstreamClientID == "" {
test.wantDownstreamClientID = pinnipedCLIClientID // default assertion value when not provided by test case test.wantDownstreamClientID = pinnipedCLIClientID // default assertion value when not provided by test case
@ -3301,20 +3300,3 @@ func requireEqualURLs(t *testing.T, actualURL string, expectedURL string, ignore
} }
require.Equal(t, expectedLocationQuery, actualLocationQuery) require.Equal(t, expectedLocationQuery, actualLocationQuery)
} }
// filterActions 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 filterActions(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

@ -48,7 +48,8 @@ func NewHandler(
reconstitutedAuthRequest := &http.Request{Form: downstreamAuthParams} reconstitutedAuthRequest := &http.Request{Form: downstreamAuthParams}
authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), reconstitutedAuthRequest) authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), reconstitutedAuthRequest)
if err != nil { if err != nil {
plog.Error("error using state downstream auth params", err) plog.Error("error using state downstream auth params", err,
"fositeErr", oidc.FositeErrorForLog(err))
return httperr.New(http.StatusBadRequest, "error using state downstream auth params") return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
} }
@ -83,7 +84,8 @@ func NewHandler(
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {
plog.WarningErr("error while generating and saving authcode", err, "upstreamName", upstreamIDPConfig.GetName()) plog.WarningErr("error while generating and saving authcode", err,
"upstreamName", upstreamIDPConfig.GetName(), "fositeErr", oidc.FositeErrorForLog(err))
return httperr.Wrap(http.StatusInternalServerError, "error while generating and saving authcode", err) return httperr.Wrap(http.StatusInternalServerError, "error while generating and saving authcode", err)
} }

View File

@ -54,7 +54,9 @@ const (
downstreamIssuer = "https://my-downstream-issuer.com/path" downstreamIssuer = "https://my-downstream-issuer.com/path"
downstreamRedirectURI = "http://127.0.0.1/callback" downstreamRedirectURI = "http://127.0.0.1/callback"
downstreamClientID = "pinniped-cli" downstreamPinnipedClientID = "pinniped-cli"
downstreamDynamicClientID = "client.oauth.pinniped.dev-test-name"
downstreamDynamicClientUID = "fake-client-uid"
downstreamNonce = "some-nonce-value" downstreamNonce = "some-nonce-value"
downstreamPKCEChallenge = "some-challenge" downstreamPKCEChallenge = "some-challenge"
downstreamPKCEChallengeMethod = "S256" downstreamPKCEChallengeMethod = "S256"
@ -70,14 +72,19 @@ var (
happyDownstreamRequestParamsQuery = url.Values{ happyDownstreamRequestParamsQuery = url.Values{
"response_type": []string{"code"}, "response_type": []string{"code"},
"scope": []string{strings.Join(happyDownstreamScopesRequested, " ")}, "scope": []string{strings.Join(happyDownstreamScopesRequested, " ")},
"client_id": []string{downstreamClientID}, "client_id": []string{downstreamPinnipedClientID},
"state": []string{happyDownstreamState}, "state": []string{happyDownstreamState},
"nonce": []string{downstreamNonce}, "nonce": []string{downstreamNonce},
"code_challenge": []string{downstreamPKCEChallenge}, "code_challenge": []string{downstreamPKCEChallenge},
"code_challenge_method": []string{downstreamPKCEChallengeMethod}, "code_challenge_method": []string{downstreamPKCEChallengeMethod},
"redirect_uri": []string{downstreamRedirectURI}, "redirect_uri": []string{downstreamRedirectURI},
} }
happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode() happyDownstreamRequestParams = happyDownstreamRequestParamsQuery.Encode()
happyDownstreamRequestParamsForDynamicClient = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
map[string]string{"client_id": downstreamDynamicClientID},
).Encode()
happyDownstreamCustomSessionData = &psession.CustomSessionData{ happyDownstreamCustomSessionData = &psession.CustomSessionData{
ProviderUID: happyUpstreamIDPResourceUID, ProviderUID: happyUpstreamIDPResourceUID,
ProviderName: happyUpstreamIDPName, ProviderName: happyUpstreamIDPName,
@ -122,6 +129,7 @@ func TestCallbackEndpoint(t *testing.T) {
happyCookieCodec.SetSerializer(securecookie.JSONEncoder{}) happyCookieCodec.SetSerializer(securecookie.JSONEncoder{})
happyState := happyUpstreamStateParam().Build(t, happyStateCodec) happyState := happyUpstreamStateParam().Build(t, happyStateCodec)
happyStateForDynamicClient := happyUpstreamStateParamForDynamicClient().Build(t, happyStateCodec)
encodedIncomingCookieCSRFValue, err := happyCookieCodec.Encode("csrf", happyDownstreamCSRF) encodedIncomingCookieCSRFValue, err := happyCookieCodec.Encode("csrf", happyDownstreamCSRF)
require.NoError(t, err) require.NoError(t, err)
@ -137,6 +145,13 @@ func TestCallbackEndpoint(t *testing.T) {
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost})
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
}
tests := []struct { tests := []struct {
name string name string
@ -157,6 +172,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamIDTokenGroups []string wantDownstreamIDTokenGroups []string
wantDownstreamRequestedScopes []string wantDownstreamRequestedScopes []string
wantDownstreamNonce string wantDownstreamNonce string
wantDownstreamClientID string
wantDownstreamPKCEChallenge string wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string wantDownstreamPKCEChallengeMethod string
wantDownstreamCustomSessionData *psession.CustomSessionData wantDownstreamCustomSessionData *psession.CustomSessionData
@ -185,6 +201,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -208,6 +225,32 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
wantAuthcodeExchangeCall: &expectedAuthcodeExchange{
performedByUpstreamName: happyUpstreamIDPName,
args: happyExchangeAndValidateTokensArgs,
},
},
{
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()),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
method: http.MethodGet,
path: newRequestPath().WithState(happyStateForDynamicClient).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusSeeOther,
wantRedirectLocationRegexp: happyDownstreamRedirectLocationRegexp,
wantBody: "",
wantDownstreamIDTokenSubject: oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped,
wantDownstreamIDTokenUsername: oidcUpstreamUsername,
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamDynamicClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -231,6 +274,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamAccessTokenCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamAccessTokenCustomSessionData,
@ -263,6 +307,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: []string{"openid"}, wantDownstreamRequestedScopes: []string{"openid"},
wantDownstreamGrantedScopes: []string{"openid"}, wantDownstreamGrantedScopes: []string{"openid"},
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -286,6 +331,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: &psession.CustomSessionData{ wantDownstreamCustomSessionData: &psession.CustomSessionData{
@ -321,6 +367,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -346,6 +393,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -373,6 +421,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -401,6 +450,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -531,6 +581,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -556,6 +607,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -581,6 +633,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: happyDownstreamScopesRequested, wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -714,6 +767,42 @@ func TestCallbackEndpoint(t *testing.T) {
wantContentType: htmlContentType, wantContentType: htmlContentType,
wantBody: "Bad Request: error using state downstream auth params\n", wantBody: "Bad Request: error using state downstream auth params\n",
}, },
{
name: "state's downstream auth params have invalid client_id",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
method: http.MethodGet,
path: newRequestPath().WithState(
happyUpstreamStateParam().
WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": "bogus"}).Encode()).
Build(t, happyStateCodec),
).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: error using state downstream auth params\n",
},
{
name: "dynamic clients do not allow response_mode=form_post",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
method: http.MethodGet,
path: newRequestPath().WithState(
happyUpstreamStateParam().WithAuthorizeRequestParams(
shallowCopyAndModifyQuery(
happyDownstreamRequestParamsQuery,
map[string]string{
"client_id": downstreamDynamicClientID,
"response_mode": "form_post",
"scope": "openid",
},
).Encode(),
).Build(t, happyStateCodec),
).String(),
csrfCookie: happyCSRFCookie,
wantStatus: http.StatusBadRequest,
wantContentType: htmlContentType,
wantBody: "Bad Request: error using state downstream auth params\n",
},
{ {
name: "state's downstream auth params does not contain openid scope", name: "state's downstream auth params does not contain openid scope",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(happyUpstream().Build()),
@ -733,6 +822,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamGrantedScopes: []string{"groups"}, wantDownstreamGrantedScopes: []string{"groups"},
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -759,6 +849,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamRequestedScopes: []string{"profile", "email"}, wantDownstreamRequestedScopes: []string{"profile", "email"},
wantDownstreamGrantedScopes: []string{}, wantDownstreamGrantedScopes: []string{},
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -786,6 +877,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "groups"}, wantDownstreamGrantedScopes: []string{"openid", "offline_access", "groups"},
wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership, wantDownstreamIDTokenGroups: oidcUpstreamGroupMembership,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -884,6 +976,7 @@ func TestCallbackEndpoint(t *testing.T) {
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamIDTokenGroups: []string{}, wantDownstreamIDTokenGroups: []string{},
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClientID: downstreamPinnipedClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: happyDownstreamCustomSessionData, wantDownstreamCustomSessionData: happyDownstreamCustomSessionData,
@ -1139,7 +1232,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamPKCEChallenge, test.wantDownstreamPKCEChallenge,
test.wantDownstreamPKCEChallengeMethod, test.wantDownstreamPKCEChallengeMethod,
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, test.wantDownstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData, test.wantDownstreamCustomSessionData,
) )
@ -1166,7 +1259,7 @@ func TestCallbackEndpoint(t *testing.T) {
test.wantDownstreamPKCEChallenge, test.wantDownstreamPKCEChallenge,
test.wantDownstreamPKCEChallengeMethod, test.wantDownstreamPKCEChallengeMethod,
test.wantDownstreamNonce, test.wantDownstreamNonce,
downstreamClientID, test.wantDownstreamClientID,
downstreamRedirectURI, downstreamRedirectURI,
test.wantDownstreamCustomSessionData, test.wantDownstreamCustomSessionData,
) )
@ -1237,6 +1330,12 @@ func happyUpstreamStateParam() *oidctestutil.UpstreamStateParamBuilder {
} }
} }
func happyUpstreamStateParamForDynamicClient() *oidctestutil.UpstreamStateParamBuilder {
p := happyUpstreamStateParam()
p.P = happyDownstreamRequestParamsForDynamicClient
return p
}
func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder { func happyUpstream() *oidctestutil.TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder(). return oidctestutil.NewTestUpstreamOIDCIdentityProviderBuilder().
WithName(happyUpstreamIDPName). WithName(happyUpstreamIDPName).

View File

@ -41,7 +41,8 @@ func NewPostHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvider
if err != nil { if err != nil {
// This shouldn't really happen because the authorization endpoint has already validated these params // This shouldn't really happen because the authorization endpoint has already validated these params
// by calling NewAuthorizeRequest() itself. // by calling NewAuthorizeRequest() itself.
plog.Error("error using state downstream auth params", err) plog.Error("error using state downstream auth params", err,
"fositeErr", oidc.FositeErrorForLog(err))
return httperr.New(http.StatusBadRequest, "error using state downstream auth params") return httperr.New(http.StatusBadRequest, "error using state downstream auth params")
} }

View File

@ -38,7 +38,9 @@ func TestPostLoginEndpoint(t *testing.T) {
downstreamIssuer = "https://my-downstream-issuer.com/path" downstreamIssuer = "https://my-downstream-issuer.com/path"
downstreamRedirectURI = "http://127.0.0.1/callback" downstreamRedirectURI = "http://127.0.0.1/callback"
downstreamClientID = "pinniped-cli" downstreamPinnipedCLIClientID = "pinniped-cli"
downstreamDynamicClientID = "client.oauth.pinniped.dev-test-name"
downstreamDynamicClientUID = "fake-client-uid"
happyDownstreamState = "8b-state" happyDownstreamState = "8b-state"
downstreamNonce = "some-nonce-value" downstreamNonce = "some-nonce-value"
downstreamPKCEChallenge = "some-challenge" downstreamPKCEChallenge = "some-challenge"
@ -90,7 +92,7 @@ func TestPostLoginEndpoint(t *testing.T) {
happyDownstreamRequestParamsQuery := url.Values{ happyDownstreamRequestParamsQuery := url.Values{
"response_type": []string{"code"}, "response_type": []string{"code"},
"scope": []string{strings.Join(happyDownstreamScopesRequested, " ")}, "scope": []string{strings.Join(happyDownstreamScopesRequested, " ")},
"client_id": []string{downstreamClientID}, "client_id": []string{downstreamPinnipedCLIClientID},
"state": []string{happyDownstreamState}, "state": []string{happyDownstreamState},
"nonce": []string{downstreamNonce}, "nonce": []string{downstreamNonce},
"code_challenge": []string{downstreamPKCEChallenge}, "code_challenge": []string{downstreamPKCEChallenge},
@ -99,14 +101,10 @@ func TestPostLoginEndpoint(t *testing.T) {
} }
happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode() happyDownstreamRequestParams := happyDownstreamRequestParamsQuery.Encode()
copyOfHappyDownstreamRequestParamsQuery := func() url.Values { happyDownstreamRequestParamsQueryForDynamicClient := shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
params := url.Values{} map[string]string{"client_id": downstreamDynamicClientID},
for k, v := range happyDownstreamRequestParamsQuery { )
params[k] = make([]string, len(v)) happyDownstreamRequestParamsForDynamicClient := happyDownstreamRequestParamsQueryForDynamicClient.Encode()
copy(params[k], v)
}
return params
}
happyLDAPDecodedState := &oidc.UpstreamStateParamData{ happyLDAPDecodedState := &oidc.UpstreamStateParamData{
AuthParams: happyDownstreamRequestParams, AuthParams: happyDownstreamRequestParams,
@ -124,15 +122,20 @@ func TestPostLoginEndpoint(t *testing.T) {
return &copyOfHappyLDAPDecodedState return &copyOfHappyLDAPDecodedState
} }
happyActiveDirectoryDecodedState := &oidc.UpstreamStateParamData{ happyLDAPDecodedStateForDynamicClient := modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
AuthParams: happyDownstreamRequestParams, data.AuthParams = happyDownstreamRequestParamsForDynamicClient
UpstreamName: activeDirectoryUpstreamName, })
UpstreamType: activeDirectoryUpstreamType,
Nonce: happyDownstreamNonce, happyActiveDirectoryDecodedState := modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
CSRFToken: happyDownstreamCSRF, data.UpstreamName = activeDirectoryUpstreamName
PKCECode: happyDownstreamPKCE, data.UpstreamType = activeDirectoryUpstreamType
FormatVersion: happyDownstreamStateVersion, })
}
happyActiveDirectoryDecodedStateForDynamicClient := modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
data.AuthParams = happyDownstreamRequestParamsForDynamicClient
data.UpstreamName = activeDirectoryUpstreamName
data.UpstreamType = activeDirectoryUpstreamType
})
happyLDAPUsername := "some-ldap-user" happyLDAPUsername := "some-ldap-user"
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username" happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
@ -232,6 +235,13 @@ func TestPostLoginEndpoint(t *testing.T) {
return urlToReturn return urlToReturn
} }
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
"some-namespace", downstreamDynamicClientID, downstreamDynamicClientUID, downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost})
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
require.NoError(t, kubeClient.Tracker().Add(secret))
}
tests := []struct { tests := []struct {
name string name string
idps *oidctestutil.UpstreamIDPListerBuilder idps *oidctestutil.UpstreamIDPListerBuilder
@ -262,6 +272,7 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamPKCEChallenge string wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string wantDownstreamPKCEChallengeMethod string
wantDownstreamNonce string wantDownstreamNonce string
wantDownstreamClient string
wantDownstreamCustomSessionData *psession.CustomSessionData wantDownstreamCustomSessionData *psession.CustomSessionData
// Authorization requests for either a successful OIDC upstream or for an error with any upstream // Authorization requests for either a successful OIDC upstream or for an error with any upstream
@ -289,6 +300,31 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamRedirectURI: downstreamRedirectURI, wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamPinnipedCLIClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy LDAP login with dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: happyLDAPDecodedStateForDynamicClient,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamDynamicClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
@ -311,6 +347,31 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamRedirectURI: downstreamRedirectURI, wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamPinnipedCLIClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
},
{
name: "happy AD login with dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().
WithLDAP(&erroringUpstreamLDAPIdentityProvider).
WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider), // should pick this one
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: happyActiveDirectoryDecodedStateForDynamicClient,
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamDynamicClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyActiveDirectoryUpstreamCustomSession,
@ -319,9 +380,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form", name: "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["response_mode"] = []string{"form_post"} map[string]string{"response_mode": "form_post"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
@ -335,6 +396,7 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamRedirectURI: downstreamRedirectURI, wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamPinnipedCLIClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
@ -343,9 +405,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number", 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: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["redirect_uri"] = []string{"http://127.0.0.1:4242/callback"} map[string]string{"redirect_uri": "http://127.0.0.1:4242/callback"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -359,6 +421,33 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback", wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback",
wantDownstreamGrantedScopes: happyDownstreamScopesGranted, wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamPinnipedCLIClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
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),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
map[string]string{"redirect_uri": "http://127.0.0.1:4242/callback"},
).Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: "http://127.0.0.1:4242/callback" + `\?code=([^&]+)&scope=openid\+groups&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: happyDownstreamScopesRequested,
wantDownstreamRedirectURI: "http://127.0.0.1:4242/callback",
wantDownstreamGrantedScopes: happyDownstreamScopesGranted,
wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamDynamicClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
@ -367,9 +456,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "happy LDAP login when there are additional allowed downstream requested scopes", name: "happy LDAP login when there are additional allowed downstream requested scopes",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["scope"] = []string{"openid offline_access pinniped:request-audience"} map[string]string{"scope": "openid offline_access pinniped:request-audience"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -383,6 +472,33 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamRedirectURI: downstreamRedirectURI, wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"}, wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamPinnipedCLIClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
},
{
name: "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
map[string]string{"scope": "openid offline_access pinniped:request-audience"},
).Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state=` + happyDownstreamState,
wantDownstreamIDTokenSubject: upstreamLDAPURL + "&sub=" + happyLDAPUID,
wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator,
wantDownstreamIDTokenGroups: happyLDAPGroups,
wantDownstreamRequestedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{"openid", "offline_access", "pinniped:request-audience"},
wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamDynamicClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
@ -391,11 +507,13 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested", name: "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["scope"] = []string{"email"} map[string]string{
// The following prompt value is illegal when openid is requested, but note that openid is not requested. "scope": "email",
query["prompt"] = []string{"none login"} // The following prompt value is illegal when openid is requested, but note that openid is not requested.
data.AuthParams = query.Encode() "prompt": "none login",
},
).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -409,6 +527,7 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamRedirectURI: downstreamRedirectURI, wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{}, // no scopes granted wantDownstreamGrantedScopes: []string{}, // no scopes granted
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamPinnipedCLIClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
@ -419,9 +538,9 @@ func TestPostLoginEndpoint(t *testing.T) {
WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one WithLDAP(&upstreamLDAPIdentityProvider). // should pick this one
WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider), WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["scope"] = []string{"openid"} map[string]string{"scope": "openid"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -434,6 +553,7 @@ func TestPostLoginEndpoint(t *testing.T) {
wantDownstreamRedirectURI: downstreamRedirectURI, wantDownstreamRedirectURI: downstreamRedirectURI,
wantDownstreamGrantedScopes: []string{"openid"}, wantDownstreamGrantedScopes: []string{"openid"},
wantDownstreamNonce: downstreamNonce, wantDownstreamNonce: downstreamNonce,
wantDownstreamClient: downstreamPinnipedCLIClientID,
wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallenge: downstreamPKCEChallenge,
wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod,
wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession, wantDownstreamCustomSessionData: expectedHappyLDAPUpstreamCustomSession,
@ -502,9 +622,21 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "downstream redirect uri does not match what is configured for client", name: "downstream redirect uri does not match what is configured for client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["redirect_uri"] = []string{"http://127.0.0.1/wrong_callback"} map[string]string{"redirect_uri": "http://127.0.0.1/wrong_callback"},
data.AuthParams = query.Encode() ).Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream redirect uri does not match what is configured for client with dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
map[string]string{"redirect_uri": "http://127.0.0.1/wrong_callback"},
).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params", wantErr: "error using state downstream auth params",
@ -513,9 +645,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "downstream client does not exist", name: "downstream client does not exist",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["client_id"] = []string{"wrong_client_id"} map[string]string{"client_id": "wrong_client_id"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params", wantErr: "error using state downstream auth params",
@ -524,9 +656,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "downstream client is missing", name: "downstream client is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
delete(query, "client_id") map[string]string{"client_id": ""},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params", wantErr: "error using state downstream auth params",
@ -535,9 +667,21 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "response type is unsupported", name: "response type is unsupported",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["response_type"] = []string{"unsupported"} map[string]string{"response_type": "unsupported"},
data.AuthParams = query.Encode() ).Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "response type form_post is unsupported for dynamic clients",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
map[string]string{"response_type": "form_post"},
).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params", wantErr: "error using state downstream auth params",
@ -546,9 +690,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "response type is missing", name: "response type is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
delete(query, "response_type") map[string]string{"response_type": ""},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params", wantErr: "error using state downstream auth params",
@ -557,9 +701,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "PKCE code_challenge is missing", name: "PKCE code_challenge is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
delete(query, "code_challenge") map[string]string{"code_challenge": ""},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -572,9 +716,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "PKCE code_challenge_method is invalid", name: "PKCE code_challenge_method is invalid",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["code_challenge_method"] = []string{"this-is-not-a-valid-pkce-alg"} map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -587,9 +731,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "PKCE code_challenge_method is `plain`", name: "PKCE code_challenge_method is `plain`",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["code_challenge_method"] = []string{"plain"} // plain is not allowed map[string]string{"code_challenge_method": "plain"}, // plain is not allowed
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -602,9 +746,25 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "PKCE code_challenge_method is missing", name: "PKCE code_challenge_method is missing",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
delete(query, "code_challenge_method") map[string]string{"code_challenge_method": ""},
data.AuthParams = query.Encode() ).Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther,
wantContentType: htmlContentType,
wantBodyString: "",
wantRedirectLocationString: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error
},
{
name: "PKCE code_challenge_method is missing with dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
map[string]string{"code_challenge_method": ""},
).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -617,9 +777,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "prompt param is not allowed to have none and another legal value at the same time", name: "prompt param is not allowed to have none and another legal value at the same time",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["prompt"] = []string{"none login"} map[string]string{"prompt": "none login"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantStatus: http.StatusSeeOther, wantStatus: http.StatusSeeOther,
@ -632,9 +792,9 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "downstream state does not have enough entropy", name: "downstream state does not have enough entropy",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["state"] = []string{"short"} map[string]string{"state": "short"},
data.AuthParams = query.Encode() ).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params", wantErr: "error using state downstream auth params",
@ -643,9 +803,21 @@ func TestPostLoginEndpoint(t *testing.T) {
name: "downstream scopes do not match what is configured for client", name: "downstream scopes do not match what is configured for client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) { decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
query := copyOfHappyDownstreamRequestParamsQuery() data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery,
query["scope"] = []string{"openid offline_access pinniped:request-audience scope_not_allowed"} map[string]string{"scope": "openid offline_access pinniped:request-audience scope_not_allowed"},
data.AuthParams = query.Encode() ).Encode()
}),
formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params",
},
{
name: "downstream scopes do not match what is configured for client with dynamic client",
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
decodedState: modifyHappyLDAPDecodedState(func(data *oidc.UpstreamStateParamData) {
data.AuthParams = shallowCopyAndModifyQuery(happyDownstreamRequestParamsQueryForDynamicClient,
map[string]string{"scope": "openid offline_access pinniped:request-audience scope_not_allowed"},
).Encode()
}), }),
formParams: happyUsernamePasswordFormParams, formParams: happyUsernamePasswordFormParams,
wantErr: "error using state downstream auth params", wantErr: "error using state downstream auth params",
@ -677,8 +849,8 @@ func TestPostLoginEndpoint(t *testing.T) {
secretsClient := kubeClient.CoreV1().Secrets("some-namespace") secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace") oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace")
if test.kubeResources != nil { if tt.kubeResources != nil {
test.kubeResources(t, supervisorClient, kubeClient) tt.kubeResources(t, supervisorClient, kubeClient)
} }
// Configure fosite the same way that the production code would. // Configure fosite the same way that the production code would.
@ -704,7 +876,7 @@ func TestPostLoginEndpoint(t *testing.T) {
err := subject(rsp, req, happyEncodedUpstreamState, tt.decodedState) err := subject(rsp, req, happyEncodedUpstreamState, tt.decodedState)
if tt.wantErr != "" { if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr) require.EqualError(t, err, tt.wantErr)
require.Empty(t, kubeClient.Actions()) require.Empty(t, oidctestutil.FilterClientSecretCreateActions(kubeClient.Actions()))
return // the http response doesn't matter when the function returns an error, because the caller should handle the error return // the http response doesn't matter when the function returns an error, because the caller should handle the error
} }
// Otherwise, expect no error. // Otherwise, expect no error.
@ -735,7 +907,7 @@ func TestPostLoginEndpoint(t *testing.T) {
tt.wantDownstreamPKCEChallenge, tt.wantDownstreamPKCEChallenge,
tt.wantDownstreamPKCEChallengeMethod, tt.wantDownstreamPKCEChallengeMethod,
tt.wantDownstreamNonce, tt.wantDownstreamNonce,
downstreamClientID, tt.wantDownstreamClient,
tt.wantDownstreamRedirectURI, tt.wantDownstreamRedirectURI,
tt.wantDownstreamCustomSessionData, tt.wantDownstreamCustomSessionData,
) )
@ -745,12 +917,12 @@ func TestPostLoginEndpoint(t *testing.T) {
expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath + expectedLocation := downstreamIssuer + oidc.PinnipedLoginPath +
"?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState "?err=" + tt.wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState
require.Equal(t, expectedLocation, actualLocation) require.Equal(t, expectedLocation, actualLocation)
require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) require.Len(t, oidctestutil.FilterClientSecretCreateActions(kubeClient.Actions()), tt.wantUnnecessaryStoredRecords)
case tt.wantRedirectLocationString != "": case tt.wantRedirectLocationString != "":
// Expecting an error redirect to the client. // Expecting an error redirect to the client.
require.Equal(t, tt.wantBodyString, rsp.Body.String()) require.Equal(t, tt.wantBodyString, rsp.Body.String())
require.Equal(t, tt.wantRedirectLocationString, actualLocation) require.Equal(t, tt.wantRedirectLocationString, actualLocation)
require.Len(t, kubeClient.Actions(), tt.wantUnnecessaryStoredRecords) require.Len(t, oidctestutil.FilterClientSecretCreateActions(kubeClient.Actions()), tt.wantUnnecessaryStoredRecords)
case tt.wantBodyFormResponseRegexp != "": case tt.wantBodyFormResponseRegexp != "":
// Expecting the body of the response to be a html page with a form (for "response_mode=form_post"). // Expecting the body of the response to be a html page with a form (for "response_mode=form_post").
_, hasLocationHeader := rsp.Header()["Location"] _, hasLocationHeader := rsp.Header()["Location"]
@ -770,7 +942,7 @@ func TestPostLoginEndpoint(t *testing.T) {
tt.wantDownstreamPKCEChallenge, tt.wantDownstreamPKCEChallenge,
tt.wantDownstreamPKCEChallengeMethod, tt.wantDownstreamPKCEChallengeMethod,
tt.wantDownstreamNonce, tt.wantDownstreamNonce,
downstreamClientID, tt.wantDownstreamClient,
tt.wantDownstreamRedirectURI, tt.wantDownstreamRedirectURI,
tt.wantDownstreamCustomSessionData, tt.wantDownstreamCustomSessionData,
) )
@ -781,3 +953,18 @@ func TestPostLoginEndpoint(t *testing.T) {
}) })
} }
} }
func shallowCopyAndModifyQuery(query url.Values, modifications map[string]string) url.Values {
copied := url.Values{}
for key, value := range query {
copied[key] = value
}
for key, value := range modifications {
if value == "" {
copied.Del(key)
} else {
copied[key] = []string{value}
}
}
return copied
}

View File

@ -457,7 +457,7 @@ func PerformAuthcodeRedirect(
) { ) {
authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession)
if err != nil { if err != nil {
plog.WarningErr("error while generating and saving authcode", err) plog.WarningErr("error while generating and saving authcode", err, "fositeErr", FositeErrorForLog(err))
WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless) WriteAuthorizeError(w, oauthHelper, authorizeRequester, err, isBrowserless)
return return
} }

View File

@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1"
kubetesting "k8s.io/client-go/testing"
"k8s.io/utils/strings/slices" "k8s.io/utils/strings/slices"
"go.pinniped.dev/internal/authenticators" "go.pinniped.dev/internal/authenticators"
@ -954,7 +955,7 @@ func RequireAuthCodeRegexpMatch(
if includesOpenIDScope(wantDownstreamGrantedScopes) { if includesOpenIDScope(wantDownstreamGrantedScopes) {
expectedNumberOfCreatedSecrets++ expectedNumberOfCreatedSecrets++
} }
require.Len(t, kubeClient.Actions(), expectedNumberOfCreatedSecrets) require.Len(t, FilterClientSecretCreateActions(kubeClient.Actions()), expectedNumberOfCreatedSecrets)
// One authcode should have been stored. // One authcode should have been stored.
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
@ -1164,3 +1165,20 @@ func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requ
return storedRequest, storedSession 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

@ -15,6 +15,8 @@ import (
"testing" "testing"
"time" "time"
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
authorizationv1 "k8s.io/api/authorization/v1" authorizationv1 "k8s.io/api/authorization/v1"
@ -435,7 +437,7 @@ func createOIDCClientSecret(t *testing.T, forOIDCClient *configv1alpha1.OIDCClie
_, err := io.ReadFull(rand.Reader, buf[:]) _, err := io.ReadFull(rand.Reader, buf[:])
require.NoError(t, err) require.NoError(t, err)
randomSecret := hex.EncodeToString(buf[:]) randomSecret := hex.EncodeToString(buf[:])
hashedRandomSecret, err := bcrypt.GenerateFromPassword([]byte(randomSecret), 15) hashedRandomSecret, err := bcrypt.GenerateFromPassword([]byte(randomSecret), oidcclientvalidator.DefaultMinBcryptCost)
require.NoError(t, err) require.NoError(t, err)
created, err := kubeClient.CoreV1().Secrets(env.SupervisorNamespace).Create(ctx, &corev1.Secret{ created, err := kubeClient.CoreV1().Secrets(env.SupervisorNamespace).Create(ctx, &corev1.Secret{