2022-01-07 23:04:58 +00:00
// Copyright 2020-2022 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"
2022-05-10 23:22:07 +00:00
"errors"
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"
2021-09-03 00:17:15 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/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"
2021-10-13 22:12:19 +00:00
"go.pinniped.dev/internal/psession"
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
)
2022-02-16 14:20:28 +00:00
func TestSupervisorLogin_Browser ( t * testing . T ) {
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
2021-04-07 19:56:09 +00:00
2022-05-10 23:22:07 +00:00
skipNever := func ( t * testing . T ) {
// never need to skip this test
}
skipLDAPTests := func ( t * testing . T ) {
t . Helper ( )
if len ( env . ToolsNamespace ) == 0 && ! env . HasCapability ( testlib . CanReachInternetLDAPPorts ) {
t . Skip ( "LDAP integration test requires connectivity to an LDAP server" )
}
}
skipActiveDirectoryTests := func ( t * testing . T ) {
t . Helper ( )
skipLDAPTests ( t )
if env . SupervisorUpstreamActiveDirectory . Host == "" {
t . Skip ( "Active Directory hostname not specified" )
}
}
basicOIDCIdentityProviderSpec := func ( ) idpv1alpha1 . OIDCIdentityProviderSpec {
return idpv1alpha1 . OIDCIdentityProviderSpec {
Issuer : env . SupervisorUpstreamOIDC . Issuer ,
TLS : & idpv1alpha1 . TLSSpec {
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamOIDC . CABundle ) ) ,
} ,
Client : idpv1alpha1 . OIDCClient {
SecretName : testlib . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
} ,
}
}
createActiveDirectoryIdentityProvider := func ( t * testing . T , edit func ( spec * idpv1alpha1 . ActiveDirectoryIdentityProviderSpec ) ) ( * idpv1alpha1 . ActiveDirectoryIdentityProvider , * v1 . Secret ) {
t . Helper ( )
secret := testlib . CreateTestSecret ( t , env . SupervisorNamespace , "ad-service-account" , v1 . SecretTypeBasicAuth ,
map [ string ] string {
v1 . BasicAuthUsernameKey : env . SupervisorUpstreamActiveDirectory . BindUsername ,
v1 . BasicAuthPasswordKey : env . SupervisorUpstreamActiveDirectory . BindPassword ,
} ,
)
spec := idpv1alpha1 . ActiveDirectoryIdentityProviderSpec {
Host : env . SupervisorUpstreamActiveDirectory . Host ,
TLS : & idpv1alpha1 . TLSSpec {
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamActiveDirectory . CABundle ) ) ,
} ,
Bind : idpv1alpha1 . ActiveDirectoryIdentityProviderBind {
SecretName : secret . Name ,
} ,
}
if edit != nil {
edit ( & spec )
}
adIDP := testlib . CreateTestActiveDirectoryIdentityProvider ( t , spec , idpv1alpha1 . ActiveDirectoryPhaseReady )
expectedMsg := fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
spec . Host , env . SupervisorUpstreamActiveDirectory . BindUsername ,
secret . Name , secret . ResourceVersion ,
)
requireSuccessfulActiveDirectoryIdentityProviderConditions ( t , adIDP , expectedMsg )
return adIDP , secret
}
createLDAPIdentityProvider := func ( t * testing . T , edit func ( spec * idpv1alpha1 . LDAPIdentityProviderSpec ) ) ( * idpv1alpha1 . LDAPIdentityProvider , * v1 . Secret ) {
t . Helper ( )
secret := testlib . CreateTestSecret ( t , env . SupervisorNamespace , "ldap-service-account" , v1 . SecretTypeBasicAuth ,
map [ string ] string {
v1 . BasicAuthUsernameKey : env . SupervisorUpstreamLDAP . BindUsername ,
v1 . BasicAuthPasswordKey : env . SupervisorUpstreamLDAP . BindPassword ,
} ,
)
spec := idpv1alpha1 . LDAPIdentityProviderSpec {
Host : env . SupervisorUpstreamLDAP . Host ,
TLS : & idpv1alpha1 . TLSSpec {
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . CABundle ) ) ,
} ,
Bind : idpv1alpha1 . LDAPIdentityProviderBind {
SecretName : secret . Name ,
} ,
UserSearch : idpv1alpha1 . LDAPIdentityProviderUserSearch {
Base : env . SupervisorUpstreamLDAP . UserSearchBase ,
Filter : "" ,
Attributes : idpv1alpha1 . LDAPIdentityProviderUserSearchAttributes {
Username : env . SupervisorUpstreamLDAP . TestUserMailAttributeName ,
UID : env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeName ,
} ,
} ,
GroupSearch : idpv1alpha1 . LDAPIdentityProviderGroupSearch {
Base : env . SupervisorUpstreamLDAP . GroupSearchBase ,
Filter : "" ,
Attributes : idpv1alpha1 . LDAPIdentityProviderGroupSearchAttributes {
GroupName : "dn" ,
} ,
} ,
}
if edit != nil {
edit ( & spec )
}
ldapIDP := testlib . CreateTestLDAPIdentityProvider ( t , spec , idpv1alpha1 . LDAPPhaseReady )
expectedMsg := fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
spec . Host , env . SupervisorUpstreamLDAP . BindUsername ,
secret . Name , secret . ResourceVersion ,
)
requireSuccessfulLDAPIdentityProviderConditions ( t , ldapIDP , expectedMsg )
return ldapIDP , secret
}
2021-04-06 17:10:01 +00:00
tests := [ ] struct {
2022-02-01 17:03:52 +00:00
name string
maybeSkip func ( t * testing . T )
createTestUser func ( t * testing . T ) ( string , string )
deleteTestUser func ( t * testing . T , username string )
2022-05-10 19:54:40 +00:00
requestAuthorization func ( t * testing . T , downstreamIssuer , downstreamAuthorizeURL , downstreamCallbackURL , username , password string , httpClient * http . Client )
2022-02-01 17:03:52 +00:00
createIDP func ( t * testing . T ) string
2022-06-07 23:32:19 +00:00
requestTokenExchangeAud string
2022-05-10 23:22:07 +00:00
wantLocalhostCallbackToNeverHappen bool
2022-02-01 17:03:52 +00:00
wantDownstreamIDTokenSubjectToMatch string
wantDownstreamIDTokenUsernameToMatch func ( username string ) string
wantDownstreamIDTokenGroups [ ] string
wantErrorDescription string
wantErrorType string
2022-06-07 23:32:19 +00:00
wantTokenExchangeResponse func ( t * testing . T , status int , body string )
2021-10-13 22:12:19 +00:00
2021-10-28 19:00:56 +00:00
// Either revoke the user's session on the upstream provider, or manipulate the user's session
2021-10-13 22:12:19 +00:00
// data in such a way that it should cause the next upstream refresh attempt to fail.
2021-10-28 19:00:56 +00:00
breakRefreshSessionData func ( t * testing . T , sessionData * psession . PinnipedSession , idpName , username string )
2022-01-26 00:19:56 +00:00
// Edit the refresh session data between the initial login and the refresh, which is expected to
// succeed.
2022-02-01 17:03:52 +00:00
editRefreshSessionDataWithoutBreaking func ( t * testing . T , sessionData * psession . PinnipedSession , idpName , username string ) [ ] string
2021-04-06 17:10:01 +00:00
} {
{
2022-05-10 23:22:07 +00:00
name : "oidc with default username and groups claim settings" ,
maybeSkip : skipNever ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
return testlib . CreateTestOIDCIdentityProvider ( t , basicOIDCIdentityProviderSpec ( ) , idpv1alpha1 . PhaseReady ) . Name
2021-04-06 17:10:01 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowOIDC ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2022-01-07 23:04:58 +00:00
pinnipedSessionData := pinnipedSession . Custom
pinnipedSessionData . OIDC . UpstreamIssuer = "wrong-issuer"
2021-10-13 22:12:19 +00:00
} ,
2021-04-07 19:56:09 +00:00
// the ID token Subject should include the upstream user ID after the upstream issuer name
2021-08-26 23:18:05 +00:00
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" ,
2021-04-07 19:56:09 +00:00
// the ID token Username should include the upstream user ID after the upstream issuer name
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string { return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" } ,
2021-05-28 17:37:46 +00:00
} ,
{
2022-05-10 23:22:07 +00:00
name : "oidc with custom username and groups claim settings" ,
maybeSkip : skipNever ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
spec := basicOIDCIdentityProviderSpec ( )
spec . Claims = idpv1alpha1 . OIDCClaims {
Username : env . SupervisorUpstreamOIDC . UsernameClaim ,
Groups : env . SupervisorUpstreamOIDC . GroupsClaim ,
}
spec . AuthorizationConfig = idpv1alpha1 . OIDCAuthorizationConfig {
AdditionalScopes : env . SupervisorUpstreamOIDC . AdditionalScopes ,
}
return testlib . CreateTestOIDCIdentityProvider ( t , spec , idpv1alpha1 . PhaseReady ) . Name
2021-05-28 17:37:46 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowOIDC ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-12-14 19:59:52 +00:00
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Extra [ "username" ] = "some-incorrect-username"
2021-10-13 22:12:19 +00:00
} ,
2021-08-26 23:18:05 +00:00
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" ,
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string { return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Username ) + "$" } ,
2021-05-17 18:10:26 +00:00
wantDownstreamIDTokenGroups : env . SupervisorUpstreamOIDC . ExpectedGroups ,
2022-02-01 17:03:52 +00:00
editRefreshSessionDataWithoutBreaking : func ( t * testing . T , sessionData * psession . PinnipedSession , _ , _ string ) [ ] string {
2022-01-26 00:19:56 +00:00
// even if we update this group to the wrong thing, we expect that it will return to the correct
// value after we refresh.
// However if there are no expected groups then they will not update, so we should skip this.
if len ( env . SupervisorUpstreamOIDC . ExpectedGroups ) > 0 {
sessionData . Fosite . Claims . Extra [ "groups" ] = [ ] string { "some-wrong-group" , "some-other-group" }
}
2022-02-01 17:03:52 +00:00
return env . SupervisorUpstreamOIDC . ExpectedGroups
2022-01-26 00:19:56 +00:00
} ,
2021-04-06 17:10:01 +00:00
} ,
2022-01-05 18:31:38 +00:00
{
2022-05-10 23:22:07 +00:00
name : "oidc without refresh token" ,
maybeSkip : skipNever ,
2022-01-05 18:31:38 +00:00
createIDP : func ( t * testing . T ) string {
2022-01-19 21:29:26 +00:00
var additionalScopes [ ] string
// keep all the scopes except for offline access so we can test the access token based refresh flow.
if len ( env . ToolsNamespace ) == 0 {
additionalScopes = env . SupervisorUpstreamOIDC . AdditionalScopes
} else {
for _ , additionalScope := range env . SupervisorUpstreamOIDC . AdditionalScopes {
if additionalScope != "offline_access" {
additionalScopes = append ( additionalScopes , additionalScope )
}
}
}
2022-05-10 23:22:07 +00:00
spec := basicOIDCIdentityProviderSpec ( )
spec . Claims = idpv1alpha1 . OIDCClaims {
Username : env . SupervisorUpstreamOIDC . UsernameClaim ,
Groups : env . SupervisorUpstreamOIDC . GroupsClaim ,
}
spec . AuthorizationConfig = idpv1alpha1 . OIDCAuthorizationConfig {
AdditionalScopes : additionalScopes ,
}
return testlib . CreateTestOIDCIdentityProvider ( t , spec , idpv1alpha1 . PhaseReady ) . Name
2022-01-05 18:31:38 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowOIDC ,
2022-01-05 18:31:38 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Extra [ "username" ] = "some-incorrect-username"
} ,
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" ,
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string { return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Username ) + "$" } ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamOIDC . ExpectedGroups ,
} ,
2021-08-12 17:00:18 +00:00
{
2022-05-10 23:22:07 +00:00
name : "oidc with CLI password flow" ,
maybeSkip : skipNever ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
spec := basicOIDCIdentityProviderSpec ( )
spec . AuthorizationConfig = idpv1alpha1 . OIDCAuthorizationConfig {
AllowPasswordGrant : true , // allow the CLI password flow for this OIDCIdentityProvider
}
return testlib . CreateTestOIDCIdentityProvider ( t , spec , idpv1alpha1 . PhaseReady ) . Name
2021-08-12 17:00:18 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-08-12 17:00:18 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamOIDC . Username , // username to present to server during login
env . SupervisorUpstreamOIDC . Password , // password to present to server during login
httpClient ,
2021-08-24 19:19:29 +00:00
false ,
2021-08-12 17:00:18 +00:00
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-13 22:12:19 +00:00
require . Equal ( t , psession . ProviderTypeOIDC , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . OIDC . UpstreamRefreshToken )
customSessionData . OIDC . UpstreamRefreshToken = "invalid-updated-refresh-token"
} ,
2021-08-12 17:00:18 +00:00
// the ID token Subject should include the upstream user ID after the upstream issuer name
2021-08-26 23:18:05 +00:00
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" ,
2021-08-12 17:00:18 +00:00
// the ID token Username should include the upstream user ID after the upstream issuer name
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string { return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" } ,
2021-08-12 17:00:18 +00:00
} ,
2021-04-06 17:10:01 +00:00
{
2022-05-10 23:22:07 +00:00
name : "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS" ,
maybeSkip : skipLDAPTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
2021-04-07 19:56:09 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-08-12 17:00:18 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
2021-04-07 19:56:09 +00:00
downstreamAuthorizeURL ,
2021-07-15 23:58:26 +00:00
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
2021-04-07 19:56:09 +00:00
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
2021-07-22 23:15:44 +00:00
false ,
2021-04-07 19:56:09 +00:00
)
2021-04-06 17:10:01 +00:00
} ,
2022-02-01 17:03:52 +00:00
editRefreshSessionDataWithoutBreaking : func ( t * testing . T , sessionData * psession . PinnipedSession , _ , _ string ) [ ] string {
2022-01-26 00:19:56 +00:00
// even if we update this group to the wrong thing, we expect that it will return to the correct
// value after we refresh.
sessionData . Fosite . Claims . Extra [ "groups" ] = [ ] string { "some-wrong-group" , "some-other-group" }
2022-02-01 17:03:52 +00:00
return env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs
} ,
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
customSessionData := pinnipedSession . Custom
require . Equal ( t , psession . ProviderTypeLDAP , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . LDAP . UserDN )
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Subject = "not-right"
} ,
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
} ,
2022-05-10 19:54:40 +00:00
{
2022-05-10 23:22:07 +00:00
name : "ldap with browser flow" ,
maybeSkip : skipLDAPTests ,
2022-05-10 19:54:40 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
2022-05-10 19:54:40 +00:00
} ,
createTestUser : func ( t * testing . T ) ( string , string ) {
// return the username and password of the existing user that we want to use for this test
return env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword // password to present to server during login
} ,
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowLDAP ,
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
} ,
2022-02-01 17:03:52 +00:00
{
2022-05-10 23:22:07 +00:00
name : "ldap with browser flow with wrong password" ,
maybeSkip : skipLDAPTests ,
createIDP : func ( t * testing . T ) string {
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
} ,
createTestUser : func ( t * testing . T ) ( string , string ) {
// return the username and password of the existing user that we want to use for this test
return env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
"this is the wrong password" // password to present to server during login
2022-02-01 17:03:52 +00:00
} ,
2022-05-10 23:22:07 +00:00
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentials ,
wantLocalhostCallbackToNeverHappen : true , // we should have been sent back to the login page to retry login
} ,
{
name : "ldap with browser flow with wrong username" ,
maybeSkip : skipLDAPTests ,
2022-02-01 17:03:52 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
} ,
createTestUser : func ( t * testing . T ) ( string , string ) {
// return the username and password of the existing user that we want to use for this test
return "this is the wrong username" , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword // password to present to server during login
} ,
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentials ,
wantLocalhostCallbackToNeverHappen : true , // we should have been sent back to the login page to retry login
} ,
{
name : "ldap with browser flow with wrong password and then correct password" ,
maybeSkip : skipLDAPTests ,
createIDP : func ( t * testing . T ) string {
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
} ,
createTestUser : func ( t * testing . T ) ( string , string ) {
// return the username and password of the existing user that we want to use for this test
return env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword // password to present to server during login
} ,
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials ,
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
} ,
{
name : "ldap skip group refresh" ,
maybeSkip : skipLDAPTests ,
createIDP : func ( t * testing . T ) string {
idp , _ := createLDAPIdentityProvider ( t , func ( spec * idpv1alpha1 . LDAPIdentityProviderSpec ) {
spec . GroupSearch . SkipGroupRefresh = true
} )
return idp . Name
2022-02-01 17:03:52 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2022-02-01 17:03:52 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
editRefreshSessionDataWithoutBreaking : func ( t * testing . T , sessionData * psession . PinnipedSession , _ , _ string ) [ ] string {
// update the list of groups to the wrong thing and see that they do not get updated because
// skip group refresh is set
wrongGroups := [ ] string { "some-wrong-group" , "some-other-group" }
sessionData . Fosite . Claims . Extra [ "groups" ] = wrongGroups
return wrongGroups
2022-01-26 00:19:56 +00:00
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeLDAP , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . LDAP . UserDN )
2021-10-26 23:24:02 +00:00
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Subject = "not-right"
2021-10-22 20:57:30 +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
2021-08-26 23:18:05 +00:00
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"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
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
2021-04-06 17:10:01 +00:00
} ,
2022-02-11 18:23:44 +00:00
{
name : "ldap with email as username and group search base that doesn't return anything, and using an LDAP provider which supports TLS" ,
maybeSkip : func ( t * testing . T ) {
2022-05-10 23:22:07 +00:00
skipLDAPTests ( t )
2022-02-11 18:23:44 +00:00
if env . SupervisorUpstreamLDAP . UserSearchBase == env . SupervisorUpstreamLDAP . GroupSearchBase {
// This test relies on using the user search base as the group search base, to simulate
// searching for groups and not finding any.
// If the users and groups are stored in the same place, then we will get groups
// back, so this test wouldn't make sense.
t . Skip ( "must have a different user search base than group search base" )
}
} ,
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , func ( spec * idpv1alpha1 . LDAPIdentityProviderSpec ) {
spec . GroupSearch . Base = env . SupervisorUpstreamLDAP . UserSearchBase // groups not stored at the user search base
} )
return idp . Name
2022-02-11 18:23:44 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2022-02-11 18:23:44 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2022-02-01 17:03:52 +00:00
editRefreshSessionDataWithoutBreaking : func ( t * testing . T , sessionData * psession . PinnipedSession , _ , _ string ) [ ] string {
2022-02-11 18:23:44 +00:00
// even if we update this group to the wrong thing, we expect that it will return to the correct
2022-02-01 17:03:52 +00:00
// value (no groups) after we refresh.
2022-02-11 18:23:44 +00:00
sessionData . Fosite . Claims . Extra [ "groups" ] = [ ] string { "some-wrong-group" , "some-other-group" }
2022-02-01 17:03:52 +00:00
return [ ] string { }
2022-02-11 18:23:44 +00:00
} ,
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
customSessionData := pinnipedSession . Custom
require . Equal ( t , psession . ProviderTypeLDAP , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . LDAP . UserDN )
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Subject = "not-right"
} ,
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : [ ] string { } ,
} ,
2021-04-16 21:04:05 +00:00
{
2022-05-10 23:22:07 +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
maybeSkip : skipLDAPTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , func ( spec * idpv1alpha1 . LDAPIdentityProviderSpec ) {
spec . Host = env . SupervisorUpstreamLDAP . StartTLSOnlyHost
spec . UserSearch . Filter = "cn={}" // try using a non-default search filter
spec . UserSearch . Attributes . Username = "dn" // try using the user's DN as the downstream username
spec . GroupSearch . Attributes . GroupName = "cn"
} )
return idp . Name
2021-04-16 21:04:05 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-08-12 17:00:18 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
2021-04-16 21:04:05 +00:00
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserCN , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
2021-07-22 23:15:44 +00:00
false ,
2021-04-16 21:04:05 +00:00
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeLDAP , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . LDAP . UserDN )
2021-10-26 23:24:02 +00:00
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Extra [ "username" ] = "not-the-same"
2021-10-22 20:57:30 +00:00
} ,
2021-04-16 21:04:05 +00:00
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
2021-08-26 23:18:05 +00:00
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"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
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string { return "^" + 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-07-26 23:32:46 +00:00
{
2022-05-10 23:22:07 +00:00
name : "logging in to ldap with the wrong password fails" ,
maybeSkip : skipLDAPTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
2021-07-26 23:32:46 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-08-24 19:19:29 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
2021-07-26 23:32:46 +00:00
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
"incorrect" , // password to present to server during login
httpClient ,
true ,
)
} ,
2021-10-22 20:57:30 +00:00
wantErrorDescription : "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider." ,
wantErrorType : "access_denied" ,
2021-07-26 23:32:46 +00:00
} ,
2021-09-07 18:45:32 +00:00
{
2022-05-10 23:22:07 +00:00
name : "ldap login still works after updating bind secret" ,
maybeSkip : skipLDAPTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2021-09-07 18:45:32 +00:00
t . Helper ( )
2022-05-10 23:22:07 +00:00
idp , secret := createLDAPIdentityProvider ( t , nil )
2021-09-07 18:45:32 +00:00
secret . Annotations = map [ string ] string { "pinniped.dev/test" : "" , "another-label" : "another-key" }
// update that secret, which will cause the cache to recheck tls and search base values
client := testlib . NewKubernetesClientset ( t )
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer cancel ( )
updatedSecret , err := client . CoreV1 ( ) . Secrets ( env . SupervisorNamespace ) . Update ( ctx , secret , metav1 . UpdateOptions { } )
require . NoError ( t , err )
expectedMsg := fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
env . SupervisorUpstreamLDAP . Host , env . SupervisorUpstreamLDAP . BindUsername ,
updatedSecret . Name , updatedSecret . ResourceVersion ,
)
supervisorClient := testlib . NewSupervisorClientset ( t )
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
2022-05-10 23:22:07 +00:00
idp , err = supervisorClient . IDPV1alpha1 ( ) . LDAPIdentityProviders ( env . SupervisorNamespace ) . Get ( ctx , idp . Name , metav1 . GetOptions { } )
2021-09-07 18:45:32 +00:00
requireEventually . NoError ( err )
2022-05-10 23:22:07 +00:00
requireEventuallySuccessfulLDAPIdentityProviderConditions ( t , requireEventually , idp , expectedMsg )
2021-09-07 18:45:32 +00:00
} , time . Minute , 500 * time . Millisecond )
2022-05-10 23:22:07 +00:00
return idp . Name
2021-09-07 18:45:32 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-09-07 18:45:32 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeLDAP , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . LDAP . UserDN )
customSessionData . LDAP . UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
} ,
2021-09-07 18:45:32 +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 (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
2021-09-07 18:45:32 +00:00
} ,
{
2022-05-10 23:22:07 +00:00
name : "ldap login still works after deleting and recreating the bind secret" ,
maybeSkip : skipLDAPTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2021-09-07 18:45:32 +00:00
t . Helper ( )
2022-05-10 23:22:07 +00:00
idp , secret := createLDAPIdentityProvider ( t , nil )
2021-09-07 18:45:32 +00:00
// delete, then recreate that secret, which will cause the cache to recheck tls and search base values
client := testlib . NewKubernetesClientset ( t )
deleteCtx , deleteCancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer deleteCancel ( )
2022-05-10 23:22:07 +00:00
err := client . CoreV1 ( ) . Secrets ( env . SupervisorNamespace ) . Delete ( deleteCtx , secret . Name , metav1 . DeleteOptions { } )
2021-09-07 18:45:32 +00:00
require . NoError ( t , err )
// create the secret again
recreateCtx , recreateCancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer recreateCancel ( )
recreatedSecret , err := client . CoreV1 ( ) . Secrets ( env . SupervisorNamespace ) . Create ( recreateCtx , & v1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
2022-05-10 23:22:07 +00:00
Name : secret . Name ,
2021-09-07 18:45:32 +00:00
Namespace : env . SupervisorNamespace ,
} ,
Type : v1 . SecretTypeBasicAuth ,
StringData : map [ string ] string {
v1 . BasicAuthUsernameKey : env . SupervisorUpstreamLDAP . BindUsername ,
v1 . BasicAuthPasswordKey : env . SupervisorUpstreamLDAP . BindPassword ,
} ,
} , metav1 . CreateOptions { } )
require . NoError ( t , err )
expectedMsg := fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
env . SupervisorUpstreamLDAP . Host , env . SupervisorUpstreamLDAP . BindUsername ,
recreatedSecret . Name , recreatedSecret . ResourceVersion ,
)
supervisorClient := testlib . NewSupervisorClientset ( t )
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
2022-05-10 23:22:07 +00:00
idp , err = supervisorClient . IDPV1alpha1 ( ) . LDAPIdentityProviders ( env . SupervisorNamespace ) . Get ( ctx , idp . Name , metav1 . GetOptions { } )
2021-09-07 18:45:32 +00:00
requireEventually . NoError ( err )
2022-05-10 23:22:07 +00:00
requireEventuallySuccessfulLDAPIdentityProviderConditions ( t , requireEventually , idp , expectedMsg )
2021-09-07 18:45:32 +00:00
} , time . Minute , 500 * time . Millisecond )
2022-05-10 23:22:07 +00:00
return idp . Name
2021-09-07 18:45:32 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-09-07 18:45:32 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeLDAP , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . LDAP . UserDN )
customSessionData . LDAP . UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
} ,
2021-09-07 18:45:32 +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 (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
2021-09-07 18:45:32 +00:00
} ,
2021-07-06 18:34:54 +00:00
{
2022-05-10 23:22:07 +00:00
name : "active directory with all default options" ,
maybeSkip : skipActiveDirectoryTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createActiveDirectoryIdentityProvider ( t , nil )
return idp . Name
2021-07-07 16:23:32 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-08-24 19:19:29 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
2021-07-07 16:23:32 +00:00
downstreamAuthorizeURL ,
2021-08-18 20:18:53 +00:00
env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue , // username to present to server during login
env . SupervisorUpstreamActiveDirectory . TestUserPassword , // password to present to server during login
2021-07-07 16:23:32 +00:00
httpClient ,
2021-07-22 23:15:44 +00:00
false ,
2021-07-07 16:23:32 +00:00
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeActiveDirectory , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . ActiveDirectory . UserDN )
2021-10-26 23:24:02 +00:00
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Extra [ "username" ] = "not-the-same"
2021-10-22 20:57:30 +00:00
} ,
2021-07-07 16:23:32 +00:00
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
2021-08-26 23:18:05 +00:00
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"ldaps://" + env . SupervisorUpstreamActiveDirectory . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamActiveDirectory . DefaultNamingContextSearchBase ) +
"&sub=" + env . SupervisorUpstreamActiveDirectory . TestUserUniqueIDAttributeValue ,
) + "$" ,
2021-07-07 16:23:32 +00:00
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamActiveDirectory . TestUserIndirectGroupsSAMAccountPlusDomainNames ,
2022-05-10 19:54:40 +00:00
} ,
{
2022-05-10 23:22:07 +00:00
name : "active directory with custom options" ,
maybeSkip : skipActiveDirectoryTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createActiveDirectoryIdentityProvider ( t , func ( spec * idpv1alpha1 . ActiveDirectoryIdentityProviderSpec ) {
spec . UserSearch = idpv1alpha1 . ActiveDirectoryIdentityProviderUserSearch {
2021-07-26 23:03:12 +00:00
Base : env . SupervisorUpstreamActiveDirectory . UserSearchBase ,
Filter : env . SupervisorUpstreamActiveDirectory . TestUserMailAttributeName + "={}" ,
Attributes : idpv1alpha1 . ActiveDirectoryIdentityProviderUserSearchAttributes {
Username : env . SupervisorUpstreamActiveDirectory . TestUserMailAttributeName ,
} ,
2022-05-10 23:22:07 +00:00
}
spec . GroupSearch = idpv1alpha1 . ActiveDirectoryIdentityProviderGroupSearch {
2021-07-26 23:03:12 +00:00
Filter : "member={}" , // excluding nested groups
Base : env . SupervisorUpstreamActiveDirectory . GroupSearchBase ,
Attributes : idpv1alpha1 . ActiveDirectoryIdentityProviderGroupSearchAttributes {
GroupName : "dn" ,
} ,
2022-05-10 23:22:07 +00:00
}
} )
return idp . Name
2021-07-26 23:03:12 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-08-24 19:19:29 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
2021-07-26 23:03:12 +00:00
downstreamAuthorizeURL ,
env . SupervisorUpstreamActiveDirectory . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamActiveDirectory . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeActiveDirectory , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . ActiveDirectory . UserDN )
2021-10-26 23:24:02 +00:00
fositeSessionData := pinnipedSession . Fosite
fositeSessionData . Claims . Subject = "not-right"
2021-10-22 20:57:30 +00:00
} ,
2021-07-26 23:03:12 +00:00
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
2021-08-26 23:18:05 +00:00
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"ldaps://" + env . SupervisorUpstreamActiveDirectory . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamActiveDirectory . UserSearchBase ) +
"&sub=" + env . SupervisorUpstreamActiveDirectory . TestUserUniqueIDAttributeValue ,
) + "$" ,
2021-07-26 23:03:12 +00:00
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamActiveDirectory . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamActiveDirectory . TestUserDirectGroupsDNs ,
2021-07-06 18:34:54 +00:00
} ,
2021-09-03 00:17:15 +00:00
{
2022-05-10 23:22:07 +00:00
name : "active directory login still works after updating bind secret" ,
maybeSkip : skipActiveDirectoryTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2021-09-03 00:17:15 +00:00
t . Helper ( )
2022-05-10 23:22:07 +00:00
idp , secret := createActiveDirectoryIdentityProvider ( t , nil )
2021-09-03 00:17:15 +00:00
secret . Annotations = map [ string ] string { "pinniped.dev/test" : "" , "another-label" : "another-key" }
2021-09-07 18:45:32 +00:00
// update that secret, which will cause the cache to recheck tls and search base values
2021-09-03 00:17:15 +00:00
client := testlib . NewKubernetesClientset ( t )
ctx , cancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer cancel ( )
updatedSecret , err := client . CoreV1 ( ) . Secrets ( env . SupervisorNamespace ) . Update ( ctx , secret , metav1 . UpdateOptions { } )
require . NoError ( t , err )
expectedMsg := fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
env . SupervisorUpstreamActiveDirectory . Host , env . SupervisorUpstreamActiveDirectory . BindUsername ,
updatedSecret . Name , updatedSecret . ResourceVersion ,
)
supervisorClient := testlib . NewSupervisorClientset ( t )
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
2022-05-10 23:22:07 +00:00
idp , err = supervisorClient . IDPV1alpha1 ( ) . ActiveDirectoryIdentityProviders ( env . SupervisorNamespace ) . Get ( ctx , idp . Name , metav1 . GetOptions { } )
2021-09-03 00:17:15 +00:00
requireEventually . NoError ( err )
2022-05-10 23:22:07 +00:00
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions ( t , requireEventually , idp , expectedMsg )
2021-09-03 00:17:15 +00:00
} , time . Minute , 500 * time . Millisecond )
2022-05-10 23:22:07 +00:00
return idp . Name
2021-09-03 00:17:15 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-09-03 00:17:15 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue , // username to present to server during login
env . SupervisorUpstreamActiveDirectory . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeActiveDirectory , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . ActiveDirectory . UserDN )
customSessionData . ActiveDirectory . UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
} ,
2021-09-03 00:17:15 +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 (
"ldaps://" + env . SupervisorUpstreamActiveDirectory . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamActiveDirectory . DefaultNamingContextSearchBase ) +
"&sub=" + env . SupervisorUpstreamActiveDirectory . TestUserUniqueIDAttributeValue ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamActiveDirectory . TestUserIndirectGroupsSAMAccountPlusDomainNames ,
2021-09-03 00:17:15 +00:00
} ,
2021-09-07 18:45:32 +00:00
{
2022-05-10 23:22:07 +00:00
name : "active directory login still works after deleting and recreating bind secret" ,
maybeSkip : skipActiveDirectoryTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2021-09-07 18:45:32 +00:00
t . Helper ( )
2022-05-10 23:22:07 +00:00
idp , secret := createActiveDirectoryIdentityProvider ( t , nil )
2021-09-07 18:45:32 +00:00
// delete the secret
client := testlib . NewKubernetesClientset ( t )
deleteCtx , deleteCancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer deleteCancel ( )
2022-05-10 23:22:07 +00:00
err := client . CoreV1 ( ) . Secrets ( env . SupervisorNamespace ) . Delete ( deleteCtx , secret . Name , metav1 . DeleteOptions { } )
2021-09-07 18:45:32 +00:00
require . NoError ( t , err )
// create the secret again
recreateCtx , recreateCancel := context . WithTimeout ( context . Background ( ) , time . Minute )
defer recreateCancel ( )
recreatedSecret , err := client . CoreV1 ( ) . Secrets ( env . SupervisorNamespace ) . Create ( recreateCtx , & v1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
2022-05-10 23:22:07 +00:00
Name : secret . Name ,
2021-09-07 18:45:32 +00:00
Namespace : env . SupervisorNamespace ,
} ,
Type : v1 . SecretTypeBasicAuth ,
StringData : map [ string ] string {
v1 . BasicAuthUsernameKey : env . SupervisorUpstreamActiveDirectory . BindUsername ,
v1 . BasicAuthPasswordKey : env . SupervisorUpstreamActiveDirectory . BindPassword ,
} ,
} , metav1 . CreateOptions { } )
require . NoError ( t , err )
expectedMsg := fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
env . SupervisorUpstreamActiveDirectory . Host , env . SupervisorUpstreamActiveDirectory . BindUsername ,
recreatedSecret . Name , recreatedSecret . ResourceVersion ,
)
supervisorClient := testlib . NewSupervisorClientset ( t )
testlib . RequireEventually ( t , func ( requireEventually * require . Assertions ) {
ctx , cancel := context . WithTimeout ( context . Background ( ) , 10 * time . Second )
defer cancel ( )
2022-05-10 23:22:07 +00:00
idp , err = supervisorClient . IDPV1alpha1 ( ) . ActiveDirectoryIdentityProviders ( env . SupervisorNamespace ) . Get ( ctx , idp . Name , metav1 . GetOptions { } )
2021-09-07 18:45:32 +00:00
requireEventually . NoError ( err )
2022-05-10 23:22:07 +00:00
requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions ( t , requireEventually , idp , expectedMsg )
2021-09-07 18:45:32 +00:00
} , time . Minute , 500 * time . Millisecond )
2022-05-10 23:22:07 +00:00
return idp . Name
2021-09-07 18:45:32 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-09-07 18:45:32 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue , // username to present to server during login
env . SupervisorUpstreamActiveDirectory . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , _ , _ string ) {
2021-10-26 23:24:02 +00:00
customSessionData := pinnipedSession . Custom
2021-10-22 20:57:30 +00:00
require . Equal ( t , psession . ProviderTypeActiveDirectory , customSessionData . ProviderType )
require . NotEmpty ( t , customSessionData . ActiveDirectory . UserDN )
customSessionData . ActiveDirectory . UserDN = "cn=not-a-user,dc=pinniped,dc=dev"
} ,
2021-09-07 18:45:32 +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 (
"ldaps://" + env . SupervisorUpstreamActiveDirectory . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamActiveDirectory . DefaultNamingContextSearchBase ) +
"&sub=" + env . SupervisorUpstreamActiveDirectory . TestUserUniqueIDAttributeValue ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamActiveDirectory . TestUserIndirectGroupsSAMAccountPlusDomainNames ,
} ,
{
2022-05-10 23:22:07 +00:00
name : "active directory login fails after the user password is changed" ,
maybeSkip : skipActiveDirectoryTests ,
2021-10-28 19:00:56 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createActiveDirectoryIdentityProvider ( t , nil )
return idp . Name
2021-10-28 19:00:56 +00:00
} ,
createTestUser : func ( t * testing . T ) ( string , string ) {
2022-03-02 17:50:07 +00:00
return testlib . CreateFreshADTestUser ( t , env )
2021-10-28 19:00:56 +00:00
} ,
deleteTestUser : func ( t * testing . T , username string ) {
2022-03-02 17:50:07 +00:00
testlib . DeleteTestADUser ( t , env , username )
2021-10-28 19:00:56 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , testUserName , testUserPassword string , httpClient * http . Client ) {
2021-10-28 19:00:56 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
testUserName , // username to present to server during login
testUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
breakRefreshSessionData : func ( t * testing . T , sessionData * psession . PinnipedSession , _ , username string ) {
2022-03-02 17:50:07 +00:00
testlib . ChangeADTestUserPassword ( t , env , username )
2021-11-05 18:53:07 +00:00
} ,
// we can't know the subject ahead of time because we created a new user and don't know their uid,
// so skip wantDownstreamIDTokenSubjectToMatch
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( username string ) string {
return "^" + regexp . QuoteMeta ( username + "@" + env . SupervisorUpstreamActiveDirectory . Domain ) + "$"
} ,
wantDownstreamIDTokenGroups : [ ] string { } , // none for now.
} ,
{
2022-05-10 23:22:07 +00:00
name : "active directory login fails after the user is deactivated" ,
maybeSkip : skipActiveDirectoryTests ,
2021-11-05 18:53:07 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createActiveDirectoryIdentityProvider ( t , nil )
return idp . Name
2021-11-05 18:53:07 +00:00
} ,
createTestUser : func ( t * testing . T ) ( string , string ) {
2022-03-02 17:50:07 +00:00
return testlib . CreateFreshADTestUser ( t , env )
2021-11-05 18:53:07 +00:00
} ,
deleteTestUser : func ( t * testing . T , username string ) {
2022-03-02 17:50:07 +00:00
testlib . DeleteTestADUser ( t , env , username )
2021-11-05 18:53:07 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , testUserName , testUserPassword string , httpClient * http . Client ) {
2021-11-05 18:53:07 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
testUserName , // username to present to server during login
testUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
breakRefreshSessionData : func ( t * testing . T , sessionData * psession . PinnipedSession , _ , username string ) {
2022-03-02 17:50:07 +00:00
testlib . DeactivateADTestUser ( t , env , username )
2021-10-28 19:00:56 +00:00
} ,
// we can't know the subject ahead of time because we created a new user and don't know their uid,
// so skip wantDownstreamIDTokenSubjectToMatch
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( username string ) string {
return "^" + regexp . QuoteMeta ( username + "@" + env . SupervisorUpstreamActiveDirectory . Domain ) + "$"
} ,
wantDownstreamIDTokenGroups : [ ] string { } , // none for now.
2021-09-07 18:45:32 +00:00
} ,
2021-11-17 00:31:32 +00:00
{
2022-05-10 23:22:07 +00:00
name : "active directory login fails after the user is locked" ,
maybeSkip : skipActiveDirectoryTests ,
2021-11-17 00:31:32 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createActiveDirectoryIdentityProvider ( t , nil )
return idp . Name
2021-11-17 00:31:32 +00:00
} ,
createTestUser : func ( t * testing . T ) ( string , string ) {
2022-03-02 17:50:07 +00:00
return testlib . CreateFreshADTestUser ( t , env )
2021-11-17 00:31:32 +00:00
} ,
deleteTestUser : func ( t * testing . T , username string ) {
2022-03-02 17:50:07 +00:00
testlib . DeleteTestADUser ( t , env , username )
2021-11-17 00:31:32 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , testUserName , testUserPassword string , httpClient * http . Client ) {
2021-11-17 00:31:32 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
testUserName , // username to present to server during login
testUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
breakRefreshSessionData : func ( t * testing . T , sessionData * psession . PinnipedSession , _ , username string ) {
2022-03-02 17:50:07 +00:00
testlib . LockADTestUser ( t , env , username )
2021-11-17 00:31:32 +00:00
} ,
// we can't know the subject ahead of time because we created a new user and don't know their uid,
// so skip wantDownstreamIDTokenSubjectToMatch
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( username string ) string {
return "^" + regexp . QuoteMeta ( username + "@" + env . SupervisorUpstreamActiveDirectory . Domain ) + "$"
} ,
wantDownstreamIDTokenGroups : [ ] string { } ,
} ,
2021-07-22 23:15:44 +00:00
{
2022-05-10 23:22:07 +00:00
name : "logging in to active directory with a deactivated user fails" ,
maybeSkip : skipActiveDirectoryTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createActiveDirectoryIdentityProvider ( t , nil )
return idp . Name
2021-07-22 23:15:44 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-08-24 19:19:29 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
2021-07-22 23:15:44 +00:00
downstreamAuthorizeURL ,
env . SupervisorUpstreamActiveDirectory . TestDeactivatedUserSAMAccountNameValue , // username to present to server during login
env . SupervisorUpstreamActiveDirectory . TestDeactivatedUserPassword , // password to present to server during login
httpClient ,
true ,
)
} ,
2021-10-22 20:57:30 +00:00
breakRefreshSessionData : nil ,
2021-10-13 22:12:19 +00:00
wantErrorDescription : "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider." ,
wantErrorType : "access_denied" ,
2021-07-22 23:15:44 +00:00
} ,
2021-11-05 21:18:54 +00:00
{
2022-05-10 23:22:07 +00:00
name : "ldap refresh fails when username changes from email as username to dn as username" ,
maybeSkip : skipLDAPTests ,
2021-11-05 21:18:54 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
2021-11-05 21:18:54 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2021-11-05 21:18:54 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2021-10-28 19:00:56 +00:00
breakRefreshSessionData : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , idpName , _ string ) {
2021-11-05 21:18:54 +00:00
// get the idp, update the config.
client := testlib . NewSupervisorClientset ( t )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
defer cancel ( )
upstreams := client . IDPV1alpha1 ( ) . LDAPIdentityProviders ( env . SupervisorNamespace )
ldapIDP , err := upstreams . Get ( ctx , idpName , metav1 . GetOptions { } )
require . NoError ( t , err )
ldapIDP . Spec . UserSearch . Attributes . Username = "dn"
_ , err = upstreams . Update ( ctx , ldapIDP , metav1 . UpdateOptions { } )
require . NoError ( t , err )
time . Sleep ( 10 * time . Second ) // wait for controllers to pick up the change
} ,
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch : "^" + regexp . QuoteMeta (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
2021-11-17 00:31:32 +00:00
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
2021-10-28 19:00:56 +00:00
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
2021-11-05 21:18:54 +00:00
} ,
2022-02-15 21:52:33 +00:00
{
2022-05-10 23:22:07 +00:00
name : "ldap refresh updates groups to be empty after deleting the group search base" ,
maybeSkip : skipLDAPTests ,
2022-02-15 21:52:33 +00:00
createIDP : func ( t * testing . T ) string {
2022-05-10 23:22:07 +00:00
idp , _ := createLDAPIdentityProvider ( t , nil )
return idp . Name
2022-02-15 21:52:33 +00:00
} ,
2022-05-10 19:54:40 +00:00
requestAuthorization : func ( t * testing . T , _ , downstreamAuthorizeURL , _ , _ , _ string , httpClient * http . Client ) {
2022-02-15 21:52:33 +00:00
requestAuthorizationUsingCLIPasswordFlow ( t ,
downstreamAuthorizeURL ,
env . SupervisorUpstreamLDAP . TestUserMailAttributeValue , // username to present to server during login
env . SupervisorUpstreamLDAP . TestUserPassword , // password to present to server during login
httpClient ,
false ,
)
} ,
2022-02-01 17:03:52 +00:00
editRefreshSessionDataWithoutBreaking : func ( t * testing . T , pinnipedSession * psession . PinnipedSession , idpName , _ string ) [ ] string {
2022-02-15 21:52:33 +00:00
// get the idp, update the config.
client := testlib . NewSupervisorClientset ( t )
ctx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
defer cancel ( )
upstreams := client . IDPV1alpha1 ( ) . LDAPIdentityProviders ( env . SupervisorNamespace )
ldapIDP , err := upstreams . Get ( ctx , idpName , metav1 . GetOptions { } )
require . NoError ( t , err )
ldapIDP . Spec . GroupSearch . Base = ""
_ , err = upstreams . Update ( ctx , ldapIDP , metav1 . UpdateOptions { } )
require . NoError ( t , err )
time . Sleep ( 10 * time . Second ) // wait for controllers to pick up the change
2022-02-01 17:03:52 +00:00
return [ ] string { }
2022-02-15 21:52:33 +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 (
"ldaps://" + env . SupervisorUpstreamLDAP . Host +
"?base=" + url . QueryEscape ( env . SupervisorUpstreamLDAP . UserSearchBase ) +
"&sub=" + base64 . RawURLEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . TestUserUniqueIDAttributeValue ) ) ,
) + "$" ,
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch : func ( _ string ) string {
return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamLDAP . TestUserMailAttributeValue ) + "$"
} ,
2022-02-01 17:03:52 +00:00
wantDownstreamIDTokenGroups : env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs ,
2022-02-15 21:52:33 +00:00
} ,
2022-06-07 23:32:19 +00:00
{
name : "disallowed requested audience using reserved substring on token exchange results in token exchange error" ,
maybeSkip : skipNever ,
createIDP : func ( t * testing . T ) string {
return testlib . CreateTestOIDCIdentityProvider ( t , basicOIDCIdentityProviderSpec ( ) , idpv1alpha1 . PhaseReady ) . Name
} ,
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowOIDC ,
2022-06-13 19:08:11 +00:00
requestTokenExchangeAud : "contains-disallowed-substring.pinniped.dev-something" , // .pinniped.dev substring is not allowed
2022-06-07 23:32:19 +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 : func ( _ string ) string { return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" } ,
wantTokenExchangeResponse : func ( t * testing . T , status int , body string ) {
require . Equal ( t , http . StatusBadRequest , status )
require . Equal ( t ,
` { "error":"invalid_request","error_description":"The request is missing a required parameter, ` +
` includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. ` +
2022-06-13 19:08:11 +00:00
` requested audience cannot contain '.pinniped.dev'"} ` ,
body )
} ,
} ,
{
name : "disallowed requested audience using specific reserved name of a dynamic client on token exchange results in token exchange error" ,
maybeSkip : skipNever ,
createIDP : func ( t * testing . T ) string {
return testlib . CreateTestOIDCIdentityProvider ( t , basicOIDCIdentityProviderSpec ( ) , idpv1alpha1 . PhaseReady ) . Name
} ,
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowOIDC ,
requestTokenExchangeAud : "client.oauth.pinniped.dev-client-name" , // OIDC dynamic client name is not allowed
// 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 : func ( _ string ) string { return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" } ,
wantTokenExchangeResponse : func ( t * testing . T , status int , body string ) {
require . Equal ( t , http . StatusBadRequest , status )
require . Equal ( t ,
` { "error":"invalid_request","error_description":"The request is missing a required parameter, ` +
` includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. ` +
` requested audience cannot contain '.pinniped.dev'"} ` ,
2022-06-07 23:32:19 +00:00
body )
} ,
} ,
{
name : "disallowed requested audience pinniped-cli on token exchange results in token exchange error" ,
maybeSkip : skipNever ,
createIDP : func ( t * testing . T ) string {
return testlib . CreateTestOIDCIdentityProvider ( t , basicOIDCIdentityProviderSpec ( ) , idpv1alpha1 . PhaseReady ) . Name
} ,
requestAuthorization : requestAuthorizationUsingBrowserAuthcodeFlowOIDC ,
requestTokenExchangeAud : "pinniped-cli" , // pinniped-cli is not allowed
// 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 : func ( _ string ) string { return "^" + regexp . QuoteMeta ( env . SupervisorUpstreamOIDC . Issuer + "?sub=" ) + ".+" } ,
wantTokenExchangeResponse : func ( t * testing . T , status int , body string ) {
require . Equal ( t , http . StatusBadRequest , status )
require . Equal ( t ,
` { "error":"invalid_request","error_description":"The request is missing a required parameter, ` +
` includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. ` +
` requested audience cannot equal 'pinniped-cli'"} ` ,
body )
} ,
} ,
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 ,
2022-01-26 00:19:56 +00:00
tt . editRefreshSessionDataWithoutBreaking ,
2021-10-13 22:12:19 +00:00
tt . breakRefreshSessionData ,
2021-10-28 19:00:56 +00:00
tt . createTestUser ,
tt . deleteTestUser ,
2022-06-07 23:32:19 +00:00
tt . requestTokenExchangeAud ,
2022-05-10 23:22:07 +00:00
tt . wantLocalhostCallbackToNeverHappen ,
2021-05-28 23:12:57 +00:00
tt . wantDownstreamIDTokenSubjectToMatch ,
tt . wantDownstreamIDTokenUsernameToMatch ,
tt . wantDownstreamIDTokenGroups ,
2021-10-13 22:12:19 +00:00
tt . wantErrorDescription ,
tt . wantErrorType ,
2022-06-07 23:32:19 +00:00
tt . wantTokenExchangeResponse ,
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 )
}
2022-05-10 19:54:40 +00:00
2021-07-07 16:23:32 +00:00
func requireSuccessfulActiveDirectoryIdentityProviderConditions ( t * testing . T , adIDP * idpv1alpha1 . ActiveDirectoryIdentityProvider , expectedActiveDirectoryConnectionValidMessage string ) {
2021-07-21 20:24:54 +00:00
require . Len ( t , adIDP . Status . Conditions , 4 )
2021-07-07 16:23:32 +00:00
conditionsSummary := [ ] [ ] string { }
for _ , condition := range adIDP . Status . Conditions {
conditionsSummary = append ( conditionsSummary , [ ] string { condition . Type , string ( condition . Status ) , condition . Reason } )
t . Logf ( "Saw ActiveDirectoryIdentityProvider 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 )
2021-07-19 20:54:07 +00:00
case "LDAPConnectionValid" :
2021-07-07 16:23:32 +00:00
require . Equal ( t , expectedActiveDirectoryConnectionValidMessage , condition . Message )
}
}
2021-07-26 23:03:12 +00:00
expectedUserSearchReason := ""
if adIDP . Spec . UserSearch . Base == "" || adIDP . Spec . GroupSearch . Base == "" {
expectedUserSearchReason = "Success"
} else {
expectedUserSearchReason = "UsingConfigurationFromSpec"
}
2021-07-07 16:23:32 +00:00
require . ElementsMatch ( t , [ ] [ ] string {
{ "BindSecretValid" , "True" , "Success" } ,
2021-09-03 00:17:15 +00:00
{ "TLSConfigurationValid" , "True" , "Success" } ,
{ "LDAPConnectionValid" , "True" , "Success" } ,
{ "SearchBaseFound" , "True" , expectedUserSearchReason } ,
} , conditionsSummary )
}
2021-09-07 18:45:32 +00:00
func requireEventuallySuccessfulLDAPIdentityProviderConditions ( t * testing . T , requireEventually * require . Assertions , ldapIDP * idpv1alpha1 . LDAPIdentityProvider , expectedLDAPConnectionValidMessage string ) {
t . Helper ( )
requireEventually . Len ( 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 ActiveDirectoryIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s" ,
condition . Type , string ( condition . Status ) , condition . Reason , condition . Message )
switch condition . Type {
case "BindSecretValid" :
requireEventually . Equal ( "loaded bind secret" , condition . Message )
case "TLSConfigurationValid" :
requireEventually . Equal ( "loaded TLS configuration" , condition . Message )
case "LDAPConnectionValid" :
requireEventually . Equal ( expectedLDAPConnectionValidMessage , condition . Message )
}
}
requireEventually . ElementsMatch ( [ ] [ ] string {
{ "BindSecretValid" , "True" , "Success" } ,
{ "TLSConfigurationValid" , "True" , "Success" } ,
{ "LDAPConnectionValid" , "True" , "Success" } ,
} , conditionsSummary )
}
2021-09-03 00:17:15 +00:00
func requireEventuallySuccessfulActiveDirectoryIdentityProviderConditions ( t * testing . T , requireEventually * require . Assertions , adIDP * idpv1alpha1 . ActiveDirectoryIdentityProvider , expectedActiveDirectoryConnectionValidMessage string ) {
t . Helper ( )
requireEventually . Len ( adIDP . Status . Conditions , 4 )
conditionsSummary := [ ] [ ] string { }
for _ , condition := range adIDP . Status . Conditions {
conditionsSummary = append ( conditionsSummary , [ ] string { condition . Type , string ( condition . Status ) , condition . Reason } )
t . Logf ( "Saw ActiveDirectoryIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s" ,
condition . Type , string ( condition . Status ) , condition . Reason , condition . Message )
switch condition . Type {
case "BindSecretValid" :
requireEventually . Equal ( "loaded bind secret" , condition . Message )
case "TLSConfigurationValid" :
requireEventually . Equal ( "loaded TLS configuration" , condition . Message )
case "LDAPConnectionValid" :
requireEventually . Equal ( expectedActiveDirectoryConnectionValidMessage , condition . Message )
}
}
expectedUserSearchReason := ""
if adIDP . Spec . UserSearch . Base == "" || adIDP . Spec . GroupSearch . Base == "" {
expectedUserSearchReason = "Success"
} else {
expectedUserSearchReason = "UsingConfigurationFromSpec"
}
requireEventually . ElementsMatch ( [ ] [ ] string {
{ "BindSecretValid" , "True" , "Success" } ,
2021-07-07 16:23:32 +00:00
{ "TLSConfigurationValid" , "True" , "Success" } ,
2021-07-19 20:54:07 +00:00
{ "LDAPConnectionValid" , "True" , "Success" } ,
2021-07-26 23:03:12 +00:00
{ "SearchBaseFound" , "True" , expectedUserSearchReason } ,
2021-07-07 16:23:32 +00:00
} , conditionsSummary )
}
2021-04-16 21:04:05 +00:00
2021-04-06 17:10:01 +00:00
func testSupervisorLogin (
t * testing . T ,
2021-11-05 21:18:54 +00:00
createIDP func ( t * testing . T ) string ,
2022-05-10 19:54:40 +00:00
requestAuthorization func ( t * testing . T , downstreamIssuer string , downstreamAuthorizeURL string , downstreamCallbackURL string , username string , password string , httpClient * http . Client ) ,
2022-02-01 17:03:52 +00:00
editRefreshSessionDataWithoutBreaking func ( t * testing . T , pinnipedSession * psession . PinnipedSession , idpName , username string ) [ ] string ,
2022-02-15 21:52:33 +00:00
breakRefreshSessionData func ( t * testing . T , pinnipedSession * psession . PinnipedSession , idpName , username string ) ,
2021-10-28 19:00:56 +00:00
createTestUser func ( t * testing . T ) ( string , string ) ,
deleteTestUser func ( t * testing . T , username string ) ,
2022-06-07 23:32:19 +00:00
requestTokenExchangeAud string ,
2022-05-10 23:22:07 +00:00
wantLocalhostCallbackToNeverHappen bool ,
2021-10-28 19:00:56 +00:00
wantDownstreamIDTokenSubjectToMatch string ,
wantDownstreamIDTokenUsernameToMatch func ( username string ) string ,
wantDownstreamIDTokenGroups [ ] string ,
wantErrorDescription string ,
wantErrorType string ,
2022-06-07 23:32:19 +00:00
wantTokenExchangeResponse func ( t * testing . T , status int , body 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 {
2022-03-08 20:28:09 +00:00
TLSClientConfig : & tls . Config { RootCAs : ca . Pool ( ) } , //nolint:gosec // not concerned with TLS MinVersion here
2020-12-16 03:42:11 +00:00
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.
2021-11-05 21:18:54 +00:00
idpName := createIDP ( t )
2020-12-02 21:50:42 +00:00
2021-10-28 19:00:56 +00:00
username , password := "" , ""
if createTestUser != nil {
username , password = createTestUser ( t )
2022-05-10 19:54:40 +00:00
if deleteTestUser != nil {
defer deleteTestUser ( t , username )
}
2021-10-28 19:00:56 +00:00
}
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.
2022-05-10 19:54:40 +00:00
// Note that this is not using response_type=form_post, so the Supervisor will redirect to the callback endpoint
// directly, without using the Javascript form_post HTML page to POST back to the callback endpoint. The e2e
// tests which use the Pinniped CLI are testing the form_post part of the flow, so that is covered elsewhere.
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.
2022-05-10 19:54:40 +00:00
requestAuthorization ( t , downstream . Spec . Issuer , downstreamAuthorizeURL , localCallbackServer . URL , username , password , httpClient )
2020-12-02 21:50:42 +00:00
// Expect that our callback handler was invoked.
2022-05-10 23:22:07 +00:00
callback , err := localCallbackServer . waitForCallback ( 10 * time . Second )
if wantLocalhostCallbackToNeverHappen {
require . Error ( t , err )
// When we want the localhost callback to have never happened, then this is the end of the test. The login was
// unable to finish so there is nothing to assert about what should have happened with the callback, and there
// won't be any error sent to the callback either.
return
}
// Else, no error.
require . NoError ( t , err )
2021-06-22 15:23:19 +00:00
t . Logf ( "got callback request: %s" , testlib . MaskTokens ( callback . URL . String ( ) ) )
2021-07-22 23:15:44 +00:00
if wantErrorType == "" {
require . Equal ( t , stateParam . String ( ) , callback . URL . Query ( ) . Get ( "state" ) )
require . ElementsMatch ( t , [ ] string { "openid" , "pinniped:request-audience" , "offline_access" } , strings . Split ( callback . URL . Query ( ) . Get ( "scope" ) , " " ) )
authcode := callback . URL . Query ( ) . Get ( "code" )
require . NotEmpty ( t , authcode )
2022-04-13 17:13:27 +00:00
// Authcodes should start with the custom prefix "pin_ac_" to make them identifiable as authcodes when seen by a user out of context.
require . True ( t , strings . HasPrefix ( authcode , "pin_ac_" ) , "token %q did not have expected prefix 'pin_ac_'" , authcode )
2021-07-22 23:15:44 +00:00
// Call the token endpoint to get tokens.
tokenResponse , err := downstreamOAuth2Config . Exchange ( oidcHTTPClientContext , authcode , pkceParam . Verifier ( ) )
require . NoError ( t , err )
expectedIDTokenClaims := [ ] string { "iss" , "exp" , "sub" , "aud" , "auth_time" , "iat" , "jti" , "nonce" , "rat" , "username" , "groups" }
verifyTokenResponse ( t ,
tokenResponse , discovery , downstreamOAuth2Config , nonceParam ,
2021-10-28 19:00:56 +00:00
expectedIDTokenClaims , wantDownstreamIDTokenSubjectToMatch , wantDownstreamIDTokenUsernameToMatch ( username ) , wantDownstreamIDTokenGroups )
2021-07-22 23:15:44 +00:00
// token exchange on the original token
2022-06-07 23:32:19 +00:00
if requestTokenExchangeAud == "" {
requestTokenExchangeAud = "some-cluster-123" // use a default test value
}
doTokenExchange ( t , requestTokenExchangeAud , & downstreamOAuth2Config , tokenResponse , httpClient , discovery , wantTokenExchangeResponse )
2021-07-22 23:15:44 +00:00
2022-02-01 17:03:52 +00:00
refreshedGroups := wantDownstreamIDTokenGroups
2022-01-26 00:19:56 +00:00
if editRefreshSessionDataWithoutBreaking != nil {
latestRefreshToken := tokenResponse . RefreshToken
signatureOfLatestRefreshToken := getFositeDataSignature ( t , latestRefreshToken )
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
kubeClient := testlib . NewKubernetesClientset ( t )
supervisorSecretsClient := kubeClient . CoreV1 ( ) . Secrets ( env . SupervisorNamespace )
oauthStore := oidc . NewKubeStorage ( supervisorSecretsClient , oidc . DefaultOIDCTimeoutsConfiguration ( ) )
storedRefreshSession , err := oauthStore . GetRefreshTokenSession ( ctx , signatureOfLatestRefreshToken , nil )
require . NoError ( t , err )
// Next mutate the part of the session that is used during upstream refresh.
pinnipedSession , ok := storedRefreshSession . GetSession ( ) . ( * psession . PinnipedSession )
require . True ( t , ok , "should have been able to cast session data to PinnipedSession" )
2022-02-01 17:03:52 +00:00
refreshedGroups = editRefreshSessionDataWithoutBreaking ( t , pinnipedSession , idpName , username )
2022-01-26 00:19:56 +00:00
// Then save the mutated Secret back to Kubernetes.
// There is no update function, so delete and create again at the same name.
require . NoError ( t , oauthStore . DeleteRefreshTokenSession ( ctx , signatureOfLatestRefreshToken ) )
require . NoError ( t , oauthStore . CreateRefreshTokenSession ( ctx , signatureOfLatestRefreshToken , storedRefreshSession ) )
}
2021-07-22 23:15:44 +00:00
// 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 )
// 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" }
verifyTokenResponse ( t ,
refreshedTokenResponse , discovery , downstreamOAuth2Config , "" ,
2022-02-01 17:03:52 +00:00
expectRefreshedIDTokenClaims , wantDownstreamIDTokenSubjectToMatch , wantDownstreamIDTokenUsernameToMatch ( username ) , refreshedGroups )
2021-07-22 23:15:44 +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
2022-06-07 23:32:19 +00:00
doTokenExchange ( t , requestTokenExchangeAud , & downstreamOAuth2Config , refreshedTokenResponse , httpClient , discovery , wantTokenExchangeResponse )
2021-10-13 22:12:19 +00:00
// Now that we have successfully performed a refresh, let's test what happens when an
// upstream refresh fails during the next downstream refresh.
if breakRefreshSessionData != nil {
latestRefreshToken := refreshedTokenResponse . RefreshToken
signatureOfLatestRefreshToken := getFositeDataSignature ( t , latestRefreshToken )
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
kubeClient := testlib . NewKubernetesClientset ( t )
supervisorSecretsClient := kubeClient . CoreV1 ( ) . Secrets ( env . SupervisorNamespace )
oauthStore := oidc . NewKubeStorage ( supervisorSecretsClient , oidc . DefaultOIDCTimeoutsConfiguration ( ) )
storedRefreshSession , err := oauthStore . GetRefreshTokenSession ( ctx , signatureOfLatestRefreshToken , nil )
require . NoError ( t , err )
// Next mutate the part of the session that is used during upstream refresh.
pinnipedSession , ok := storedRefreshSession . GetSession ( ) . ( * psession . PinnipedSession )
require . True ( t , ok , "should have been able to cast session data to PinnipedSession" )
2021-10-28 19:00:56 +00:00
breakRefreshSessionData ( t , pinnipedSession , idpName , username )
2021-10-13 22:12:19 +00:00
// Then save the mutated Secret back to Kubernetes.
// There is no update function, so delete and create again at the same name.
require . NoError ( t , oauthStore . DeleteRefreshTokenSession ( ctx , signatureOfLatestRefreshToken ) )
require . NoError ( t , oauthStore . CreateRefreshTokenSession ( ctx , signatureOfLatestRefreshToken , storedRefreshSession ) )
// Now try to perform a downstream refresh again, knowing that the corresponding upstream refresh should fail.
_ , err = downstreamOAuth2Config . TokenSource ( oidcHTTPClientContext , & oauth2 . Token { RefreshToken : latestRefreshToken } ) . Token ( )
// Should have got an error since the upstream refresh should have failed.
require . Error ( t , err )
require . Regexp ( t ,
regexp . QuoteMeta ( "oauth2: cannot fetch token: 401 Unauthorized\n" ) +
2021-11-03 22:17:50 +00:00
regexp . QuoteMeta ( ` Response: { "error":"error","error_description":"Error during upstream refresh. Upstream refresh failed ` ) +
"[^']+" ,
2021-10-13 22:12:19 +00:00
err . Error ( ) ,
)
}
2021-07-22 23:15:44 +00:00
} else {
errorDescription := callback . URL . Query ( ) . Get ( "error_description" )
errorType := callback . URL . Query ( ) . Get ( "error" )
require . Equal ( t , errorDescription , wantErrorDescription )
require . Equal ( t , errorType , wantErrorType )
}
2020-12-10 01:07:37 +00:00
}
2021-10-13 22:12:19 +00:00
// getFositeDataSignature returns the signature of the provided data. The provided data could be an auth code, access
// token, etc. It is assumed that the code is of the format "data.signature", which is how Fosite generates auth codes
// and access tokens.
func getFositeDataSignature ( t * testing . T , data string ) string {
split := strings . Split ( data , "." )
require . Len ( t , split , 2 )
return split [ 1 ]
}
2020-12-10 01:07:37 +00:00
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 )
2022-04-13 17:13:27 +00:00
// Access tokens should start with the custom prefix "pin_at_" to make them identifiable as access tokens when seen by a user out of context.
require . True ( t , strings . HasPrefix ( tokenResponse . AccessToken , "pin_at_" ) , "token %q did not have expected prefix 'pin_at_'" , tokenResponse . AccessToken )
2020-12-05 01:07:04 +00:00
2020-12-10 01:07:37 +00:00
require . NotEmpty ( t , tokenResponse . RefreshToken )
2022-04-13 17:13:27 +00:00
// Refresh tokens should start with the custom prefix "pin_rt_" to make them identifiable as refresh tokens when seen by a user out of context.
require . True ( t , strings . HasPrefix ( tokenResponse . RefreshToken , "pin_rt_" ) , "token %q did not have expected prefix 'pin_rt_'" , tokenResponse . RefreshToken )
2020-12-02 21:50:42 +00:00
}
2020-11-17 15:21:17 +00:00
2022-05-10 19:54:40 +00:00
func requestAuthorizationUsingBrowserAuthcodeFlowOIDC ( 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.
2022-05-10 19:54:40 +00:00
makeAuthorizationRequestAndRequireSecurityHeaders ( ctx , t , downstreamAuthorizeURL , httpClient )
2021-04-14 01:11:16 +00:00
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.
2022-05-09 22:43:36 +00:00
browsertest . LoginToUpstreamOIDC ( 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 )
}
2022-05-10 19:54:40 +00:00
func requestAuthorizationUsingBrowserAuthcodeFlowLDAP ( t * testing . T , downstreamIssuer , downstreamAuthorizeURL , downstreamCallbackURL , username , password string , httpClient * http . Client ) {
t . Helper ( )
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Minute )
defer cancelFunc ( )
// Make the authorize request once "manually" so we can check its response security headers.
makeAuthorizationRequestAndRequireSecurityHeaders ( ctx , t , downstreamAuthorizeURL , httpClient )
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest . Open ( t )
t . Logf ( "opening browser to downstream authorize URL %s" , testlib . MaskTokens ( downstreamAuthorizeURL ) )
require . NoError ( t , page . Navigate ( downstreamAuthorizeURL ) )
// Expect to be redirected to the upstream provider and log in.
browsertest . LoginToUpstreamLDAP ( t , page , downstreamIssuer , username , password )
// 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 )
}
2022-05-10 23:22:07 +00:00
func requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentials ( t * testing . T , downstreamIssuer , downstreamAuthorizeURL , _ , username , password string , _ * http . Client ) {
t . Helper ( )
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest . Open ( t )
t . Logf ( "opening browser to downstream authorize URL %s" , testlib . MaskTokens ( downstreamAuthorizeURL ) )
require . NoError ( t , page . Navigate ( downstreamAuthorizeURL ) )
// This functions assumes that it has been passed either a bad username or a bad password, and submits the
// provided credentials. Expect to be redirected to the upstream provider and attempt to log in.
browsertest . LoginToUpstreamLDAP ( t , page , downstreamIssuer , username , password )
// After failing login expect to land back on the login page again with an error message.
browsertest . WaitForUpstreamLDAPLoginPageWithError ( t , page , downstreamIssuer )
}
func requestAuthorizationUsingBrowserAuthcodeFlowLDAPWithBadCredentialsAndThenGoodCredentials ( t * testing . T , downstreamIssuer , downstreamAuthorizeURL , _ , username , password string , _ * http . Client ) {
t . Helper ( )
// Open the web browser and navigate to the downstream authorize URL.
page := browsertest . Open ( t )
t . Logf ( "opening browser to downstream authorize URL %s" , testlib . MaskTokens ( downstreamAuthorizeURL ) )
require . NoError ( t , page . Navigate ( downstreamAuthorizeURL ) )
// Expect to be redirected to the upstream provider and attempt to log in.
browsertest . LoginToUpstreamLDAP ( t , page , downstreamIssuer , username , "this is the wrong password!" )
// After failing login expect to land back on the login page again with an error message.
browsertest . WaitForUpstreamLDAPLoginPageWithError ( t , page , downstreamIssuer )
// Already at the login page, so this time can directly submit it using the provided username and password.
browsertest . SubmitUpstreamLDAPLoginForm ( t , page , username , password )
}
2022-05-10 19:54:40 +00:00
func makeAuthorizationRequestAndRequireSecurityHeaders ( ctx context . Context , t * testing . T , downstreamAuthorizeURL string , httpClient * http . Client ) {
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-08-24 19:19:29 +00:00
func requestAuthorizationUsingCLIPasswordFlow ( t * testing . T , downstreamAuthorizeURL , upstreamUsername , upstreamPassword string , httpClient * http . Client , wantErr bool ) {
2021-04-06 17:10:01 +00:00
t . Helper ( )
2021-04-07 19:56:09 +00:00
2021-07-15 18:32:15 +00:00
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Minute )
2021-04-07 19:56:09 +00:00
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
2021-08-18 23:24:05 +00:00
} , 60 * time . Second , 200 * time . Millisecond )
2021-06-02 18:36:48 +00:00
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" )
2021-07-22 23:15:44 +00:00
if wantErr {
require . Contains ( t , redirectLocation , "error_description" )
} else {
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
2022-05-10 23:22:07 +00:00
func ( s * localCallbackServer ) waitForCallback ( timeout time . Duration ) ( * http . Request , error ) {
2020-12-02 21:50:42 +00:00
select {
case callback := <- s . callbacks :
2022-05-10 23:22:07 +00:00
return callback , nil
2020-12-02 21:50:42 +00:00
case <- time . After ( timeout ) :
2022-05-10 23:22:07 +00:00
return nil , errors . New ( "timed out waiting for callback request" )
2020-12-02 21:50:42 +00:00
}
2020-11-17 15:21:17 +00:00
}
2020-12-09 16:23:10 +00:00
2022-06-07 23:32:19 +00:00
func doTokenExchange (
t * testing . T ,
requestTokenExchangeAud string ,
config * oauth2 . Config ,
tokenResponse * oauth2 . Token ,
httpClient * http . Client ,
provider * coreosoidc . Provider ,
wantTokenExchangeResponse func ( t * testing . T , status int , body string ) ,
) {
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" } ,
2022-06-07 23:32:19 +00:00
"audience" : [ ] string { requestTokenExchangeAud } ,
2020-12-09 16:23:10 +00:00
"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 )
2022-06-13 19:08:11 +00:00
defer func ( ) { _ = resp . Body . Close ( ) } ( )
2022-06-07 23:32:19 +00:00
// If a function was passed, call it, so it can make the desired assertions.
if wantTokenExchangeResponse != nil {
body , err := ioutil . ReadAll ( resp . Body )
require . NoError ( t , err )
wantTokenExchangeResponse ( t , resp . StatusCode , string ( body ) )
return // the above call should have made all desired assertions about the response, so return
}
// Else, want a successful response.
2020-12-17 01:59:39 +00:00
require . Equal ( t , resp . StatusCode , http . StatusOK )
2022-06-07 23:32:19 +00:00
2020-12-09 16:23:10 +00:00
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 ) )
2022-06-07 23:32:19 +00:00
var clusterVerifier = provider . Verifier ( & coreosoidc . Config { ClientID : requestTokenExchangeAud } )
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
2022-06-07 18:20:59 +00:00
cspHeader := h . Get ( "Content-Security-Policy" )
require . Contains ( t , cspHeader , "script-src '" ) // loose assertion
require . Contains ( t , cspHeader , "style-src '" ) // loose assertion
require . Contains ( t , cspHeader , "img-src data:" )
require . Contains ( t , cspHeader , "connect-src *" )
require . Contains ( t , cspHeader , "default-src 'none'" )
require . Contains ( t , cspHeader , "frame-ancestors 'none'" )
2020-12-16 03:42:11 +00:00
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" ) )
}