mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-04-15 07:06:45 +00:00
Merge remote-tracking branch 'upstream/callback-endpoint' into token-endpoint
This commit is contained in:
@@ -20,7 +20,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sclevine/agouti"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
@@ -30,6 +29,7 @@ import (
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||
"go.pinniped.dev/test/library"
|
||||
"go.pinniped.dev/test/library/browsertest"
|
||||
)
|
||||
|
||||
func TestCLIGetKubeconfig(t *testing.T) {
|
||||
@@ -108,80 +108,14 @@ func runPinnipedCLIGetKubeconfig(t *testing.T, pinnipedExe, token, namespaceName
|
||||
return string(output)
|
||||
}
|
||||
|
||||
type loginProviderPatterns struct {
|
||||
Name string
|
||||
IssuerPattern *regexp.Regexp
|
||||
LoginPagePattern *regexp.Regexp
|
||||
UsernameSelector string
|
||||
PasswordSelector string
|
||||
LoginButtonSelector string
|
||||
}
|
||||
|
||||
func getLoginProvider(t *testing.T) *loginProviderPatterns {
|
||||
t.Helper()
|
||||
issuer := library.IntegrationEnv(t).CLITestUpstream.Issuer
|
||||
for _, p := range []loginProviderPatterns{
|
||||
{
|
||||
Name: "Okta",
|
||||
IssuerPattern: regexp.MustCompile(`\Ahttps://.+\.okta\.com/.+\z`),
|
||||
LoginPagePattern: regexp.MustCompile(`\Ahttps://.+\.okta\.com/.+\z`),
|
||||
UsernameSelector: "input#okta-signin-username",
|
||||
PasswordSelector: "input#okta-signin-password",
|
||||
LoginButtonSelector: "input#okta-signin-submit",
|
||||
},
|
||||
{
|
||||
Name: "Dex",
|
||||
IssuerPattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex.*\z`),
|
||||
LoginPagePattern: regexp.MustCompile(`\Ahttps://dex\.dex\.svc\.cluster\.local/dex/auth/local.+\z`),
|
||||
UsernameSelector: "input#login",
|
||||
PasswordSelector: "input#password",
|
||||
LoginButtonSelector: "button#submit-login",
|
||||
},
|
||||
} {
|
||||
if p.IssuerPattern.MatchString(issuer) {
|
||||
return &p
|
||||
}
|
||||
}
|
||||
require.Failf(t, "could not find login provider for issuer %q", issuer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCLILoginOIDC(t *testing.T) {
|
||||
env := library.IntegrationEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Find the login CSS selectors for the test issuer, or fail fast.
|
||||
loginProvider := getLoginProvider(t)
|
||||
|
||||
// Start the browser driver.
|
||||
t.Logf("opening browser driver")
|
||||
caps := agouti.NewCapabilities()
|
||||
if env.Proxy != "" {
|
||||
t.Logf("configuring Chrome to use proxy %q", env.Proxy)
|
||||
caps = caps.Proxy(agouti.ProxyConfig{
|
||||
ProxyType: "manual",
|
||||
HTTPProxy: env.Proxy,
|
||||
SSLProxy: env.Proxy,
|
||||
NoProxy: "127.0.0.1",
|
||||
})
|
||||
}
|
||||
agoutiDriver := agouti.ChromeDriver(
|
||||
agouti.Desired(caps),
|
||||
agouti.ChromeOptions("args", []string{
|
||||
"--no-sandbox",
|
||||
"--ignore-certificate-errors",
|
||||
"--headless", // Comment out this line to see the tests happen in a visible browser window.
|
||||
}),
|
||||
// Uncomment this to see stdout/stderr from chromedriver.
|
||||
// agouti.Debug,
|
||||
)
|
||||
require.NoError(t, agoutiDriver.Start())
|
||||
t.Cleanup(func() { require.NoError(t, agoutiDriver.Stop()) })
|
||||
page, err := agoutiDriver.NewPage(agouti.Browser("chrome"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, page.Reset())
|
||||
page := browsertest.Open(t)
|
||||
|
||||
// Build pinniped CLI.
|
||||
t.Logf("building CLI binary")
|
||||
@@ -262,28 +196,18 @@ func TestCLILoginOIDC(t *testing.T) {
|
||||
t.Logf("navigating to login page")
|
||||
require.NoError(t, page.Navigate(loginURL))
|
||||
|
||||
// Expect to be redirected to the login page.
|
||||
t.Logf("waiting for redirect to %s login page", loginProvider.Name)
|
||||
waitForURL(t, page, loginProvider.LoginPagePattern)
|
||||
// Expect to be redirected to the upstream provider and log in.
|
||||
browsertest.LoginToUpstream(t, page, env.CLITestUpstream)
|
||||
|
||||
// Wait for the login page to be rendered.
|
||||
waitForVisibleElements(t, page, loginProvider.UsernameSelector, loginProvider.PasswordSelector, loginProvider.LoginButtonSelector)
|
||||
|
||||
// Fill in the username and password and click "submit".
|
||||
t.Logf("logging into %s", loginProvider.Name)
|
||||
require.NoError(t, page.First(loginProvider.UsernameSelector).Fill(env.CLITestUpstream.Username))
|
||||
require.NoError(t, page.First(loginProvider.PasswordSelector).Fill(env.CLITestUpstream.Password))
|
||||
require.NoError(t, page.First(loginProvider.LoginButtonSelector).Click())
|
||||
|
||||
// Wait for the login to happen and us be redirected back to a localhost callback.
|
||||
t.Logf("waiting for redirect to localhost callback")
|
||||
// Expect to be redirected to the localhost callback.
|
||||
t.Logf("waiting for redirect to callback")
|
||||
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLITestUpstream.CallbackURL) + `\?.+\z`)
|
||||
waitForURL(t, page, callbackURLPattern)
|
||||
browsertest.WaitForURL(t, page, callbackURLPattern)
|
||||
|
||||
// Wait for the "pre" element that gets rendered for a `text/plain` page, and
|
||||
// assert that it contains the success message.
|
||||
t.Logf("verifying success page")
|
||||
waitForVisibleElements(t, page, "pre")
|
||||
browsertest.WaitForVisibleElements(t, page, "pre")
|
||||
msg, err := page.First("pre").Text()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "you have been logged in and may now close this tab", msg)
|
||||
@@ -361,44 +285,6 @@ func TestCLILoginOIDC(t *testing.T) {
|
||||
require.NotEqual(t, credOutput2.Status.Token, credOutput3.Status.Token)
|
||||
}
|
||||
|
||||
func waitForVisibleElements(t *testing.T, page *agouti.Page, selectors ...string) {
|
||||
t.Helper()
|
||||
require.Eventually(t,
|
||||
func() bool {
|
||||
for _, sel := range selectors {
|
||||
vis, err := page.First(sel).Visible()
|
||||
if !(err == nil && vis) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
10*time.Second,
|
||||
100*time.Millisecond,
|
||||
)
|
||||
}
|
||||
|
||||
func waitForURL(t *testing.T, page *agouti.Page, pat *regexp.Regexp) {
|
||||
var lastURL string
|
||||
require.Eventuallyf(t,
|
||||
func() bool {
|
||||
url, err := page.URL()
|
||||
if err == nil && pat.MatchString(url) {
|
||||
return true
|
||||
}
|
||||
if url != lastURL {
|
||||
t.Logf("saw URL %s", url)
|
||||
lastURL = url
|
||||
}
|
||||
return false
|
||||
},
|
||||
10*time.Second,
|
||||
100*time.Millisecond,
|
||||
"expected to browse to %s, but never got there",
|
||||
pat,
|
||||
)
|
||||
}
|
||||
|
||||
func readAndExpectEmpty(r io.Reader) (err error) {
|
||||
var remainder bytes.Buffer
|
||||
_, err = io.Copy(&remainder, r)
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"go.pinniped.dev/internal/fosite/authorizationcode"
|
||||
"go.pinniped.dev/internal/fositestorage/authorizationcode"
|
||||
"go.pinniped.dev/test/library"
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestAuthorizeCodeStorage(t *testing.T) {
|
||||
// randomly generated HMAC authorization code (see below)
|
||||
code = "TQ72B8YjdEOZyxridYbTLE-pzoK4hpdkZxym5j4EmSc.TKRTgQG41IBQ16FDKTthRdhXfLlNaErcMd9Fy47uXAw"
|
||||
// name of the secret that will be created in Kube
|
||||
name = "pinniped-storage-authorization-codes-jssfhaibxdkiaugxufbsso3bixmfo7fzjvuevxbr35c4xdxolqga"
|
||||
name = "pinniped-storage-authcode-jssfhaibxdkiaugxufbsso3bixmfo7fzjvuevxbr35c4xdxolqga"
|
||||
)
|
||||
|
||||
hmac := compose.NewOAuth2HMACStrategy(&compose.Config{}, []byte("super-secret-32-byte-for-testing"), nil)
|
||||
|
||||
@@ -111,7 +111,7 @@ func TestSupervisorOIDCDiscovery(t *testing.T) {
|
||||
|
||||
// When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving.
|
||||
config6Duplicate1, _ := requireCreatingOIDCProviderCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client)
|
||||
config6Duplicate2 := library.CreateTestOIDCProvider(ctx, t, issuer6, "")
|
||||
config6Duplicate2 := library.CreateTestOIDCProvider(ctx, t, issuer6, "", "")
|
||||
requireStatus(t, client, ns, config6Duplicate1.Name, v1alpha1.DuplicateOIDCProviderStatusCondition)
|
||||
requireStatus(t, client, ns, config6Duplicate2.Name, v1alpha1.DuplicateOIDCProviderStatusCondition)
|
||||
requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6)
|
||||
@@ -136,7 +136,7 @@ func TestSupervisorOIDCDiscovery(t *testing.T) {
|
||||
}
|
||||
|
||||
// When we create a provider with an invalid issuer, the status is set to invalid.
|
||||
badConfig := library.CreateTestOIDCProvider(ctx, t, badIssuer, "")
|
||||
badConfig := library.CreateTestOIDCProvider(ctx, t, badIssuer, "", "")
|
||||
requireStatus(t, client, ns, badConfig.Name, v1alpha1.InvalidOIDCProviderStatusCondition)
|
||||
requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer)
|
||||
requireDeletingOIDCProviderCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer)
|
||||
@@ -162,7 +162,7 @@ func TestSupervisorTLSTerminationWithSNI(t *testing.T) {
|
||||
certSecretName1 := "integration-test-cert-1"
|
||||
|
||||
// Create an OIDCProvider with a spec.tls.secretName.
|
||||
oidcProvider1 := library.CreateTestOIDCProvider(ctx, t, issuer1, certSecretName1)
|
||||
oidcProvider1 := library.CreateTestOIDCProvider(ctx, t, issuer1, certSecretName1, "")
|
||||
requireStatus(t, pinnipedClient, oidcProvider1.Namespace, oidcProvider1.Name, v1alpha1.SuccessOIDCProviderStatusCondition)
|
||||
|
||||
// The spec.tls.secretName Secret does not exist, so the endpoints should fail with TLS errors.
|
||||
@@ -198,7 +198,7 @@ func TestSupervisorTLSTerminationWithSNI(t *testing.T) {
|
||||
certSecretName2 := "integration-test-cert-2"
|
||||
|
||||
// Create an OIDCProvider with a spec.tls.secretName.
|
||||
oidcProvider2 := library.CreateTestOIDCProvider(ctx, t, issuer2, certSecretName2)
|
||||
oidcProvider2 := library.CreateTestOIDCProvider(ctx, t, issuer2, certSecretName2, "")
|
||||
requireStatus(t, pinnipedClient, oidcProvider2.Namespace, oidcProvider2.Name, v1alpha1.SuccessOIDCProviderStatusCondition)
|
||||
|
||||
// Create the Secret.
|
||||
@@ -232,31 +232,30 @@ func TestSupervisorTLSTerminationWithDefaultCerts(t *testing.T) {
|
||||
port = hostAndPortSegments[1]
|
||||
}
|
||||
|
||||
ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
|
||||
ips, err := library.LookupIP(ctx, hostname)
|
||||
require.NoError(t, err)
|
||||
ip := ips[0]
|
||||
ipAsString := ip.String()
|
||||
ipWithPort := ipAsString + ":" + port
|
||||
require.NotEmpty(t, ips)
|
||||
ipWithPort := ips[0].String() + ":" + port
|
||||
|
||||
issuerUsingIPAddress := fmt.Sprintf("%s://%s/issuer1", scheme, ipWithPort)
|
||||
issuerUsingHostname := fmt.Sprintf("%s://%s/issuer1", scheme, address)
|
||||
|
||||
// Create an OIDCProvider without a spec.tls.secretName.
|
||||
oidcProvider1 := library.CreateTestOIDCProvider(ctx, t, issuerUsingIPAddress, "")
|
||||
oidcProvider1 := library.CreateTestOIDCProvider(ctx, t, issuerUsingIPAddress, "", "")
|
||||
requireStatus(t, pinnipedClient, oidcProvider1.Namespace, oidcProvider1.Name, v1alpha1.SuccessOIDCProviderStatusCondition)
|
||||
|
||||
// There is no default TLS cert and the spec.tls.secretName was not set, so the endpoints should fail with TLS errors.
|
||||
requireEndpointHasTLSErrorBecauseCertificatesAreNotReady(t, issuerUsingIPAddress)
|
||||
|
||||
// Create a Secret at the special name which represents the default TLS cert.
|
||||
defaultCA := createTLSCertificateSecret(ctx, t, ns, "cert-hostname-doesnt-matter", []net.IP{ip.IP}, defaultTLSCertSecretName(env), kubeClient)
|
||||
defaultCA := createTLSCertificateSecret(ctx, t, ns, "cert-hostname-doesnt-matter", []net.IP{ips[0]}, defaultTLSCertSecretName(env), kubeClient)
|
||||
|
||||
// Now that the Secret exists, we should be able to access the endpoints by IP address using the CA.
|
||||
_ = requireDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil)
|
||||
|
||||
// Create an OIDCProvider with a spec.tls.secretName.
|
||||
certSecretName := "integration-test-cert-1"
|
||||
oidcProvider2 := library.CreateTestOIDCProvider(ctx, t, issuerUsingHostname, certSecretName)
|
||||
oidcProvider2 := library.CreateTestOIDCProvider(ctx, t, issuerUsingHostname, certSecretName, "")
|
||||
requireStatus(t, pinnipedClient, oidcProvider2.Namespace, oidcProvider2.Name, v1alpha1.SuccessOIDCProviderStatusCondition)
|
||||
|
||||
// Create the Secret.
|
||||
@@ -429,7 +428,7 @@ func requireCreatingOIDCProviderCausesDiscoveryEndpointsToAppear(
|
||||
client pinnipedclientset.Interface,
|
||||
) (*v1alpha1.OIDCProvider, *ExpectedJWKSResponseFormat) {
|
||||
t.Helper()
|
||||
newOIDCProvider := library.CreateTestOIDCProvider(ctx, t, issuerName, "")
|
||||
newOIDCProvider := library.CreateTestOIDCProvider(ctx, t, issuerName, "", "")
|
||||
jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil)
|
||||
requireStatus(t, client, newOIDCProvider.Namespace, newOIDCProvider.Name, v1alpha1.SuccessOIDCProviderStatusCondition)
|
||||
return newOIDCProvider, jwksResult
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestSupervisorOIDCKeys(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
// Create our OPC under test.
|
||||
opc := library.CreateTestOIDCProvider(ctx, t, "", "")
|
||||
opc := library.CreateTestOIDCProvider(ctx, t, "", "", "")
|
||||
|
||||
// Ensure a secret is created with the OPC's JWKS.
|
||||
var updatedOPC *configv1alpha1.OIDCProvider
|
||||
|
||||
@@ -6,236 +6,180 @@ package integration
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1"
|
||||
idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||
"go.pinniped.dev/pkg/oidcclient/pkce"
|
||||
"go.pinniped.dev/pkg/oidcclient/state"
|
||||
"go.pinniped.dev/test/library"
|
||||
"go.pinniped.dev/test/library/browsertest"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
// If anything in this test crashes, dump out the supervisor pod logs.
|
||||
defer library.DumpLogs(t, env.SupervisorNamespace)
|
||||
|
||||
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},
|
||||
}
|
||||
// Infer the downstream issuer URL from the callback associated with the upstream test client registration.
|
||||
issuerURL, err := url.Parse(env.SupervisorTestUpstream.CallbackURL)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(issuerURL.Path, "/callback"))
|
||||
issuerURL.Path = strings.TrimSuffix(issuerURL.Path, "/callback")
|
||||
t.Logf("testing with downstream issuer URL %s", issuerURL.String())
|
||||
|
||||
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)
|
||||
// Generate a CA bundle with which to serve this provider.
|
||||
t.Logf("generating test CA")
|
||||
ca, err := certauthority.New(pkix.Name{CommonName: "Downstream Test CA"}, 1*time.Hour)
|
||||
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,
|
||||
// Create an HTTP client that can reach the downstream discovery endpoint using the CA certs.
|
||||
httpClient := &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{RootCAs: ca.Pool()},
|
||||
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||
if env.Proxy == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return url.Parse(env.Proxy)
|
||||
},
|
||||
}}
|
||||
|
||||
// Use the CA to issue a TLS server cert.
|
||||
t.Logf("issuing test certificate")
|
||||
tlsCert, err := ca.Issue(
|
||||
pkix.Name{CommonName: issuerURL.Hostname()},
|
||||
[]string{issuerURL.Hostname()},
|
||||
nil,
|
||||
1*time.Hour,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
certPEM, keyPEM, err := certauthority.ToPEM(tlsCert)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write the serving cert to a secret.
|
||||
certSecret := library.CreateTestSecret(t,
|
||||
env.SupervisorNamespace,
|
||||
"oidc-provider-tls",
|
||||
"kubernetes.io/tls",
|
||||
map[string]string{"tls.crt": string(certPEM), "tls.key": string(keyPEM)},
|
||||
)
|
||||
|
||||
if strings.HasSuffix(redirectURIPathWithoutLastSegment, "/") {
|
||||
redirectURIPathWithoutLastSegment = redirectURIPathWithoutLastSegment[:len(redirectURIPathWithoutLastSegment)-1]
|
||||
}
|
||||
// Create the downstream OIDCProvider and expect it to go into the success status condition.
|
||||
downstream := library.CreateTestOIDCProvider(ctx, t,
|
||||
issuerURL.String(),
|
||||
certSecret.Name,
|
||||
configv1alpha1.SuccessOIDCProviderStatusCondition,
|
||||
)
|
||||
|
||||
return redirectURIPathWithoutLastSegment
|
||||
}
|
||||
// Create upstream OIDC provider and wait for it to become ready.
|
||||
library.CreateTestUpstreamOIDCProvider(t, idpv1alpha1.UpstreamOIDCProviderSpec{
|
||||
Issuer: env.SupervisorTestUpstream.Issuer,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)),
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: library.CreateClientCredsSecret(t, env.SupervisorTestUpstream.ClientID, env.SupervisorTestUpstream.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
|
||||
//nolint:unused
|
||||
func makeDownstreamAuthURL(t *testing.T, scheme, addr, path string) string {
|
||||
t.Helper()
|
||||
// Perform OIDC discovery for our downstream.
|
||||
var discovery *oidc.Provider
|
||||
assert.Eventually(t, func() bool {
|
||||
discovery, err = oidc.NewProvider(oidc.ClientContext(ctx, httpClient), downstream.Spec.Issuer)
|
||||
return err == nil
|
||||
}, 60*time.Second, 1*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start a callback server on localhost.
|
||||
localCallbackServer := startLocalCallbackServer(t)
|
||||
|
||||
// Form the OAuth2 configuration corresponding to our CLI client.
|
||||
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",
|
||||
ClientID: "pinniped-cli",
|
||||
Endpoint: discovery.Endpoint(),
|
||||
RedirectURL: localCallbackServer.URL,
|
||||
Scopes: []string{"openid"},
|
||||
}
|
||||
state, nonce, pkce := generateAuthRequestParams(t)
|
||||
return downstreamOAuth2Config.AuthCodeURL(
|
||||
state.String(),
|
||||
nonce.Param(),
|
||||
pkce.Challenge(),
|
||||
pkce.Method(),
|
||||
|
||||
// Build a valid downstream authorize URL for the supervisor.
|
||||
stateParam, err := state.Generate()
|
||||
require.NoError(t, err)
|
||||
nonceParam, err := nonce.Generate()
|
||||
require.NoError(t, err)
|
||||
pkceParam, err := pkce.Generate()
|
||||
require.NoError(t, err)
|
||||
downstreamAuthorizeURL := downstreamOAuth2Config.AuthCodeURL(
|
||||
stateParam.String(),
|
||||
nonceParam.Param(),
|
||||
pkceParam.Challenge(),
|
||||
pkceParam.Method(),
|
||||
)
|
||||
|
||||
// Open the web browser and navigate to the downstream authorize URL.
|
||||
page := browsertest.Open(t)
|
||||
t.Logf("opening browser to downstream authorize URL %s", library.MaskTokens(downstreamAuthorizeURL))
|
||||
require.NoError(t, page.Navigate(downstreamAuthorizeURL))
|
||||
|
||||
// Expect to be redirected to the upstream provider and log in.
|
||||
browsertest.LoginToUpstream(t, page, env.SupervisorTestUpstream)
|
||||
|
||||
// Wait for the login to happen and us be redirected back to a localhost callback.
|
||||
t.Logf("waiting for redirect to callback")
|
||||
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(localCallbackServer.URL) + `\?.+\z`)
|
||||
browsertest.WaitForURL(t, page, callbackURLPattern)
|
||||
|
||||
// Expect that our callback handler was invoked.
|
||||
callback := localCallbackServer.waitForCallback(10 * time.Second)
|
||||
t.Logf("got callback request: %s", library.MaskTokens(callback.URL.String()))
|
||||
require.Equal(t, stateParam.String(), callback.URL.Query().Get("state"))
|
||||
require.Equal(t, "openid", callback.URL.Query().Get("scope"))
|
||||
require.NotEmpty(t, callback.URL.Query().Get("code"))
|
||||
}
|
||||
|
||||
//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
|
||||
func startLocalCallbackServer(t *testing.T) *localCallbackServer {
|
||||
// Handle the callback by sending the *http.Request object back through a channel.
|
||||
callbacks := make(chan *http.Request, 1)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callbacks <- r
|
||||
}))
|
||||
server.URL += "/callback"
|
||||
t.Cleanup(server.Close)
|
||||
t.Cleanup(func() { close(callbacks) })
|
||||
return &localCallbackServer{Server: server, t: t, callbacks: callbacks}
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func requireValidRedirectLocation(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
issuer, clientID, redirectURI, actualLocation string,
|
||||
) {
|
||||
t.Helper()
|
||||
env := library.IntegrationEnv(t)
|
||||
type localCallbackServer struct {
|
||||
*httptest.Server
|
||||
t *testing.T
|
||||
callbacks <-chan *http.Request
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
func (s *localCallbackServer) waitForCallback(timeout time.Duration) *http.Request {
|
||||
select {
|
||||
case callback := <-s.callbacks:
|
||||
return callback
|
||||
case <-time.After(timeout):
|
||||
require.Fail(s.t, "timed out waiting for callback request")
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -4,13 +4,10 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1"
|
||||
@@ -28,7 +25,7 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
|
||||
SecretName: "does-not-exist",
|
||||
},
|
||||
}
|
||||
upstream := makeTestUpstream(t, spec, v1alpha1.PhaseError)
|
||||
upstream := library.CreateTestUpstreamOIDCProvider(t, spec, v1alpha1.PhaseError)
|
||||
expectUpstreamConditions(t, upstream, []v1alpha1.Condition{
|
||||
{
|
||||
Type: "ClientCredentialsValid",
|
||||
@@ -56,10 +53,10 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
|
||||
AdditionalScopes: []string{"email", "profile"},
|
||||
},
|
||||
Client: v1alpha1.OIDCClient{
|
||||
SecretName: makeTestClientCredsSecret(t, "test-client-id", "test-client-secret").Name,
|
||||
SecretName: library.CreateClientCredsSecret(t, "test-client-id", "test-client-secret").Name,
|
||||
},
|
||||
}
|
||||
upstream := makeTestUpstream(t, spec, v1alpha1.PhaseReady)
|
||||
upstream := library.CreateTestUpstreamOIDCProvider(t, spec, v1alpha1.PhaseReady)
|
||||
expectUpstreamConditions(t, upstream, []v1alpha1.Condition{
|
||||
{
|
||||
Type: "ClientCredentialsValid",
|
||||
@@ -87,74 +84,3 @@ func expectUpstreamConditions(t *testing.T, upstream *v1alpha1.UpstreamOIDCProvi
|
||||
}
|
||||
require.ElementsMatch(t, expected, normalized)
|
||||
}
|
||||
|
||||
func makeTestClientCredsSecret(t *testing.T, clientID string, clientSecret string) *corev1.Secret {
|
||||
t.Helper()
|
||||
env := library.IntegrationEnv(t)
|
||||
client := library.NewClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
created, err := client.CoreV1().Secrets(env.SupervisorNamespace).Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: env.SupervisorNamespace,
|
||||
GenerateName: "test-client-creds-",
|
||||
Labels: map[string]string{"pinniped.dev/test": ""},
|
||||
Annotations: map[string]string{"pinniped.dev/testName": t.Name()},
|
||||
},
|
||||
Type: "secrets.pinniped.dev/oidc-client",
|
||||
StringData: map[string]string{
|
||||
"clientID": clientID,
|
||||
"clientSecret": clientSecret,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := client.CoreV1().Secrets(env.SupervisorNamespace).Delete(context.Background(), created.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Logf("created test client credentials Secret %s", created.Name)
|
||||
return created
|
||||
}
|
||||
|
||||
func makeTestUpstream(t *testing.T, spec v1alpha1.UpstreamOIDCProviderSpec, expectedPhase v1alpha1.UpstreamOIDCProviderPhase) *v1alpha1.UpstreamOIDCProvider {
|
||||
t.Helper()
|
||||
env := library.IntegrationEnv(t)
|
||||
client := library.NewSupervisorClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create the UpstreamOIDCProvider using GenerateName to get a random name.
|
||||
created, err := client.IDPV1alpha1().
|
||||
UpstreamOIDCProviders(env.SupervisorNamespace).
|
||||
Create(ctx, &v1alpha1.UpstreamOIDCProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: env.SupervisorNamespace,
|
||||
GenerateName: "test-upstream-",
|
||||
Labels: map[string]string{"pinniped.dev/test": ""},
|
||||
Annotations: map[string]string{"pinniped.dev/testName": t.Name()},
|
||||
},
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Always clean this up after this point.
|
||||
t.Cleanup(func() {
|
||||
err := client.IDPV1alpha1().
|
||||
UpstreamOIDCProviders(env.SupervisorNamespace).
|
||||
Delete(context.Background(), created.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Logf("created test UpstreamOIDCProvider %s", created.Name)
|
||||
|
||||
// Wait for the UpstreamOIDCProvider to enter the expected phase (or time out).
|
||||
var result *v1alpha1.UpstreamOIDCProvider
|
||||
require.Eventuallyf(t, func() bool {
|
||||
var err error
|
||||
result, err = client.IDPV1alpha1().
|
||||
UpstreamOIDCProviders(created.Namespace).Get(ctx, created.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
return result.Status.Phase == expectedPhase
|
||||
}, 60*time.Second, 1*time.Second, "expected the UpstreamOIDCProvider to go into phase %s", expectedPhase)
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user