2021-01-07 22:58:09 +00:00
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
2020-11-17 15:21:17 +00:00
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"context"
"crypto/tls"
"encoding/base64"
2020-12-09 16:23:10 +00:00
"encoding/json"
2020-12-17 01:59:39 +00:00
"fmt"
2021-04-07 19:56:09 +00:00
"io/ioutil"
2020-11-17 15:21:17 +00:00
"net/http"
2020-12-02 21:50:42 +00:00
"net/http/httptest"
2020-11-17 15:21:17 +00:00
"net/url"
2020-12-02 21:50:42 +00:00
"regexp"
2020-11-30 14:58:08 +00:00
"strings"
2020-11-17 15:21:17 +00:00
"testing"
"time"
2021-01-20 17:54:44 +00:00
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
2020-12-03 00:07:52 +00:00
"github.com/stretchr/testify/assert"
2020-11-17 15:21:17 +00:00
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
2021-04-06 17:10:01 +00:00
v1 "k8s.io/api/core/v1"
2020-11-17 15:21:17 +00:00
2021-02-16 19:00:08 +00:00
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
2020-12-02 21:50:42 +00:00
"go.pinniped.dev/internal/certauthority"
2020-12-10 18:14:54 +00:00
"go.pinniped.dev/internal/oidc"
2020-12-05 01:07:04 +00:00
"go.pinniped.dev/internal/testutil"
2020-11-17 18:46:54 +00:00
"go.pinniped.dev/pkg/oidcclient/nonce"
"go.pinniped.dev/pkg/oidcclient/pkce"
"go.pinniped.dev/pkg/oidcclient/state"
2021-06-22 15:23:19 +00:00
"go.pinniped.dev/test/testlib"
"go.pinniped.dev/test/testlib/browsertest"
2020-11-17 15:21:17 +00:00
)
func TestSupervisorLogin ( t * testing . T ) {
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
2021-04-07 19:56:09 +00:00
2021-04-06 17:10:01 +00:00
tests := [ ] struct {
2021-04-07 19:56:09 +00:00
name string
2021-05-28 23:12:57 +00:00
maybeSkip func ( t * testing . T )
2021-04-07 19:56:09 +00:00
createIDP func ( t * testing . T )
requestAuthorization func ( t * testing . T , downstreamAuthorizeURL , downstreamCallbackURL string , httpClient * http . Client )
wantDownstreamIDTokenSubjectToMatch string
wantDownstreamIDTokenUsernameToMatch string
2021-05-17 18:10:26 +00:00
wantDownstreamIDTokenGroups [ ] string
2021-04-06 17:10:01 +00:00
} {
{
2021-05-28 17:37:46 +00:00
name : "oidc with default username and groups claim settings" ,
2021-05-29 00:06:01 +00:00
maybeSkip : func ( t * testing . T ) {
// never need to skip this test
} ,
2021-04-06 17:10:01 +00:00
createIDP : func ( t * testing . T ) {
t . Helper ( )
2021-06-22 15:23:19 +00:00
testlib . CreateTestOIDCIdentityProvider ( t , idpv1alpha1 . OIDCIdentityProviderSpec {
2021-04-07 19:56:09 +00:00
Issuer : env . SupervisorUpstreamOIDC . Issuer ,
2021-04-06 17:10:01 +00:00
TLS : & idpv1alpha1 . TLSSpec {
2021-04-07 19:56:09 +00:00
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamOIDC . CABundle ) ) ,
2021-04-06 17:10:01 +00:00
} ,
Client : idpv1alpha1 . OIDCClient {
2021-06-22 15:23:19 +00:00
SecretName : testlib . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
2021-04-06 17:10:01 +00:00
} ,
} , idpv1alpha1 . PhaseReady )
} ,
requestAuthorization : requestAuthorizationUsingOIDCIdentityProvider ,
2021-04-07 19:56:09 +00:00
// the ID token Subject should include the upstream user ID after the upstream issuer name
wantDownstreamIDTokenSubjectToMatch : regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" ,
// the ID token Username should include the upstream user ID after the upstream issuer name
wantDownstreamIDTokenUsernameToMatch : regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" ,
2021-05-28 17:37:46 +00:00
} ,
{
name : "oidc with custom username and groups claim settings" ,
2021-05-29 00:06:01 +00:00
maybeSkip : func ( t * testing . T ) {
// never need to skip this test
} ,
2021-05-28 17:37:46 +00:00
createIDP : func ( t * testing . T ) {
t . Helper ( )
2021-06-22 15:23:19 +00:00
testlib . CreateTestOIDCIdentityProvider ( t , idpv1alpha1 . OIDCIdentityProviderSpec {
2021-05-28 17:37:46 +00:00
Issuer : env . SupervisorUpstreamOIDC . Issuer ,
TLS : & idpv1alpha1 . TLSSpec {
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamOIDC . CABundle ) ) ,
} ,
Client : idpv1alpha1 . OIDCClient {
2021-06-22 15:23:19 +00:00
SecretName : testlib . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
2021-05-28 17:37:46 +00:00
} ,
Claims : idpv1alpha1 . OIDCClaims {
Username : env . SupervisorUpstreamOIDC . UsernameClaim ,
Groups : env . SupervisorUpstreamOIDC . GroupsClaim ,
} ,
AuthorizationConfig : idpv1alpha1 . OIDCAuthorizationConfig {
AdditionalScopes : env . SupervisorUpstreamOIDC . AdditionalScopes ,
} ,
} , idpv1alpha1 . PhaseReady )
} ,
requestAuthorization : requestAuthorizationUsingOIDCIdentityProvider ,
wantDownstreamIDTokenSubjectToMatch : regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" ,
wantDownstreamIDTokenUsernameToMatch : regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Username ) ,
2021-05-17 18:10:26 +00:00
wantDownstreamIDTokenGroups : env . SupervisorUpstreamOIDC . ExpectedGroups ,
2021-04-06 17:10:01 +00:00
} ,
{
2021-05-20 20:39:48 +00:00
name : "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS" ,
2021-05-28 23:12:57 +00:00
maybeSkip : func ( t * testing . T ) {
t . Helper ( )
2021-06-22 15:23:19 +00:00
if len ( env . ToolsNamespace ) == 0 && ! env . HasCapability ( testlib . CanReachInternetLDAPPorts ) {
2021-05-28 23:12:57 +00:00
t . Skip ( "LDAP integration test requires connectivity to an LDAP server" )
}
} ,
2021-04-06 17:10:01 +00:00
createIDP : func ( t * testing . T ) {
t . Helper ( )
2021-06-22 15:23:19 +00:00
secret := testlib . CreateTestSecret ( t , env . SupervisorNamespace , "ldap-service-account" , v1 . SecretTypeBasicAuth ,
2021-04-07 19:56:09 +00:00
map [ string ] string {
v1 . BasicAuthUsernameKey : env . SupervisorUpstreamLDAP . BindUsername ,
v1 . BasicAuthPasswordKey : env . SupervisorUpstreamLDAP . BindPassword ,
} ,
)
2021-06-22 15:23:19 +00:00
ldapIDP := testlib . CreateTestLDAPIdentityProvider ( t , idpv1alpha1 . LDAPIdentityProviderSpec {
2021-04-07 19:56:09 +00:00
Host : env . SupervisorUpstreamLDAP . Host ,
2021-04-27 19:43:09 +00:00
TLS : & idpv1alpha1 . TLSSpec {
2021-04-14 01:11:16 +00:00
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . CABundle ) ) ,
2021-04-07 19:56:09 +00:00
} ,
2021-04-27 19:43:09 +00:00
Bind : idpv1alpha1 . LDAPIdentityProviderBind {
2021-04-07 19:56:09 +00:00
SecretName : secret . Name ,
} ,
2021-04-27 19:43:09 +00:00
UserSearch : idpv1alpha1 . LDAPIdentityProviderUserSearch {
2021-04-07 19:56:09 +00:00
Base : env . SupervisorUpstreamLDAP . UserSearchBase ,
Filter : "" ,
2021-04-27 19:43:09 +00:00
Attributes : idpv1alpha1 . LDAPIdentityProviderUserSearchAttributes {
2021-04-07 19:56:09 +00:00
Username : env . SupervisorUpstreamLDAP . TestUserMailAttributeName ,
2021-04-27 19:43:09 +00:00
UID : env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeName ,
2021-04-07 19:56:09 +00:00
} ,
} ,
2021-05-17 18:10:26 +00:00
GroupSearch : idpv1alpha1 . LDAPIdentityProviderGroupSearch {
Base : env . SupervisorUpstreamLDAP . GroupSearchBase ,
Filter : "" ,
Attributes : idpv1alpha1 . LDAPIdentityProviderGroupSearchAttributes {
GroupName : "dn" ,
} ,
} ,
2021-04-14 01:11:16 +00:00
} , idpv1alpha1 . LDAPPhaseReady )
2021-04-16 21:04:05 +00:00
expectedMsg := fmt . Sprintf (
2021-04-28 21:26:57 +00:00
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
env . SupervisorUpstreamLDAP . Host , env . SupervisorUpstreamLDAP . BindUsername ,
2021-04-16 21:04:05 +00:00
secret . Name , secret . ResourceVersion ,
)
requireSuccessfulLDAPIdentityProviderConditions ( t , ldapIDP , expectedMsg )
2021-04-07 19:56:09 +00:00
} ,
requestAuthorization : func ( t * testing . T , downstreamAuthorizeURL , _ string , httpClient * http . Client ) {
requestAuthorizationUsingLDAPIdentityProvider ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
)
2021-04-06 17:10:01 +00:00
} ,
2021-04-07 19:56:09 +00:00
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch : regexp . QuoteMeta (
2021-05-27 20:47:10 +00:00
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
2021-04-07 19:56:09 +00:00
) ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) ,
2021-05-17 18:10:26 +00:00
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
2021-04-06 17:10:01 +00:00
} ,
2021-04-16 21:04:05 +00:00
{
2021-05-20 20:39:48 +00:00
name : "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS" , // try another variation of configuration options
2021-05-28 23:12:57 +00:00
maybeSkip : func ( t * testing . T ) {
t . Helper ( )
2021-06-22 15:23:19 +00:00
if len ( env . ToolsNamespace ) == 0 && ! env . HasCapability ( testlib . CanReachInternetLDAPPorts ) {
2021-05-28 23:12:57 +00:00
t . Skip ( "LDAP integration test requires connectivity to an LDAP server" )
}
} ,
2021-04-16 21:04:05 +00:00
createIDP : func ( t * testing . T ) {
t . Helper ( )
2021-06-22 15:23:19 +00:00
secret := testlib . CreateTestSecret ( t , env . SupervisorNamespace , "ldap-service-account" , v1 . SecretTypeBasicAuth ,
2021-04-16 21:04:05 +00:00
map [ string ] string {
v1 . BasicAuthUsernameKey : env . SupervisorUpstreamLDAP . BindUsername ,
v1 . BasicAuthPasswordKey : env . SupervisorUpstreamLDAP . BindPassword ,
} ,
)
2021-06-22 15:23:19 +00:00
ldapIDP := testlib . CreateTestLDAPIdentityProvider ( t , idpv1alpha1 . LDAPIdentityProviderSpec {
2021-05-20 20:39:48 +00:00
Host : env . SupervisorUpstreamLDAP . StartTLSOnlyHost ,
2021-04-27 19:43:09 +00:00
TLS : & idpv1alpha1 . TLSSpec {
2021-04-16 21:04:05 +00:00
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . CABundle ) ) ,
} ,
2021-04-27 19:43:09 +00:00
Bind : idpv1alpha1 . LDAPIdentityProviderBind {
2021-04-16 21:04:05 +00:00
SecretName : secret . Name ,
} ,
2021-04-27 19:43:09 +00:00
UserSearch : idpv1alpha1 . LDAPIdentityProviderUserSearch {
2021-04-16 21:04:05 +00:00
Base : env . SupervisorUpstreamLDAP . UserSearchBase ,
Filter : "cn={}" , // try using a non-default search filter
2021-04-27 19:43:09 +00:00
Attributes : idpv1alpha1 . LDAPIdentityProviderUserSearchAttributes {
2021-04-16 21:04:05 +00:00
Username : "dn" , // try using the user's DN as the downstream username
2021-04-27 19:43:09 +00:00
UID : env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeName ,
2021-04-16 21:04:05 +00:00
} ,
} ,
2021-05-17 18:10:26 +00:00
GroupSearch : idpv1alpha1 . LDAPIdentityProviderGroupSearch {
Base : env . SupervisorUpstreamLDAP . GroupSearchBase ,
Filter : "" ,
Attributes : idpv1alpha1 . LDAPIdentityProviderGroupSearchAttributes {
GroupName : "cn" ,
} ,
} ,
2021-04-16 21:04:05 +00:00
} , idpv1alpha1 . LDAPPhaseReady )
expectedMsg := fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
2021-05-20 20:39:48 +00:00
env . SupervisorUpstreamLDAP . StartTLSOnlyHost , env . SupervisorUpstreamLDAP . BindUsername ,
2021-04-16 21:04:05 +00:00
secret . Name , secret . ResourceVersion ,
)
requireSuccessfulLDAPIdentityProviderConditions ( t , ldapIDP , expectedMsg )
} ,
requestAuthorization : func ( t * testing . T , downstreamAuthorizeURL , _ string , httpClient * http . Client ) {
requestAuthorizationUsingLDAPIdentityProvider ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserCN , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
)
} ,
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch : regexp . QuoteMeta (
2021-05-27 20:47:10 +00:00
"ldaps://" + env . SupervisorUpstreamLDAP . StartTLSOnlyHost +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
2021-04-16 21:04:05 +00:00
) ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserDN ) ,
2021-05-17 18:10:26 +00:00
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsCNs ,
2021-04-16 21:04:05 +00:00
} ,
2021-04-06 17:10:01 +00:00
}
for _ , test := range tests {
2021-05-28 23:12:57 +00:00
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
tt . maybeSkip ( t )
2021-04-07 19:56:09 +00:00
testSupervisorLogin ( t ,
2021-05-28 23:12:57 +00:00
tt . createIDP ,
tt . requestAuthorization ,
tt . wantDownstreamIDTokenSubjectToMatch ,
tt . wantDownstreamIDTokenUsernameToMatch ,
tt . wantDownstreamIDTokenGroups ,
2021-04-07 19:56:09 +00:00
)
2021-04-06 17:10:01 +00:00
} )
}
}
2021-04-16 21:04:05 +00:00
func requireSuccessfulLDAPIdentityProviderConditions ( t * testing . T , ldapIDP * idpv1alpha1 . LDAPIdentityProvider , expectedLDAPConnectionValidMessage string ) {
require . Len ( t , ldapIDP . Status . Conditions , 3 )
conditionsSummary := [ ] [ ] string { }
for _ , condition := range ldapIDP . Status . Conditions {
conditionsSummary = append ( conditionsSummary , [ ] string { condition . Type , string ( condition . Status ) , condition . Reason } )
t . Logf ( "Saw LDAPIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s" ,
condition . Type , string ( condition . Status ) , condition . Reason , condition . Message )
switch condition . Type {
case "BindSecretValid" :
require . Equal ( t , "loaded bind secret" , condition . Message )
case "TLSConfigurationValid" :
require . Equal ( t , "loaded TLS configuration" , condition . Message )
case "LDAPConnectionValid" :
require . Equal ( t , expectedLDAPConnectionValidMessage , condition . Message )
}
}
require . ElementsMatch ( t , [ ] [ ] string {
{ "BindSecretValid" , "True" , "Success" } ,
{ "TLSConfigurationValid" , "True" , "Success" } ,
{ "LDAPConnectionValid" , "True" , "Success" } ,
} , conditionsSummary )
}
2021-04-06 17:10:01 +00:00
func testSupervisorLogin (
t * testing . T ,
createIDP func ( t * testing . T ) ,
2021-04-07 19:56:09 +00:00
requestAuthorization func ( t * testing . T , downstreamAuthorizeURL , downstreamCallbackURL string , httpClient * http . Client ) ,
2021-05-17 18:10:26 +00:00
wantDownstreamIDTokenSubjectToMatch , wantDownstreamIDTokenUsernameToMatch string , wantDownstreamIDTokenGroups [ ] string ,
2021-04-06 17:10:01 +00:00
) {
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
2020-12-03 15:35:28 +00:00
2020-11-17 15:21:17 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
defer cancel ( )
2020-12-02 21:50:42 +00:00
// Infer the downstream issuer URL from the callback associated with the upstream test client registration.
2021-04-07 19:56:09 +00:00
issuerURL , err := url . Parse ( env . SupervisorUpstreamOIDC . CallbackURL )
2020-12-02 21:50:42 +00:00
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 ( ) )
2020-11-17 15:21:17 +00:00
2020-12-02 21:50:42 +00:00
// Generate a CA bundle with which to serve this provider.
t . Logf ( "generating test CA" )
2021-03-13 00:09:16 +00:00
ca , err := certauthority . New ( "Downstream Test CA" , 1 * time . Hour )
2020-12-02 21:50:42 +00:00
require . NoError ( t , err )
2020-11-17 15:21:17 +00:00
2020-12-02 21:50:42 +00:00
// Create an HTTP client that can reach the downstream discovery endpoint using the CA certs.
2020-12-16 03:42:11 +00:00
httpClient := & http . Client {
Transport : & http . Transport {
TLSClientConfig : & tls . Config { RootCAs : ca . Pool ( ) } ,
Proxy : func ( req * http . Request ) ( * url . URL , error ) {
2021-04-14 01:11:16 +00:00
if strings . HasPrefix ( req . URL . Host , "127.0.0.1" ) {
// don't proxy requests to localhost to avoid proxying calls to our local callback listener
return nil , nil
}
2020-12-16 03:42:11 +00:00
if env . Proxy == "" {
2021-06-22 15:23:19 +00:00
t . Logf ( "passing request for %s with no proxy" , testlib . RedactURLParams ( req . URL ) )
2020-12-16 03:42:11 +00:00
return nil , nil
}
proxyURL , err := url . Parse ( env . Proxy )
require . NoError ( t , err )
2021-06-22 15:23:19 +00:00
t . Logf ( "passing request for %s through proxy %s" , testlib . RedactURLParams ( req . URL ) , proxyURL . String ( ) )
2020-12-16 03:42:11 +00:00
return proxyURL , nil
} ,
2020-12-02 21:50:42 +00:00
} ,
2020-12-16 03:42:11 +00:00
// Don't follow redirects automatically.
CheckRedirect : func ( req * http . Request , via [ ] * http . Request ) error {
return http . ErrUseLastResponse
} ,
}
2020-12-10 18:14:54 +00:00
oidcHTTPClientContext := coreosoidc . ClientContext ( ctx , httpClient )
2020-12-02 21:50:42 +00:00
// Use the CA to issue a TLS server cert.
t . Logf ( "issuing test certificate" )
2021-03-13 00:09:16 +00:00
tlsCert , err := ca . IssueServerCert ( [ ] string { issuerURL . Hostname ( ) } , nil , 1 * time . Hour )
2020-12-02 21:50:42 +00:00
require . NoError ( t , err )
certPEM , keyPEM , err := certauthority . ToPEM ( tlsCert )
2020-11-30 14:58:08 +00:00
require . NoError ( t , err )
2020-12-02 21:50:42 +00:00
// Write the serving cert to a secret.
2021-06-22 15:23:19 +00:00
certSecret := testlib . CreateTestSecret ( t ,
2020-12-02 21:50:42 +00:00
env . SupervisorNamespace ,
"oidc-provider-tls" ,
2020-12-18 23:10:17 +00:00
v1 . SecretTypeTLS ,
2020-12-02 21:50:42 +00:00
map [ string ] string { "tls.crt" : string ( certPEM ) , "tls.key" : string ( keyPEM ) } ,
2020-11-30 14:58:08 +00:00
)
2020-12-16 22:27:09 +00:00
// Create the downstream FederationDomain and expect it to go into the success status condition.
2021-06-22 15:23:19 +00:00
downstream := testlib . CreateTestFederationDomain ( ctx , t ,
2020-12-02 21:50:42 +00:00
issuerURL . String ( ) ,
certSecret . Name ,
2020-12-16 22:27:09 +00:00
configv1alpha1 . SuccessFederationDomainStatusCondition ,
2020-12-02 21:50:42 +00:00
)
2020-11-30 14:58:08 +00:00
2020-12-17 01:59:39 +00:00
// Ensure the the JWKS data is created and ready for the new FederationDomain by waiting for
// the `/jwks.json` endpoint to succeed, because there is no point in proceeding and eventually
// calling the token endpoint from this test until the JWKS data has been loaded into
// the server's in-memory JWKS cache for the token endpoint to use.
requestJWKSEndpoint , err := http . NewRequestWithContext (
ctx ,
http . MethodGet ,
fmt . Sprintf ( "%s/jwks.json" , issuerURL . String ( ) ) ,
nil ,
)
require . NoError ( t , err )
2021-06-22 15:23:19 +00:00
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
2020-12-17 01:59:39 +00:00
rsp , err := httpClient . Do ( requestJWKSEndpoint )
2021-06-16 22:51:23 +00:00
requireEventually . NoError ( err )
requireEventually . NoError ( rsp . Body . Close ( ) )
requireEventually . Equal ( http . StatusOK , rsp . StatusCode )
2020-12-17 01:59:39 +00:00
} , 30 * time . Second , 200 * time . Millisecond )
2021-04-06 17:10:01 +00:00
// Create upstream IDP and wait for it to become ready.
createIDP ( t )
2020-12-02 21:50:42 +00:00
// Perform OIDC discovery for our downstream.
2020-12-10 18:14:54 +00:00
var discovery * coreosoidc . Provider
2021-06-22 15:23:19 +00:00
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
2021-06-16 22:51:23 +00:00
var err error
2020-12-10 18:14:54 +00:00
discovery , err = coreosoidc . NewProvider ( oidcHTTPClientContext , downstream . Spec . Issuer )
2021-06-16 22:51:23 +00:00
requireEventually . NoError ( err )
2020-12-03 19:22:27 +00:00
} , 30 * time . Second , 200 * time . Millisecond )
2020-12-02 21:50:42 +00:00
// Start a callback server on localhost.
localCallbackServer := startLocalCallbackServer ( t )
2020-11-30 14:58:08 +00:00
2020-12-02 21:50:42 +00:00
// Form the OAuth2 configuration corresponding to our CLI client.
2020-11-17 15:21:17 +00:00
downstreamOAuth2Config := oauth2 . Config {
// This is the hardcoded public client that the supervisor supports.
2020-12-02 21:50:42 +00:00
ClientID : "pinniped-cli" ,
Endpoint : discovery . Endpoint ( ) ,
RedirectURL : localCallbackServer . URL ,
2020-12-16 03:59:57 +00:00
Scopes : [ ] string { "openid" , "pinniped:request-audience" , "offline_access" } ,
2020-11-17 15:21:17 +00:00
}
2020-12-02 21:50:42 +00:00
// Build a valid downstream authorize URL for the supervisor.
stateParam , err := state . Generate ( )
2020-11-17 15:21:17 +00:00
require . NoError ( t , err )
2020-12-02 21:50:42 +00:00
nonceParam , err := nonce . Generate ( )
2020-11-17 15:21:17 +00:00
require . NoError ( t , err )
2020-12-02 21:50:42 +00:00
pkceParam , err := pkce . Generate ( )
2020-11-17 15:21:17 +00:00
require . NoError ( t , err )
2020-12-02 21:50:42 +00:00
downstreamAuthorizeURL := downstreamOAuth2Config . AuthCodeURL (
stateParam . String ( ) ,
nonceParam . Param ( ) ,
pkceParam . Challenge ( ) ,
pkceParam . Method ( ) ,
)
2020-11-17 15:21:17 +00:00
2021-04-06 17:10:01 +00:00
// Perform parameterized auth code acquisition.
2021-04-07 19:56:09 +00:00
requestAuthorization ( t , downstreamAuthorizeURL , localCallbackServer . URL , httpClient )
2020-12-02 21:50:42 +00:00
// Expect that our callback handler was invoked.
callback := localCallbackServer . waitForCallback ( 10 * time . Second )
2021-06-22 15:23:19 +00:00
t . Logf ( "got callback request: %s" , testlib . MaskTokens ( callback . URL . String ( ) ) )
2020-12-02 21:50:42 +00:00
require . Equal ( t , stateParam . String ( ) , callback . URL . Query ( ) . Get ( "state" ) )
2020-12-16 03:59:57 +00:00
require . ElementsMatch ( t , [ ] string { "openid" , "pinniped:request-audience" , "offline_access" } , strings . Split ( callback . URL . Query ( ) . Get ( "scope" ) , " " ) )
2020-12-05 01:07:04 +00:00
authcode := callback . URL . Query ( ) . Get ( "code" )
require . NotEmpty ( t , authcode )
2020-12-16 13:26:44 +00:00
// Call the token endpoint to get tokens.
tokenResponse , err := downstreamOAuth2Config . Exchange ( oidcHTTPClientContext , authcode , pkceParam . Verifier ( ) )
2020-12-05 01:07:04 +00:00
require . NoError ( t , err )
2021-01-14 22:21:41 +00:00
expectedIDTokenClaims := [ ] string { "iss" , "exp" , "sub" , "aud" , "auth_time" , "iat" , "jti" , "nonce" , "rat" , "username" , "groups" }
2021-04-07 19:56:09 +00:00
verifyTokenResponse ( t ,
tokenResponse , discovery , downstreamOAuth2Config , nonceParam ,
2021-05-17 18:10:26 +00:00
expectedIDTokenClaims , wantDownstreamIDTokenSubjectToMatch , wantDownstreamIDTokenUsernameToMatch , wantDownstreamIDTokenGroups )
2020-12-10 01:07:37 +00:00
// token exchange on the original token
doTokenExchange ( t , & downstreamOAuth2Config , tokenResponse , httpClient , discovery )
// Use the refresh token to get new tokens
refreshSource := downstreamOAuth2Config . TokenSource ( oidcHTTPClientContext , & oauth2 . Token { RefreshToken : tokenResponse . RefreshToken } )
refreshedTokenResponse , err := refreshSource . Token ( )
require . NoError ( t , err )
2021-05-28 15:56:33 +00:00
// When refreshing, expect to get an "at_hash" claim, but no "nonce" claim.
expectRefreshedIDTokenClaims := [ ] string { "iss" , "exp" , "sub" , "aud" , "auth_time" , "iat" , "jti" , "rat" , "username" , "groups" , "at_hash" }
2021-04-07 19:56:09 +00:00
verifyTokenResponse ( t ,
refreshedTokenResponse , discovery , downstreamOAuth2Config , "" ,
2021-05-28 15:56:33 +00:00
expectRefreshedIDTokenClaims , wantDownstreamIDTokenSubjectToMatch , wantDownstreamIDTokenUsernameToMatch , wantDownstreamIDTokenGroups )
2020-12-10 01:07:37 +00:00
require . NotEqual ( t , tokenResponse . AccessToken , refreshedTokenResponse . AccessToken )
require . NotEqual ( t , tokenResponse . RefreshToken , refreshedTokenResponse . RefreshToken )
require . NotEqual ( t , tokenResponse . Extra ( "id_token" ) , refreshedTokenResponse . Extra ( "id_token" ) )
// token exchange on the refreshed token
doTokenExchange ( t , & downstreamOAuth2Config , refreshedTokenResponse , httpClient , discovery )
}
func verifyTokenResponse (
t * testing . T ,
tokenResponse * oauth2 . Token ,
2020-12-10 18:14:54 +00:00
discovery * coreosoidc . Provider ,
2020-12-10 01:07:37 +00:00
downstreamOAuth2Config oauth2 . Config ,
nonceParam nonce . Nonce ,
expectedIDTokenClaims [ ] string ,
2021-05-17 18:10:26 +00:00
wantDownstreamIDTokenSubjectToMatch , wantDownstreamIDTokenUsernameToMatch string , wantDownstreamIDTokenGroups [ ] string ,
2020-12-10 01:07:37 +00:00
) {
2021-03-05 01:25:43 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Minute )
2020-12-10 01:07:37 +00:00
defer cancel ( )
2020-12-05 01:07:04 +00:00
// Verify the ID Token.
rawIDToken , ok := tokenResponse . Extra ( "id_token" ) . ( string )
require . True ( t , ok , "expected to get an ID token but did not" )
2020-12-10 18:14:54 +00:00
var verifier = discovery . Verifier ( & coreosoidc . Config { ClientID : downstreamOAuth2Config . ClientID } )
2020-12-05 01:07:04 +00:00
idToken , err := verifier . Verify ( ctx , rawIDToken )
require . NoError ( t , err )
2021-04-07 19:56:09 +00:00
// Check the sub claim of the ID token.
require . Regexp ( t , wantDownstreamIDTokenSubjectToMatch , idToken . Subject )
// Check the nonce claim of the ID token.
2020-12-05 01:07:04 +00:00
require . NoError ( t , nonceParam . Validate ( idToken ) )
2021-04-07 19:56:09 +00:00
// Check the exp claim of the ID token.
2020-12-10 18:14:54 +00:00
expectedIDTokenLifetime := oidc . DefaultOIDCTimeoutsConfiguration ( ) . IDTokenLifespan
testutil . RequireTimeInDelta ( t , time . Now ( ) . UTC ( ) . Add ( expectedIDTokenLifetime ) , idToken . Expiry , time . Second * 30 )
2021-04-07 19:56:09 +00:00
// Check the full list of claim names of the ID token.
2020-12-05 01:07:04 +00:00
idTokenClaims := map [ string ] interface { } { }
err = idToken . Claims ( & idTokenClaims )
require . NoError ( t , err )
idTokenClaimNames := [ ] string { }
for k := range idTokenClaims {
idTokenClaimNames = append ( idTokenClaimNames , k )
}
2020-12-10 01:07:37 +00:00
require . ElementsMatch ( t , expectedIDTokenClaims , idTokenClaimNames )
2021-04-07 19:56:09 +00:00
// Check username claim of the ID token.
require . Regexp ( t , wantDownstreamIDTokenUsernameToMatch , idTokenClaims [ "username" ] . ( string ) )
2020-12-05 01:07:04 +00:00
2021-05-17 18:10:26 +00:00
// Check the groups claim.
require . ElementsMatch ( t , wantDownstreamIDTokenGroups , idTokenClaims [ "groups" ] )
2020-12-05 01:07:04 +00:00
// Some light verification of the other tokens that were returned.
require . NotEmpty ( t , tokenResponse . AccessToken )
require . Equal ( t , "bearer" , tokenResponse . TokenType )
require . NotZero ( t , tokenResponse . Expiry )
2020-12-10 18:14:54 +00:00
expectedAccessTokenLifetime := oidc . DefaultOIDCTimeoutsConfiguration ( ) . AccessTokenLifespan
testutil . RequireTimeInDelta ( t , time . Now ( ) . UTC ( ) . Add ( expectedAccessTokenLifetime ) , tokenResponse . Expiry , time . Second * 30 )
2020-12-05 01:07:04 +00:00
2020-12-10 01:07:37 +00:00
require . NotEmpty ( t , tokenResponse . RefreshToken )
2020-12-02 21:50:42 +00:00
}
2020-11-17 15:21:17 +00:00
2021-04-14 01:11:16 +00:00
func requestAuthorizationUsingOIDCIdentityProvider ( t * testing . T , downstreamAuthorizeURL , downstreamCallbackURL string , httpClient * http . Client ) {
2021-04-06 17:10:01 +00:00
t . Helper ( )
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
2021-04-06 17:10:01 +00:00
2021-04-14 01:11:16 +00:00
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Minute )
defer cancelFunc ( )
// Make the authorize request once "manually" so we can check its response security headers.
authorizeRequest , err := http . NewRequestWithContext ( ctx , http . MethodGet , downstreamAuthorizeURL , nil )
require . NoError ( t , err )
authorizeResp , err := httpClient . Do ( authorizeRequest )
require . NoError ( t , err )
require . NoError ( t , authorizeResp . Body . Close ( ) )
expectSecurityHeaders ( t , authorizeResp , false )
2021-04-06 17:10:01 +00:00
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest . Open ( t )
2021-06-22 15:23:19 +00:00
t . Logf ( "opening browser to downstream authorize URL %s" , testlib . MaskTokens ( downstreamAuthorizeURL ) )
2021-04-06 17:10:01 +00:00
require . NoError ( t , page . Navigate ( downstreamAuthorizeURL ) )
// Expect to be redirected to the upstream provider and log in.
2021-04-07 19:56:09 +00:00
browsertest . LoginToUpstream ( t , page , env . SupervisorUpstreamOIDC )
2021-04-06 17:10:01 +00:00
// 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 ( downstreamCallbackURL ) + ` \?.+\z ` )
browsertest . WaitForURL ( t , page , callbackURLPattern )
}
2021-04-07 19:56:09 +00:00
func requestAuthorizationUsingLDAPIdentityProvider ( t * testing . T , downstreamAuthorizeURL , upstreamUsername , upstreamPassword string , httpClient * http . Client ) {
2021-04-06 17:10:01 +00:00
t . Helper ( )
2021-04-07 19:56:09 +00:00
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Minute )
defer cancelFunc ( )
authRequest , err := http . NewRequestWithContext ( ctx , http . MethodGet , downstreamAuthorizeURL , nil )
require . NoError ( t , err )
// Set the custom username/password headers for the LDAP authorize request.
2021-05-12 20:06:08 +00:00
authRequest . Header . Set ( "Pinniped-Username" , upstreamUsername )
authRequest . Header . Set ( "Pinniped-Password" , upstreamPassword )
2021-04-07 19:56:09 +00:00
2021-06-02 18:36:48 +00:00
// At this point in the test, we've already waited for the LDAPIdentityProvider to be loaded and marked healthy by
// at least one Supervisor pod, but we can't be sure that _all_ of them have loaded the provider, so we may need
// to retry this request multiple times until we get the expected 302 status response.
var authResponse * http . Response
var responseBody [ ] byte
2021-06-22 15:23:19 +00:00
testlib . RequireEventuallyWithoutError ( t , func ( ) ( bool , error ) {
2021-06-02 18:36:48 +00:00
authResponse , err = httpClient . Do ( authRequest )
if err != nil {
t . Logf ( "got authorization response with error %v" , err )
return false , nil
}
defer func ( ) { _ = authResponse . Body . Close ( ) } ( )
responseBody , err = ioutil . ReadAll ( authResponse . Body )
if err != nil {
return false , nil
}
t . Logf ( "got authorization response with code %d (%d byte body)" , authResponse . StatusCode , len ( responseBody ) )
if authResponse . StatusCode != http . StatusFound {
return false , nil
}
return true , nil
} , 30 * time . Second , 200 * time . Millisecond )
2021-04-14 01:11:16 +00:00
expectSecurityHeaders ( t , authResponse , true )
// A successful authorize request results in a redirect to our localhost callback listener with an authcode param.
require . Equalf ( t , http . StatusFound , authResponse . StatusCode , "response body was: %s" , string ( responseBody ) )
redirectLocation := authResponse . Header . Get ( "Location" )
require . Contains ( t , redirectLocation , "127.0.0.1" )
require . Contains ( t , redirectLocation , "/callback" )
require . Contains ( t , redirectLocation , "code=" )
2021-04-07 19:56:09 +00:00
2021-04-14 01:11:16 +00:00
// Follow the redirect.
callbackRequest , err := http . NewRequestWithContext ( ctx , http . MethodGet , redirectLocation , nil )
require . NoError ( t , err )
2021-04-07 19:56:09 +00:00
2021-04-14 01:11:16 +00:00
// Our localhost callback listener should have returned 200 OK.
callbackResponse , err := httpClient . Do ( callbackRequest )
require . NoError ( t , err )
defer callbackResponse . Body . Close ( )
require . Equal ( t , http . StatusOK , callbackResponse . StatusCode )
2021-04-06 17:10:01 +00:00
}
2020-12-02 21:50:42 +00:00
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
} ) )
2020-12-02 22:11:22 +00:00
server . URL += "/callback"
2020-12-02 21:50:42 +00:00
t . Cleanup ( server . Close )
t . Cleanup ( func ( ) { close ( callbacks ) } )
return & localCallbackServer { Server : server , t : t , callbacks : callbacks }
}
2020-11-17 15:21:17 +00:00
2020-12-02 21:50:42 +00:00
type localCallbackServer struct {
* httptest . Server
t * testing . T
callbacks <- chan * http . Request
}
2020-11-17 15:21:17 +00:00
2020-12-02 21:50:42 +00:00
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
}
2020-11-17 15:21:17 +00:00
}
2020-12-09 16:23:10 +00:00
2020-12-10 18:14:54 +00:00
func doTokenExchange ( t * testing . T , config * oauth2 . Config , tokenResponse * oauth2 . Token , httpClient * http . Client , provider * coreosoidc . Provider ) {
2021-03-05 01:25:43 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Minute )
2020-12-09 16:23:10 +00:00
defer cancel ( )
// Form the HTTP POST request with the parameters specified by RFC8693.
reqBody := strings . NewReader ( url . Values {
"grant_type" : [ ] string { "urn:ietf:params:oauth:grant-type:token-exchange" } ,
"audience" : [ ] string { "cluster-1234" } ,
"client_id" : [ ] string { config . ClientID } ,
"subject_token" : [ ] string { tokenResponse . AccessToken } ,
"subject_token_type" : [ ] string { "urn:ietf:params:oauth:token-type:access_token" } ,
"requested_token_type" : [ ] string { "urn:ietf:params:oauth:token-type:jwt" } ,
} . Encode ( ) )
req , err := http . NewRequestWithContext ( ctx , http . MethodPost , config . Endpoint . TokenURL , reqBody )
require . NoError ( t , err )
req . Header . Set ( "content-type" , "application/x-www-form-urlencoded" )
resp , err := httpClient . Do ( req )
require . NoError ( t , err )
2020-12-17 01:59:39 +00:00
require . Equal ( t , resp . StatusCode , http . StatusOK )
2020-12-09 16:23:10 +00:00
defer func ( ) { _ = resp . Body . Close ( ) } ( )
var respBody struct {
AccessToken string ` json:"access_token" `
IssuedTokenType string ` json:"issued_token_type" `
TokenType string ` json:"token_type" `
}
require . NoError ( t , json . NewDecoder ( resp . Body ) . Decode ( & respBody ) )
2020-12-10 18:14:54 +00:00
var clusterVerifier = provider . Verifier ( & coreosoidc . Config { ClientID : "cluster-1234" } )
2020-12-09 16:23:10 +00:00
exchangedToken , err := clusterVerifier . Verify ( ctx , respBody . AccessToken )
require . NoError ( t , err )
var claims map [ string ] interface { }
require . NoError ( t , exchangedToken . Claims ( & claims ) )
indentedClaims , err := json . MarshalIndent ( claims , " " , " " )
require . NoError ( t , err )
t . Logf ( "exchanged token claims:\n%s" , string ( indentedClaims ) )
}
2020-12-16 03:42:11 +00:00
2021-04-14 01:11:16 +00:00
func expectSecurityHeaders ( t * testing . T , response * http . Response , expectFositeToOverrideSome bool ) {
2020-12-16 03:42:11 +00:00
h := response . Header
assert . Equal ( t , "default-src 'none'; frame-ancestors 'none'" , h . Get ( "Content-Security-Policy" ) )
assert . Equal ( t , "DENY" , h . Get ( "X-Frame-Options" ) )
assert . Equal ( t , "1; mode=block" , h . Get ( "X-XSS-Protection" ) )
assert . Equal ( t , "nosniff" , h . Get ( "X-Content-Type-Options" ) )
assert . Equal ( t , "no-referrer" , h . Get ( "Referrer-Policy" ) )
assert . Equal ( t , "off" , h . Get ( "X-DNS-Prefetch-Control" ) )
2021-04-14 01:11:16 +00:00
if expectFositeToOverrideSome {
assert . Equal ( t , "no-store" , h . Get ( "Cache-Control" ) )
} else {
assert . Equal ( t , "no-cache,no-store,max-age=0,must-revalidate" , h . Get ( "Cache-Control" ) )
}
2020-12-16 03:42:11 +00:00
assert . Equal ( t , "no-cache" , h . Get ( "Pragma" ) )
assert . Equal ( t , "0" , h . Get ( "Expires" ) )
}