2021-04-07 23:12:13 +00:00
|
|
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
2020-10-08 21:40:56 +00:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package manager
|
|
|
|
|
|
|
|
import (
|
2020-12-03 01:39:45 +00:00
|
|
|
"context"
|
2020-12-03 20:34:58 +00:00
|
|
|
"crypto/ecdsa"
|
2020-10-08 21:40:56 +00:00
|
|
|
"encoding/json"
|
2021-05-11 17:31:33 +00:00
|
|
|
"fmt"
|
2020-10-08 21:40:56 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2020-11-05 01:06:47 +00:00
|
|
|
"net/url"
|
2020-10-08 21:40:56 +00:00
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
2020-12-11 01:27:02 +00:00
|
|
|
"go.pinniped.dev/internal/secret"
|
|
|
|
|
2020-10-08 21:40:56 +00:00
|
|
|
"github.com/sclevine/spec"
|
|
|
|
"github.com/stretchr/testify/require"
|
2020-10-17 00:51:40 +00:00
|
|
|
"gopkg.in/square/go-jose.v2"
|
2020-12-03 01:39:45 +00:00
|
|
|
"k8s.io/client-go/kubernetes/fake"
|
2020-10-08 21:40:56 +00:00
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
"go.pinniped.dev/internal/here"
|
2020-10-08 21:40:56 +00:00
|
|
|
"go.pinniped.dev/internal/oidc"
|
|
|
|
"go.pinniped.dev/internal/oidc/discovery"
|
2020-10-17 00:51:40 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/jwks"
|
2020-10-08 21:40:56 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/provider"
|
2020-12-04 15:06:55 +00:00
|
|
|
"go.pinniped.dev/internal/testutil"
|
2021-04-09 00:28:01 +00:00
|
|
|
"go.pinniped.dev/internal/testutil/oidctestutil"
|
2020-12-03 01:39:45 +00:00
|
|
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
|
|
|
"go.pinniped.dev/pkg/oidcclient/oidctypes"
|
|
|
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
2020-10-08 21:40:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestManager(t *testing.T) {
|
|
|
|
spec.Run(t, "ServeHTTP", func(t *testing.T, when spec.G, it spec.S) {
|
2020-10-17 00:51:40 +00:00
|
|
|
var (
|
|
|
|
r *require.Assertions
|
|
|
|
subject *Manager
|
|
|
|
nextHandler http.HandlerFunc
|
|
|
|
fallbackHandlerWasCalled bool
|
|
|
|
dynamicJWKSProvider jwks.DynamicJWKSProvider
|
2020-12-03 01:39:45 +00:00
|
|
|
kubeClient *fake.Clientset
|
2020-10-17 00:51:40 +00:00
|
|
|
)
|
|
|
|
|
2020-11-05 01:06:47 +00:00
|
|
|
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"
|
|
|
|
upstreamIDPAuthorizationURL = "https://test-upstream.com/auth"
|
2021-04-28 20:14:21 +00:00
|
|
|
upstreamIDPName = "test-idp"
|
|
|
|
upstreamIDPType = "oidc"
|
2020-12-03 20:34:58 +00:00
|
|
|
downstreamClientID = "pinniped-cli"
|
2020-11-20 15:42:43 +00:00
|
|
|
downstreamRedirectURL = "http://127.0.0.1:12345/callback"
|
2020-12-03 20:34:58 +00:00
|
|
|
|
|
|
|
downstreamPKCECodeVerifier = "some-pkce-verifier-that-must-be-at-least-43-characters-to-meet-entropy-requirements"
|
2020-11-05 01:06:47 +00:00
|
|
|
)
|
2020-10-08 21:40:56 +00:00
|
|
|
|
|
|
|
newGetRequest := func(url string) *http.Request {
|
|
|
|
return httptest.NewRequest(http.MethodGet, url, nil)
|
|
|
|
}
|
|
|
|
|
2020-12-03 20:34:58 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-05-11 17:31:33 +00:00
|
|
|
requireDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIssuer string) {
|
2020-10-08 21:40:56 +00:00
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
2020-10-23 23:25:44 +00:00
|
|
|
subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.WellKnownEndpointPath+requestURLSuffix))
|
2020-10-08 21:40:56 +00:00
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
|
2020-11-05 01:06:47 +00:00
|
|
|
// Minimal check to ensure that the right discovery endpoint was called
|
2020-10-08 21:40:56 +00:00
|
|
|
r.Equal(http.StatusOK, recorder.Code)
|
|
|
|
responseBody, err := ioutil.ReadAll(recorder.Body)
|
|
|
|
r.NoError(err)
|
|
|
|
parsedDiscoveryResult := discovery.Metadata{}
|
|
|
|
err = json.Unmarshal(responseBody, &parsedDiscoveryResult)
|
|
|
|
r.NoError(err)
|
2021-04-28 20:14:21 +00:00
|
|
|
r.Equal(expectedIssuer, parsedDiscoveryResult.Issuer)
|
2021-05-13 17:05:56 +00:00
|
|
|
r.Equal(parsedDiscoveryResult.SupervisorDiscovery.PinnipedIDPsEndpoint, expectedIssuer+oidc.PinnipedIDPsPathV1Alpha1)
|
2021-05-11 17:31:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
requirePinnipedIDPsDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIDPName, expectedIDPType string) {
|
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
2021-05-13 17:05:56 +00:00
|
|
|
subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.PinnipedIDPsPathV1Alpha1+requestURLSuffix))
|
2021-05-11 17:31:33 +00:00
|
|
|
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
|
|
|
|
// Minimal check to ensure that the right IDP discovery endpoint was called
|
|
|
|
r.Equal(http.StatusOK, recorder.Code)
|
|
|
|
responseBody, err := ioutil.ReadAll(recorder.Body)
|
|
|
|
r.NoError(err)
|
|
|
|
r.Equal(
|
|
|
|
fmt.Sprintf(`{"pinniped_identity_providers":[{"name":"%s","type":"%s"}]}`+"\n", expectedIDPName, expectedIDPType),
|
|
|
|
string(responseBody),
|
|
|
|
)
|
2020-10-08 21:40:56 +00:00
|
|
|
}
|
|
|
|
|
2020-12-03 01:39:45 +00:00
|
|
|
requireAuthorizationRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedRedirectLocationPrefix string) (string, string) {
|
2020-11-05 01:06:47 +00:00
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
|
|
|
subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.AuthorizationEndpointPath+requestURLSuffix))
|
|
|
|
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
|
|
|
|
// Minimal check to ensure that the right endpoint was called
|
|
|
|
r.Equal(http.StatusFound, recorder.Code)
|
|
|
|
actualLocation := recorder.Header().Get("Location")
|
|
|
|
r.True(
|
|
|
|
strings.HasPrefix(actualLocation, expectedRedirectLocationPrefix),
|
|
|
|
"actual location %s did not start with expected prefix %s",
|
|
|
|
actualLocation, expectedRedirectLocationPrefix,
|
|
|
|
)
|
2020-12-03 01:39:45 +00:00
|
|
|
|
|
|
|
parsedLocation, err := url.Parse(actualLocation)
|
|
|
|
r.NoError(err)
|
|
|
|
redirectStateParam := parsedLocation.Query().Get("state")
|
|
|
|
r.NotEmpty(redirectStateParam)
|
|
|
|
|
2020-12-10 00:29:25 +00:00
|
|
|
cookies := recorder.Result().Cookies() //nolint:bodyclose
|
|
|
|
r.Len(cookies, 1)
|
|
|
|
csrfCookie := cookies[0]
|
|
|
|
r.Equal("__Host-pinniped-csrf", csrfCookie.Name)
|
|
|
|
r.NotEmpty(csrfCookie.Value)
|
2020-12-03 01:39:45 +00:00
|
|
|
|
|
|
|
// Return the important parts of the response so we can use them in our next request to the callback endpoint
|
2020-12-10 00:29:25 +00:00
|
|
|
return csrfCookie.Value, redirectStateParam
|
2020-11-05 01:06:47 +00:00
|
|
|
}
|
|
|
|
|
2020-12-03 20:34:58 +00:00
|
|
|
requireCallbackRequestToBeHandled := func(requestIssuer, requestURLSuffix, csrfCookieValue string) string {
|
2020-11-20 15:42:43 +00:00
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
2020-12-03 01:39:45 +00:00
|
|
|
numberOfKubeActionsBeforeThisRequest := len(kubeClient.Actions())
|
|
|
|
|
|
|
|
getRequest := newGetRequest(requestIssuer + oidc.CallbackEndpointPath + requestURLSuffix)
|
|
|
|
getRequest.AddCookie(&http.Cookie{
|
|
|
|
Name: "__Host-pinniped-csrf",
|
|
|
|
Value: csrfCookieValue,
|
|
|
|
})
|
|
|
|
subject.ServeHTTP(recorder, getRequest)
|
2020-11-20 15:42:43 +00:00
|
|
|
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
|
2020-12-03 01:39:45 +00:00
|
|
|
// Check just enough of the response to ensure that we wired up the callback endpoint correctly.
|
|
|
|
// The endpoint's own unit tests cover everything else.
|
|
|
|
r.Equal(http.StatusFound, recorder.Code)
|
|
|
|
actualLocation := recorder.Header().Get("Location")
|
|
|
|
r.True(
|
|
|
|
strings.HasPrefix(actualLocation, downstreamRedirectURL),
|
|
|
|
"actual location %s did not start with expected prefix %s",
|
|
|
|
actualLocation, downstreamRedirectURL,
|
|
|
|
)
|
|
|
|
parsedLocation, err := url.Parse(actualLocation)
|
|
|
|
r.NoError(err)
|
|
|
|
actualLocationQueryParams := parsedLocation.Query()
|
|
|
|
r.Contains(actualLocationQueryParams, "code")
|
|
|
|
r.Equal("openid", actualLocationQueryParams.Get("scope"))
|
2020-12-12 01:39:58 +00:00
|
|
|
r.Equal("some-state-value-with-enough-bytes-to-exceed-min-allowed", actualLocationQueryParams.Get("state"))
|
2020-12-03 01:39:45 +00:00
|
|
|
|
|
|
|
// Make sure that we wired up the callback endpoint to use kube storage for fosite sessions.
|
|
|
|
r.Equal(len(kubeClient.Actions()), numberOfKubeActionsBeforeThisRequest+3,
|
|
|
|
"did not perform any kube actions during the callback request, but should have")
|
2020-12-03 20:34:58 +00:00
|
|
|
|
|
|
|
// Return the important parts of the response so we can use them in our next request to the token endpoint.
|
|
|
|
return actualLocationQueryParams.Get("code")
|
|
|
|
}
|
|
|
|
|
2020-12-04 01:16:08 +00:00
|
|
|
requireTokenRequestToBeHandled := func(requestIssuer, authCode string, jwks *jose.JSONWebKeySet, jwkIssuer string) {
|
2020-12-03 20:34:58 +00:00
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
|
|
|
numberOfKubeActionsBeforeThisRequest := len(kubeClient.Actions())
|
|
|
|
|
|
|
|
tokenRequestBody := url.Values{
|
|
|
|
"code": []string{authCode},
|
|
|
|
"client_id": []string{downstreamClientID},
|
|
|
|
"redirect_uri": []string{downstreamRedirectURL},
|
|
|
|
"code_verifier": []string{downstreamPKCECodeVerifier},
|
|
|
|
"grant_type": []string{"authorization_code"},
|
|
|
|
}.Encode()
|
|
|
|
subject.ServeHTTP(recorder, newPostRequest(requestIssuer+oidc.TokenEndpointPath, tokenRequestBody))
|
|
|
|
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
|
|
|
|
// Minimal check to ensure that the right endpoint was called
|
|
|
|
var body map[string]interface{}
|
|
|
|
r.Equal(http.StatusOK, recorder.Code)
|
|
|
|
r.NoError(json.Unmarshal(recorder.Body.Bytes(), &body))
|
|
|
|
r.Contains(body, "id_token")
|
|
|
|
r.Contains(body, "access_token")
|
|
|
|
|
|
|
|
// Validate ID token is signed by the correct JWK to make sure we wired the token endpoint
|
|
|
|
// signing key correctly.
|
|
|
|
idToken, ok := body["id_token"].(string)
|
|
|
|
r.True(ok, "wanted id_token type to be string, but was %T", body["id_token"])
|
|
|
|
|
|
|
|
r.GreaterOrEqual(len(jwks.Keys), 1)
|
|
|
|
privateKey, ok := jwks.Keys[0].Key.(*ecdsa.PrivateKey)
|
|
|
|
r.True(ok, "wanted private key to be *ecdsa.PrivateKey, but was %T", jwks.Keys[0].Key)
|
|
|
|
|
2020-12-04 15:06:55 +00:00
|
|
|
oidctestutil.VerifyECDSAIDToken(t, jwkIssuer, downstreamClientID, privateKey, idToken)
|
2020-12-03 20:34:58 +00:00
|
|
|
|
|
|
|
// Make sure that we wired up the callback endpoint to use kube storage for fosite sessions.
|
2020-12-04 22:31:06 +00:00
|
|
|
r.Equal(len(kubeClient.Actions()), numberOfKubeActionsBeforeThisRequest+8,
|
2020-12-03 20:34:58 +00:00
|
|
|
"did not perform any kube actions during the callback request, but should have")
|
2020-11-20 15:42:43 +00:00
|
|
|
}
|
|
|
|
|
2020-12-03 20:34:58 +00:00
|
|
|
requireJWKSRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedJWKKeyID string) *jose.JSONWebKeySet {
|
2020-10-17 00:51:40 +00:00
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
|
|
|
|
subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.JWKSEndpointPath+requestURLSuffix))
|
|
|
|
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
|
2020-11-05 01:06:47 +00:00
|
|
|
// Minimal check to ensure that the right JWKS endpoint was called
|
2020-10-17 00:51:40 +00:00
|
|
|
r.Equal(http.StatusOK, recorder.Code)
|
|
|
|
responseBody, err := ioutil.ReadAll(recorder.Body)
|
|
|
|
r.NoError(err)
|
|
|
|
parsedJWKSResult := jose.JSONWebKeySet{}
|
|
|
|
err = json.Unmarshal(responseBody, &parsedJWKSResult)
|
|
|
|
r.NoError(err)
|
|
|
|
r.Equal(expectedJWKKeyID, parsedJWKSResult.Keys[0].KeyID)
|
2020-12-03 20:34:58 +00:00
|
|
|
|
|
|
|
return &parsedJWKSResult
|
2020-10-17 00:51:40 +00:00
|
|
|
}
|
|
|
|
|
2020-10-08 21:40:56 +00:00
|
|
|
it.Before(func() {
|
|
|
|
r = require.New(t)
|
|
|
|
nextHandler = func(http.ResponseWriter, *http.Request) {
|
|
|
|
fallbackHandlerWasCalled = true
|
|
|
|
}
|
2020-10-17 00:51:40 +00:00
|
|
|
dynamicJWKSProvider = jwks.NewDynamicJWKSProvider()
|
2020-11-05 01:06:47 +00:00
|
|
|
|
|
|
|
parsedUpstreamIDPAuthorizationURL, err := url.Parse(upstreamIDPAuthorizationURL)
|
|
|
|
r.NoError(err)
|
2021-04-07 23:12:13 +00:00
|
|
|
idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{
|
2021-04-28 20:14:21 +00:00
|
|
|
Name: upstreamIDPName,
|
2020-11-18 21:38:13 +00:00
|
|
|
ClientID: "test-client-id",
|
|
|
|
AuthorizationURL: *parsedUpstreamIDPAuthorizationURL,
|
|
|
|
Scopes: []string{"test-scope"},
|
2020-12-04 21:33:36 +00:00
|
|
|
ExchangeAuthcodeAndValidateTokensFunc: func(ctx context.Context, authcode string, pkceCodeVerifier pkce.Code, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) {
|
|
|
|
return &oidctypes.Token{
|
|
|
|
IDToken: &oidctypes.IDToken{
|
|
|
|
Claims: map[string]interface{}{
|
|
|
|
"iss": "https://some-issuer.com",
|
|
|
|
"sub": "some-subject",
|
|
|
|
"username": "test-username",
|
|
|
|
"groups": "test-group1",
|
|
|
|
},
|
2020-12-03 01:39:45 +00:00
|
|
|
},
|
2020-12-04 21:33:36 +00:00
|
|
|
}, nil
|
2020-12-03 01:39:45 +00:00
|
|
|
},
|
2021-04-07 23:12:13 +00:00
|
|
|
}).Build()
|
2020-11-05 01:06:47 +00:00
|
|
|
|
2020-12-03 01:39:45 +00:00
|
|
|
kubeClient = fake.NewSimpleClientset()
|
|
|
|
secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
|
|
|
|
|
2020-12-11 01:27:02 +00:00
|
|
|
cache := secret.Cache{}
|
|
|
|
cache.SetCSRFCookieEncoderHashKey([]byte("fake-csrf-hash-secret"))
|
|
|
|
|
2020-12-14 16:32:11 +00:00
|
|
|
cache.SetTokenHMACKey(issuer1, []byte("some secret 1 - must have at least 32 bytes"))
|
|
|
|
cache.SetStateEncoderHashKey(issuer1, []byte("some-state-encoder-hash-key-1"))
|
|
|
|
cache.SetStateEncoderBlockKey(issuer1, []byte("16-bytes-STATE01"))
|
|
|
|
|
|
|
|
cache.SetTokenHMACKey(issuer2, []byte("some secret 2 - must have at least 32 bytes"))
|
|
|
|
cache.SetStateEncoderHashKey(issuer2, []byte("some-state-encoder-hash-key-2"))
|
|
|
|
cache.SetStateEncoderBlockKey(issuer2, []byte("16-bytes-STATE02"))
|
2020-12-14 15:36:45 +00:00
|
|
|
|
2021-04-07 23:12:13 +00:00
|
|
|
subject = NewManager(nextHandler, dynamicJWKSProvider, idpLister, &cache, secretsClient)
|
2020-10-08 21:40:56 +00:00
|
|
|
})
|
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
when("given no providers via SetProviders()", func() {
|
2020-10-08 21:40:56 +00:00
|
|
|
it("sends all requests to the nextHandler", func() {
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
subject.ServeHTTP(httptest.NewRecorder(), newGetRequest("/anything"))
|
|
|
|
r.True(fallbackHandlerWasCalled)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2020-12-03 20:34:58 +00:00
|
|
|
newTestJWK := func(keyID string) *jose.JSONWebKey {
|
2020-10-17 00:51:40 +00:00
|
|
|
testJWKSJSONString := here.Docf(`
|
|
|
|
{
|
|
|
|
"use": "sig",
|
|
|
|
"kty": "EC",
|
|
|
|
"kid": "%s",
|
|
|
|
"crv": "P-256",
|
|
|
|
"alg": "ES256",
|
2020-12-03 20:34:58 +00:00
|
|
|
"x": "9c_oMKjd_ruVIy4pA5y9quT1E-Fampx0w270OtPx5Ng",
|
|
|
|
"y": "-Y-9nfrtJdFPl-9kzXv55-Fq9Oo2AWDg5zZBs9P-Vds",
|
|
|
|
"d": "LXdNChAEtGKETBzYXiL_G0wESXceBuajE_EBQbcRuGg"
|
2020-10-17 00:51:40 +00:00
|
|
|
}
|
|
|
|
`, keyID)
|
|
|
|
k := jose.JSONWebKey{}
|
|
|
|
r.NoError(json.Unmarshal([]byte(testJWKSJSONString), &k))
|
2020-12-03 20:34:58 +00:00
|
|
|
return &k
|
2020-10-17 00:51:40 +00:00
|
|
|
}
|
2020-10-08 21:40:56 +00:00
|
|
|
|
2020-11-05 01:06:47 +00:00
|
|
|
requireRoutesMatchingRequestsToAppropriateProvider := func() {
|
2021-05-11 17:31:33 +00:00
|
|
|
requireDiscoveryRequestToBeHandled(issuer1, "", issuer1)
|
|
|
|
requireDiscoveryRequestToBeHandled(issuer2, "", issuer2)
|
|
|
|
requireDiscoveryRequestToBeHandled(issuer2, "?some=query", issuer2)
|
|
|
|
|
|
|
|
// Hostnames are case-insensitive, so test that we can handle that.
|
|
|
|
requireDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", issuer1)
|
|
|
|
requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2)
|
|
|
|
requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2)
|
|
|
|
|
|
|
|
requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1, "", upstreamIDPName, upstreamIDPType)
|
|
|
|
requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "", upstreamIDPName, upstreamIDPType)
|
|
|
|
requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "?some=query", upstreamIDPName, upstreamIDPType)
|
2020-11-05 01:06:47 +00:00
|
|
|
|
|
|
|
// Hostnames are case-insensitive, so test that we can handle that.
|
2021-05-11 17:31:33 +00:00
|
|
|
requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", upstreamIDPName, upstreamIDPType)
|
|
|
|
requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", upstreamIDPName, upstreamIDPType)
|
|
|
|
requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", upstreamIDPName, upstreamIDPType)
|
2020-11-05 01:06:47 +00:00
|
|
|
|
2020-12-03 20:34:58 +00:00
|
|
|
issuer1JWKS := requireJWKSRequestToBeHandled(issuer1, "", issuer1KeyID)
|
|
|
|
issuer2JWKS := requireJWKSRequestToBeHandled(issuer2, "", issuer2KeyID)
|
2020-11-05 01:06:47 +00:00
|
|
|
requireJWKSRequestToBeHandled(issuer2, "?some=query", issuer2KeyID)
|
|
|
|
|
|
|
|
// Hostnames are case-insensitive, so test that we can handle that.
|
|
|
|
requireJWKSRequestToBeHandled(issuer1DifferentCaseHostname, "", issuer1KeyID)
|
|
|
|
requireJWKSRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2KeyID)
|
|
|
|
requireJWKSRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2KeyID)
|
|
|
|
|
|
|
|
authRequestParams := "?" + url.Values{
|
|
|
|
"response_type": []string{"code"},
|
|
|
|
"scope": []string{"openid profile email"},
|
2020-12-03 20:34:58 +00:00
|
|
|
"client_id": []string{downstreamClientID},
|
2020-12-12 01:39:58 +00:00
|
|
|
"state": []string{"some-state-value-with-enough-bytes-to-exceed-min-allowed"},
|
|
|
|
"nonce": []string{"some-nonce-value-with-enough-bytes-to-exceed-min-allowed"},
|
2020-12-04 15:06:55 +00:00
|
|
|
"code_challenge": []string{testutil.SHA256(downstreamPKCECodeVerifier)},
|
2020-11-05 01:06:47 +00:00
|
|
|
"code_challenge_method": []string{"S256"},
|
2020-11-20 15:42:43 +00:00
|
|
|
"redirect_uri": []string{downstreamRedirectURL},
|
2020-11-05 01:06:47 +00:00
|
|
|
}.Encode()
|
|
|
|
|
|
|
|
requireAuthorizationRequestToBeHandled(issuer1, authRequestParams, upstreamIDPAuthorizationURL)
|
|
|
|
requireAuthorizationRequestToBeHandled(issuer2, authRequestParams, upstreamIDPAuthorizationURL)
|
|
|
|
|
|
|
|
// Hostnames are case-insensitive, so test that we can handle that.
|
2020-12-14 15:36:45 +00:00
|
|
|
csrfCookieValue1, upstreamStateParam1 :=
|
|
|
|
requireAuthorizationRequestToBeHandled(issuer1DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL)
|
|
|
|
csrfCookieValue2, upstreamStateParam2 :=
|
2020-12-03 01:39:45 +00:00
|
|
|
requireAuthorizationRequestToBeHandled(issuer2DifferentCaseHostname, authRequestParams, upstreamIDPAuthorizationURL)
|
2020-11-20 15:42:43 +00:00
|
|
|
|
2020-12-14 15:36:45 +00:00
|
|
|
callbackRequestParams1 := "?" + url.Values{
|
|
|
|
"code": []string{"some-fake-code"},
|
|
|
|
"state": []string{upstreamStateParam1},
|
|
|
|
}.Encode()
|
|
|
|
callbackRequestParams2 := "?" + url.Values{
|
2020-12-03 01:39:45 +00:00
|
|
|
"code": []string{"some-fake-code"},
|
2020-12-14 15:36:45 +00:00
|
|
|
"state": []string{upstreamStateParam2},
|
2020-11-20 15:42:43 +00:00
|
|
|
}.Encode()
|
|
|
|
|
2020-12-14 15:36:45 +00:00
|
|
|
downstreamAuthCode1 := requireCallbackRequestToBeHandled(issuer1, callbackRequestParams1, csrfCookieValue1)
|
|
|
|
downstreamAuthCode2 := requireCallbackRequestToBeHandled(issuer2, callbackRequestParams2, csrfCookieValue2)
|
2020-11-20 15:42:43 +00:00
|
|
|
|
2020-12-04 01:16:08 +00:00
|
|
|
// Hostnames are case-insensitive, so test that we can handle that.
|
2020-12-14 15:36:45 +00:00
|
|
|
downstreamAuthCode3 := requireCallbackRequestToBeHandled(issuer1DifferentCaseHostname, callbackRequestParams1, csrfCookieValue1)
|
|
|
|
downstreamAuthCode4 := requireCallbackRequestToBeHandled(issuer2DifferentCaseHostname, callbackRequestParams2, csrfCookieValue2)
|
2020-12-03 20:34:58 +00:00
|
|
|
|
2020-12-04 01:16:08 +00:00
|
|
|
requireTokenRequestToBeHandled(issuer1, downstreamAuthCode1, issuer1JWKS, issuer1)
|
|
|
|
requireTokenRequestToBeHandled(issuer2, downstreamAuthCode2, issuer2JWKS, issuer2)
|
2020-12-03 20:34:58 +00:00
|
|
|
|
|
|
|
// Hostnames are case-insensitive, so test that we can handle that.
|
2020-12-04 01:16:08 +00:00
|
|
|
requireTokenRequestToBeHandled(issuer1DifferentCaseHostname, downstreamAuthCode3, issuer1JWKS, issuer1)
|
|
|
|
requireTokenRequestToBeHandled(issuer2DifferentCaseHostname, downstreamAuthCode4, issuer2JWKS, issuer2)
|
2020-11-05 01:06:47 +00:00
|
|
|
}
|
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
when("given some valid providers via SetProviders()", func() {
|
2020-10-08 21:40:56 +00:00
|
|
|
it.Before(func() {
|
2020-12-17 19:34:49 +00:00
|
|
|
p1, err := provider.NewFederationDomainIssuer(issuer1)
|
2020-10-08 21:40:56 +00:00
|
|
|
r.NoError(err)
|
2020-12-17 19:34:49 +00:00
|
|
|
p2, err := provider.NewFederationDomainIssuer(issuer2)
|
2020-10-08 21:40:56 +00:00
|
|
|
r.NoError(err)
|
|
|
|
subject.SetProviders(p1, p2)
|
2020-10-17 00:51:40 +00:00
|
|
|
|
2020-12-11 01:28:47 +00:00
|
|
|
jwksMap := map[string]*jose.JSONWebKeySet{
|
2020-12-03 20:34:58 +00:00
|
|
|
issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}},
|
|
|
|
issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}},
|
|
|
|
}
|
|
|
|
activeJWK := map[string]*jose.JSONWebKey{
|
|
|
|
issuer1: newTestJWK(issuer1KeyID),
|
|
|
|
issuer2: newTestJWK(issuer2KeyID),
|
|
|
|
}
|
2020-12-11 01:28:47 +00:00
|
|
|
dynamicJWKSProvider.SetIssuerToJWKSMap(jwksMap, activeJWK)
|
2020-10-08 21:40:56 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
it("sends all non-matching host requests to the nextHandler", func() {
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
2020-11-05 01:06:47 +00:00
|
|
|
wrongHostURL := strings.ReplaceAll(issuer1+oidc.WellKnownEndpointPath, "example.com", "wrong-host.com")
|
|
|
|
subject.ServeHTTP(httptest.NewRecorder(), newGetRequest(wrongHostURL))
|
2020-10-08 21:40:56 +00:00
|
|
|
r.True(fallbackHandlerWasCalled)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("sends all non-matching path requests to the nextHandler", func() {
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
subject.ServeHTTP(httptest.NewRecorder(), newGetRequest("https://example.com/path-does-not-match-any-provider"))
|
|
|
|
r.True(fallbackHandlerWasCalled)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("sends requests which match the issuer prefix but do not match any of that provider's known paths to the nextHandler", func() {
|
|
|
|
r.False(fallbackHandlerWasCalled)
|
|
|
|
subject.ServeHTTP(httptest.NewRecorder(), newGetRequest(issuer1+"/unhandled-sub-path"))
|
|
|
|
r.True(fallbackHandlerWasCalled)
|
|
|
|
})
|
|
|
|
|
|
|
|
it("routes matching requests to the appropriate provider", func() {
|
2020-11-05 01:06:47 +00:00
|
|
|
requireRoutesMatchingRequestsToAppropriateProvider()
|
2020-10-08 21:40:56 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
when("given the same valid providers as arguments to SetProviders() in reverse order", func() {
|
2020-10-08 21:40:56 +00:00
|
|
|
it.Before(func() {
|
2020-12-17 19:34:49 +00:00
|
|
|
p1, err := provider.NewFederationDomainIssuer(issuer1)
|
2020-10-08 21:40:56 +00:00
|
|
|
r.NoError(err)
|
2020-12-17 19:34:49 +00:00
|
|
|
p2, err := provider.NewFederationDomainIssuer(issuer2)
|
2020-10-08 21:40:56 +00:00
|
|
|
r.NoError(err)
|
|
|
|
subject.SetProviders(p2, p1)
|
2020-10-17 00:51:40 +00:00
|
|
|
|
2020-12-11 01:28:47 +00:00
|
|
|
jwksMap := map[string]*jose.JSONWebKeySet{
|
2020-12-03 20:34:58 +00:00
|
|
|
issuer1: {Keys: []jose.JSONWebKey{*newTestJWK(issuer1KeyID)}},
|
|
|
|
issuer2: {Keys: []jose.JSONWebKey{*newTestJWK(issuer2KeyID)}},
|
|
|
|
}
|
|
|
|
activeJWK := map[string]*jose.JSONWebKey{
|
|
|
|
issuer1: newTestJWK(issuer1KeyID),
|
|
|
|
issuer2: newTestJWK(issuer2KeyID),
|
|
|
|
}
|
2020-12-11 01:28:47 +00:00
|
|
|
dynamicJWKSProvider.SetIssuerToJWKSMap(jwksMap, activeJWK)
|
2020-10-08 21:40:56 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
it("still routes matching requests to the appropriate provider", func() {
|
2020-11-05 01:06:47 +00:00
|
|
|
requireRoutesMatchingRequestsToAppropriateProvider()
|
2020-10-08 21:40:56 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|