eae6d355f8
Signed-off-by: Andrew Keesler <akeesler@vmware.com>
242 lines
7.9 KiB
Go
242 lines
7.9 KiB
Go
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coreos/go-oidc"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
|
|
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1"
|
|
"go.pinniped.dev/pkg/oidcclient/nonce"
|
|
"go.pinniped.dev/pkg/oidcclient/pkce"
|
|
"go.pinniped.dev/pkg/oidcclient/state"
|
|
"go.pinniped.dev/test/library"
|
|
)
|
|
|
|
func TestSupervisorLogin(t *testing.T) {
|
|
t.Skip("waiting on new callback path logic to get merged in from the callback endpoint work")
|
|
|
|
env := library.IntegrationEnv(t)
|
|
client := library.NewSupervisorClientset(t)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
tests := []struct {
|
|
Scheme string
|
|
Address string
|
|
CABundle string
|
|
}{
|
|
{Scheme: "http", Address: env.SupervisorHTTPAddress},
|
|
{Scheme: "https", Address: env.SupervisorHTTPSIngressAddress, CABundle: env.SupervisorHTTPSIngressCABundle},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
scheme := test.Scheme
|
|
addr := test.Address
|
|
caBundle := test.CABundle
|
|
|
|
if addr == "" {
|
|
// Both cases are not required, so when one is empty skip it.
|
|
continue
|
|
}
|
|
|
|
// Create downstream OIDC provider (i.e., update supervisor with OIDC provider).
|
|
path := getDownstreamIssuerPathFromUpstreamRedirectURI(t, env.SupervisorTestUpstream.CallbackURL)
|
|
issuer := fmt.Sprintf("https://%s%s", addr, path)
|
|
_, _ = requireCreatingOIDCProviderCausesDiscoveryEndpointsToAppear(
|
|
ctx,
|
|
t,
|
|
scheme,
|
|
addr,
|
|
caBundle,
|
|
issuer,
|
|
client,
|
|
)
|
|
|
|
// Create HTTP client.
|
|
httpClient := newHTTPClient(t, caBundle, nil)
|
|
httpClient.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
|
|
// Don't follow any redirects right now, since we simply want to validate that our auth endpoint
|
|
// redirects us.
|
|
return http.ErrUseLastResponse
|
|
}
|
|
|
|
// Declare the downstream auth endpoint url we will use.
|
|
downstreamAuthURL := makeDownstreamAuthURL(t, scheme, addr, path)
|
|
|
|
// Make request to auth endpoint - should fail, since we have no upstreams.
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthURL, nil)
|
|
require.NoError(t, err)
|
|
rsp, err := httpClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer rsp.Body.Close()
|
|
require.Equal(t, http.StatusUnprocessableEntity, rsp.StatusCode)
|
|
|
|
// Create upstream OIDC provider.
|
|
spec := idpv1alpha1.UpstreamOIDCProviderSpec{
|
|
Issuer: env.SupervisorTestUpstream.Issuer,
|
|
TLS: &idpv1alpha1.TLSSpec{
|
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)),
|
|
},
|
|
Client: idpv1alpha1.OIDCClient{
|
|
SecretName: makeTestClientCredsSecret(t, env.SupervisorTestUpstream.ClientID, env.SupervisorTestUpstream.ClientSecret).Name,
|
|
},
|
|
}
|
|
upstream := makeTestUpstream(t, spec, idpv1alpha1.PhaseReady)
|
|
|
|
// Make request to authorize endpoint - should pass, since we now have an upstream.
|
|
req, err = http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthURL, nil)
|
|
require.NoError(t, err)
|
|
rsp, err = httpClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer rsp.Body.Close()
|
|
require.Equal(t, http.StatusFound, rsp.StatusCode)
|
|
requireValidRedirectLocation(
|
|
ctx,
|
|
t,
|
|
upstream.Spec.Issuer,
|
|
env.SupervisorTestUpstream.ClientID,
|
|
env.SupervisorTestUpstream.CallbackURL,
|
|
rsp.Header.Get("Location"),
|
|
)
|
|
}
|
|
}
|
|
|
|
//nolint:unused
|
|
func getDownstreamIssuerPathFromUpstreamRedirectURI(t *testing.T, upstreamRedirectURI string) string {
|
|
// We need to construct the downstream issuer path from the upstream redirect URI since the two
|
|
// are related, and the upstream redirect URI is supplied via a static test environment
|
|
// variable. The upstream redirect URI should be something like
|
|
// https://supervisor.com/some/supervisor/path/callback
|
|
// and therefore the downstream issuer should be something like
|
|
// https://supervisor.com/some/supervisor/path
|
|
// since the /callback endpoint is placed at the root of the downstream issuer path.
|
|
upstreamRedirectURL, err := url.Parse(upstreamRedirectURI)
|
|
require.NoError(t, err)
|
|
|
|
redirectURIPathWithoutLastSegment, lastUpstreamRedirectURIPathSegment := path.Split(upstreamRedirectURL.Path)
|
|
require.Equalf(
|
|
t,
|
|
"callback",
|
|
lastUpstreamRedirectURIPathSegment,
|
|
"expected upstream redirect URI (%q) to follow supervisor callback path conventions (i.e., end in /callback)",
|
|
upstreamRedirectURI,
|
|
)
|
|
|
|
if strings.HasSuffix(redirectURIPathWithoutLastSegment, "/") {
|
|
redirectURIPathWithoutLastSegment = redirectURIPathWithoutLastSegment[:len(redirectURIPathWithoutLastSegment)-1]
|
|
}
|
|
|
|
return redirectURIPathWithoutLastSegment
|
|
}
|
|
|
|
//nolint:unused
|
|
func makeDownstreamAuthURL(t *testing.T, scheme, addr, path string) string {
|
|
t.Helper()
|
|
downstreamOAuth2Config := oauth2.Config{
|
|
// This is the hardcoded public client that the supervisor supports.
|
|
ClientID: "pinniped-cli",
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: fmt.Sprintf("%s://%s%s/oauth2/authorize", scheme, addr, path),
|
|
},
|
|
// This is the hardcoded downstream redirect URI that the supervisor supports.
|
|
RedirectURL: "http://127.0.0.1/callback",
|
|
Scopes: []string{"openid"},
|
|
}
|
|
state, nonce, pkce := generateAuthRequestParams(t)
|
|
return downstreamOAuth2Config.AuthCodeURL(
|
|
state.String(),
|
|
nonce.Param(),
|
|
pkce.Challenge(),
|
|
pkce.Method(),
|
|
)
|
|
}
|
|
|
|
//nolint:unused
|
|
func generateAuthRequestParams(t *testing.T) (state.State, nonce.Nonce, pkce.Code) {
|
|
t.Helper()
|
|
state, err := state.Generate()
|
|
require.NoError(t, err)
|
|
nonce, err := nonce.Generate()
|
|
require.NoError(t, err)
|
|
pkce, err := pkce.Generate()
|
|
require.NoError(t, err)
|
|
return state, nonce, pkce
|
|
}
|
|
|
|
//nolint:unused
|
|
func requireValidRedirectLocation(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
issuer, clientID, redirectURI, actualLocation string,
|
|
) {
|
|
t.Helper()
|
|
env := library.IntegrationEnv(t)
|
|
|
|
// Do OIDC discovery on our test issuer to get auth endpoint.
|
|
transport := http.Transport{}
|
|
if env.Proxy != "" {
|
|
transport.Proxy = func(_ *http.Request) (*url.URL, error) {
|
|
return url.Parse(env.Proxy)
|
|
}
|
|
}
|
|
if env.SupervisorTestUpstream.CABundle != "" {
|
|
transport.TLSClientConfig = &tls.Config{RootCAs: x509.NewCertPool()}
|
|
transport.TLSClientConfig.RootCAs.AppendCertsFromPEM([]byte(env.SupervisorTestUpstream.CABundle))
|
|
}
|
|
|
|
ctx = oidc.ClientContext(ctx, &http.Client{Transport: &transport})
|
|
upstreamProvider, err := oidc.NewProvider(ctx, issuer)
|
|
require.NoError(t, err)
|
|
|
|
// Parse expected upstream auth URL.
|
|
expectedLocationURL, err := url.Parse(
|
|
(&oauth2.Config{
|
|
ClientID: clientID,
|
|
Endpoint: upstreamProvider.Endpoint(),
|
|
RedirectURL: redirectURI,
|
|
Scopes: []string{"openid"},
|
|
}).AuthCodeURL("", oauth2.AccessTypeOffline),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Parse actual upstream auth URL.
|
|
actualLocationURL, err := url.Parse(actualLocation)
|
|
require.NoError(t, err)
|
|
|
|
// First make some assertions on the query values. Note that we will not be able to know what
|
|
// certain query values are since they may be random (e.g., state, pkce, nonce).
|
|
expectedLocationQuery := expectedLocationURL.Query()
|
|
actualLocationQuery := actualLocationURL.Query()
|
|
require.NotEmpty(t, actualLocationQuery.Get("state"))
|
|
actualLocationQuery.Del("state")
|
|
require.NotEmpty(t, actualLocationQuery.Get("code_challenge"))
|
|
actualLocationQuery.Del("code_challenge")
|
|
require.NotEmpty(t, actualLocationQuery.Get("code_challenge_method"))
|
|
actualLocationQuery.Del("code_challenge_method")
|
|
require.NotEmpty(t, actualLocationQuery.Get("nonce"))
|
|
actualLocationQuery.Del("nonce")
|
|
require.Equal(t, expectedLocationQuery, actualLocationQuery)
|
|
|
|
// Zero-out query values, since we made specific assertions about those above, and assert that the
|
|
// URL's are equal otherwise.
|
|
expectedLocationURL.RawQuery = ""
|
|
actualLocationURL.RawQuery = ""
|
|
require.Equal(t, expectedLocationURL, actualLocationURL)
|
|
}
|