// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package endpointsmanager import ( "crypto/ecdsa" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/sclevine/spec" "github.com/stretchr/testify/require" "gopkg.in/square/go-jose.v2" "k8s.io/client-go/kubernetes/fake" supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" "go.pinniped.dev/internal/federationdomain/endpoints/discovery" "go.pinniped.dev/internal/federationdomain/endpoints/jwks" "go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/federationdomain/oidc" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/idtransform" "go.pinniped.dev/internal/secret" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil/oidctestutil" ) func TestManager(t *testing.T) { spec.Run(t, "ServeHTTP", func(t *testing.T, when spec.G, it spec.S) { var ( r *require.Assertions subject *Manager nextHandler http.HandlerFunc fallbackHandlerWasCalled bool dynamicJWKSProvider jwks.DynamicJWKSProvider federationDomainIDPs []*federationdomainproviders.FederationDomainIdentityProvider kubeClient *fake.Clientset ) const ( issuer1 = "https://example.com/some/path" issuer1DifferentCaseHostname = "https://eXamPle.coM/some/path" issuer1KeyID = "issuer1-key" issuer2 = "https://example.com/some/path/more/deeply/nested/path" // note that this is a sub-path of the other issuer url issuer2DifferentCaseHostname = "https://exAmPlE.Com/some/path/more/deeply/nested/path" issuer2KeyID = "issuer2-key" upstreamIDPAuthorizationURL1 = "https://test-upstream.com/auth1" upstreamIDPAuthorizationURL2 = "https://test-upstream.com/auth2" upstreamIDPDisplayName1 = "test-idp-display-name-1" upstreamIDPDisplayName2 = "test-idp-display-name-2" upstreamIDPName1 = "test-idp-1" upstreamIDPName2 = "test-idp-2" upstreamResourceUID1 = "test-resource-uid-1" upstreamResourceUID2 = "test-resource-uid-2" upstreamIDPType = "oidc" downstreamClientID = "pinniped-cli" downstreamRedirectURL = "http://127.0.0.1:12345/callback" downstreamPKCECodeVerifier = "some-pkce-verifier-that-must-be-at-least-43-characters-to-meet-entropy-requirements" ) var ( upstreamIDPFlows = []string{"browser_authcode"} ) newGetRequest := func(url string) *http.Request { return httptest.NewRequest(http.MethodGet, url, nil) } newPostRequest := func(url, body string) *http.Request { req := httptest.NewRequest(http.MethodPost, url, strings.NewReader(body)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return req } requireDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIssuer string) { recorder := httptest.NewRecorder() subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.WellKnownEndpointPath+requestURLSuffix)) r.False(fallbackHandlerWasCalled) // Minimal check to ensure that the right discovery endpoint was called r.Equal(http.StatusOK, recorder.Code, "unexpected response:", recorder) responseBody, err := io.ReadAll(recorder.Body) r.NoError(err) parsedDiscoveryResult := discovery.Metadata{} err = json.Unmarshal(responseBody, &parsedDiscoveryResult) r.NoError(err) r.Equal(expectedIssuer, parsedDiscoveryResult.Issuer) r.Equal(parsedDiscoveryResult.SupervisorDiscovery.PinnipedIDPsEndpoint, expectedIssuer+oidc.PinnipedIDPsPathV1Alpha1) } requirePinnipedIDPsDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix string, expectedIDPNames []string, expectedIDPTypes string, expectedFlows []string) { recorder := httptest.NewRecorder() subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.PinnipedIDPsPathV1Alpha1+requestURLSuffix)) r.False(fallbackHandlerWasCalled) expectedFlowsJSON, err := json.Marshal(expectedFlows) require.NoError(t, err) expectedIDPJSONList := []string{} for i := range expectedIDPNames { expectedIDPJSONList = append(expectedIDPJSONList, fmt.Sprintf(`{"name":"%s","type":"%s","flows":%s}`, expectedIDPNames[i], expectedIDPTypes, expectedFlowsJSON)) } // Minimal check to ensure that the right IDP discovery endpoint was called r.Equal(http.StatusOK, recorder.Code, "unexpected response:", recorder) responseBody, err := io.ReadAll(recorder.Body) r.NoError(err) r.Equal( fmt.Sprintf(`{"pinniped_identity_providers":[%s]}`+"\n", strings.Join(expectedIDPJSONList, ",")), string(responseBody), ) } requirePinnipedIDPChooserRequestToBeHandled := func(requestIssuer string, expectedIDPNames []string) { recorder := httptest.NewRecorder() requiredParams := url.Values{ "client_id": []string{"foo"}, "redirect_uri": []string{"bar"}, "scope": []string{"baz"}, "response_type": []string{"bat"}, } subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.ChooseIDPEndpointPath+"?"+requiredParams.Encode())) r.False(fallbackHandlerWasCalled) // Minimal check to ensure that the right endpoint was called r.Equal(http.StatusOK, recorder.Code, "unexpected response:", recorder) r.Equal("text/html; charset=utf-8", recorder.Header().Get("Content-Type")) responseBody, err := io.ReadAll(recorder.Body) r.NoError(err) // Should have some buttons whose URLs include the pinniped_idp_name param. r.Contains(string(responseBody), "