2022-04-29 23:01:51 +00:00
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package login
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/require"
2022-07-20 20:55:56 +00:00
"golang.org/x/crypto/bcrypt"
2022-04-29 23:01:51 +00:00
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/fake"
2022-07-14 16:51:11 +00:00
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
2022-04-29 23:01:51 +00:00
"go.pinniped.dev/internal/authenticators"
"go.pinniped.dev/internal/oidc"
"go.pinniped.dev/internal/oidc/jwks"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/oidctestutil"
)
func TestPostLoginEndpoint ( t * testing . T ) {
const (
htmlContentType = "text/html; charset=utf-8"
happyDownstreamCSRF = "test-csrf"
happyDownstreamPKCE = "test-pkce"
happyDownstreamNonce = "test-nonce"
happyDownstreamStateVersion = "2"
happyEncodedUpstreamState = "fake-encoded-state-param-value"
downstreamIssuer = "https://my-downstream-issuer.com/path"
downstreamRedirectURI = "http://127.0.0.1/callback"
2022-07-21 16:26:00 +00:00
downstreamPinnipedCLIClientID = "pinniped-cli"
downstreamDynamicClientID = "client.oauth.pinniped.dev-test-name"
downstreamDynamicClientUID = "fake-client-uid"
2022-04-29 23:01:51 +00:00
happyDownstreamState = "8b-state"
downstreamNonce = "some-nonce-value"
downstreamPKCEChallenge = "some-challenge"
downstreamPKCEChallengeMethod = "S256"
ldapUpstreamName = "some-ldap-idp"
ldapUpstreamType = "ldap"
ldapUpstreamResourceUID = "ldap-resource-uid"
activeDirectoryUpstreamName = "some-active-directory-idp"
activeDirectoryUpstreamType = "activedirectory"
activeDirectoryUpstreamResourceUID = "active-directory-resource-uid"
upstreamLDAPURL = "ldaps://some-ldap-host:123?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev"
userParam = "username"
passParam = "password"
badUserPassErrParamValue = "login_error"
internalErrParamValue = "internal_error"
)
var (
fositeMissingCodeChallengeErrorQuery = map [ string ] string {
"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. Clients must include a code_challenge when performing the authorize code flow, but it is missing." ,
"state" : happyDownstreamState ,
}
fositeInvalidCodeChallengeErrorQuery = map [ string ] string {
"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. The code_challenge_method is not supported, use S256 instead." ,
"state" : happyDownstreamState ,
}
fositeMissingCodeChallengeMethodErrorQuery = map [ string ] string {
"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. Clients must use code_challenge_method=S256, plain is not allowed." ,
"state" : happyDownstreamState ,
}
fositePromptHasNoneAndOtherValueErrorQuery = map [ string ] string {
"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. Parameter 'prompt' was set to 'none', but contains other values as well which is not allowed." ,
"state" : happyDownstreamState ,
}
)
2022-06-15 15:00:17 +00:00
happyDownstreamScopesRequested := [ ] string { "openid" , "groups" }
happyDownstreamScopesGranted := [ ] string { "openid" , "groups" }
2022-04-29 23:01:51 +00:00
happyDownstreamRequestParamsQuery := url . Values {
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { strings . Join ( happyDownstreamScopesRequested , " " ) } ,
2022-07-21 16:26:00 +00:00
"client_id" : [ ] string { downstreamPinnipedCLIClientID } ,
2022-04-29 23:01:51 +00:00
"state" : [ ] string { happyDownstreamState } ,
"nonce" : [ ] string { downstreamNonce } ,
"code_challenge" : [ ] string { downstreamPKCEChallenge } ,
"code_challenge_method" : [ ] string { downstreamPKCEChallengeMethod } ,
"redirect_uri" : [ ] string { downstreamRedirectURI } ,
}
happyDownstreamRequestParams := happyDownstreamRequestParamsQuery . Encode ( )
2022-07-21 16:26:00 +00:00
happyDownstreamRequestParamsQueryForDynamicClient := shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "client_id" : downstreamDynamicClientID } ,
)
happyDownstreamRequestParamsForDynamicClient := happyDownstreamRequestParamsQueryForDynamicClient . Encode ( )
2022-04-29 23:01:51 +00:00
happyLDAPDecodedState := & oidc . UpstreamStateParamData {
AuthParams : happyDownstreamRequestParams ,
UpstreamName : ldapUpstreamName ,
UpstreamType : ldapUpstreamType ,
Nonce : happyDownstreamNonce ,
CSRFToken : happyDownstreamCSRF ,
PKCECode : happyDownstreamPKCE ,
FormatVersion : happyDownstreamStateVersion ,
}
modifyHappyLDAPDecodedState := func ( edit func ( * oidc . UpstreamStateParamData ) ) * oidc . UpstreamStateParamData {
copyOfHappyLDAPDecodedState := * happyLDAPDecodedState
edit ( & copyOfHappyLDAPDecodedState )
return & copyOfHappyLDAPDecodedState
}
2022-07-21 16:26:00 +00:00
happyLDAPDecodedStateForDynamicClient := modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = happyDownstreamRequestParamsForDynamicClient
} )
happyActiveDirectoryDecodedState := modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . UpstreamName = activeDirectoryUpstreamName
data . UpstreamType = activeDirectoryUpstreamType
} )
happyActiveDirectoryDecodedStateForDynamicClient := modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = happyDownstreamRequestParamsForDynamicClient
data . UpstreamName = activeDirectoryUpstreamName
data . UpstreamType = activeDirectoryUpstreamType
} )
2022-04-29 23:01:51 +00:00
happyLDAPUsername := "some-ldap-user"
happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username"
happyLDAPPassword := "some-ldap-password" //nolint:gosec
happyLDAPUID := "some-ldap-uid"
happyLDAPUserDN := "cn=foo,dn=bar"
happyLDAPGroups := [ ] string { "group1" , "group2" , "group3" }
happyLDAPExtraRefreshAttribute := "some-refresh-attribute"
happyLDAPExtraRefreshValue := "some-refresh-attribute-value"
parsedUpstreamLDAPURL , err := url . Parse ( upstreamLDAPURL )
require . NoError ( t , err )
ldapAuthenticateFunc := func ( ctx context . Context , username , password string ) ( * authenticators . Response , bool , error ) {
if username == "" || password == "" {
return nil , false , fmt . Errorf ( "should not have passed empty username or password to the authenticator" )
}
if username == happyLDAPUsername && password == happyLDAPPassword {
return & authenticators . Response {
User : & user . DefaultInfo {
Name : happyLDAPUsernameFromAuthenticator ,
UID : happyLDAPUID ,
Groups : happyLDAPGroups ,
} ,
DN : happyLDAPUserDN ,
ExtraRefreshAttributes : map [ string ] string {
happyLDAPExtraRefreshAttribute : happyLDAPExtraRefreshValue ,
} ,
} , true , nil
}
return nil , false , nil
}
upstreamLDAPIdentityProvider := oidctestutil . TestUpstreamLDAPIdentityProvider {
Name : ldapUpstreamName ,
ResourceUID : ldapUpstreamResourceUID ,
URL : parsedUpstreamLDAPURL ,
AuthenticateFunc : ldapAuthenticateFunc ,
}
upstreamActiveDirectoryIdentityProvider := oidctestutil . TestUpstreamLDAPIdentityProvider {
Name : activeDirectoryUpstreamName ,
ResourceUID : activeDirectoryUpstreamResourceUID ,
URL : parsedUpstreamLDAPURL ,
AuthenticateFunc : ldapAuthenticateFunc ,
}
erroringUpstreamLDAPIdentityProvider := oidctestutil . TestUpstreamLDAPIdentityProvider {
Name : ldapUpstreamName ,
ResourceUID : ldapUpstreamResourceUID ,
AuthenticateFunc : func ( ctx context . Context , username , password string ) ( * authenticators . Response , bool , error ) {
return nil , false , fmt . Errorf ( "some ldap upstream auth error" )
} ,
}
expectedHappyActiveDirectoryUpstreamCustomSession := & psession . CustomSessionData {
ProviderUID : activeDirectoryUpstreamResourceUID ,
ProviderName : activeDirectoryUpstreamName ,
ProviderType : psession . ProviderTypeActiveDirectory ,
OIDC : nil ,
LDAP : nil ,
ActiveDirectory : & psession . ActiveDirectorySessionData {
UserDN : happyLDAPUserDN ,
ExtraRefreshAttributes : map [ string ] string { happyLDAPExtraRefreshAttribute : happyLDAPExtraRefreshValue } ,
} ,
}
expectedHappyLDAPUpstreamCustomSession := & psession . CustomSessionData {
ProviderUID : ldapUpstreamResourceUID ,
ProviderName : ldapUpstreamName ,
ProviderType : psession . ProviderTypeLDAP ,
OIDC : nil ,
LDAP : & psession . LDAPSessionData {
UserDN : happyLDAPUserDN ,
ExtraRefreshAttributes : map [ string ] string { happyLDAPExtraRefreshAttribute : happyLDAPExtraRefreshValue } ,
} ,
ActiveDirectory : nil ,
}
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
2022-06-15 15:00:17 +00:00
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + ` \?code=([^&]+)&scope=openid\+groups&state= ` + happyDownstreamState
2022-04-29 23:01:51 +00:00
happyUsernamePasswordFormParams := url . Values { userParam : [ ] string { happyLDAPUsername } , passParam : [ ] string { happyLDAPPassword } }
encodeQuery := func ( query map [ string ] string ) string {
values := url . Values { }
for k , v := range query {
values [ k ] = [ ] string { v }
}
return values . Encode ( )
}
urlWithQuery := func ( baseURL string , query map [ string ] string ) string {
urlToReturn := fmt . Sprintf ( "%s?%s" , baseURL , encodeQuery ( query ) )
_ , err := url . Parse ( urlToReturn )
require . NoError ( t , err , "urlWithQuery helper was used to create an illegal URL" )
return urlToReturn
}
2022-07-21 16:26:00 +00:00
addFullyCapableDynamicClientAndSecretToKubeResources := func ( t * testing . T , supervisorClient * supervisorfake . Clientset , kubeClient * fake . Clientset ) {
oidcClient , secret := testutil . FullyCapableOIDCClientAndStorageSecret ( t ,
"some-namespace" , downstreamDynamicClientID , downstreamDynamicClientUID , downstreamRedirectURI , [ ] string { testutil . HashedPassword1AtGoMinCost } )
require . NoError ( t , supervisorClient . Tracker ( ) . Add ( oidcClient ) )
require . NoError ( t , kubeClient . Tracker ( ) . Add ( secret ) )
}
2022-04-29 23:01:51 +00:00
tests := [ ] struct {
2022-07-20 20:55:56 +00:00
name string
idps * oidctestutil . UpstreamIDPListerBuilder
kubeResources func ( t * testing . T , supervisorClient * supervisorfake . Clientset , kubeClient * fake . Clientset )
decodedState * oidc . UpstreamStateParamData
formParams url . Values
reqURIQuery url . Values
2022-04-29 23:01:51 +00:00
wantStatus int
wantContentType string
wantBodyString string
wantErr string
// Assertion that the response should be a redirect to the login page with an error param.
wantRedirectToLoginPageError string
// Assertions for when an authcode should be returned, i.e. the request was authenticated by an
// upstream LDAP or AD provider.
wantRedirectLocationRegexp string // for loose matching
wantRedirectLocationString string // for exact matching instead
2022-05-04 19:12:14 +00:00
wantBodyFormResponseRegexp string // for form_post html page matching instead
2022-04-29 23:01:51 +00:00
wantDownstreamRedirectURI string
wantDownstreamGrantedScopes [ ] string
wantDownstreamIDTokenSubject string
wantDownstreamIDTokenUsername string
wantDownstreamIDTokenGroups [ ] string
wantDownstreamRequestedScopes [ ] string
wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string
wantDownstreamNonce string
2022-07-21 16:26:00 +00:00
wantDownstreamClient string
2022-04-29 23:01:51 +00:00
wantDownstreamCustomSessionData * psession . CustomSessionData
// Authorization requests for either a successful OIDC upstream or for an error with any upstream
// should never use Kube storage. There is only one exception to this rule, which is that certain
// OIDC validations are checked in fosite after the OAuth authcode (and sometimes the OIDC session)
// is stored, so it is possible with an LDAP upstream to store objects and then return an error to
// the client anyway (which makes the stored objects useless, but oh well).
wantUnnecessaryStoredRecords int
} {
{
name : "happy LDAP login" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) .
WithLDAP ( & upstreamLDAPIdentityProvider ) . // should pick this one
WithActiveDirectory ( & erroringUpstreamLDAPIdentityProvider ) ,
decodedState : happyLDAPDecodedState ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : happyAuthcodeDownstreamRedirectLocationRegexp ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
2022-07-21 16:26:00 +00:00
wantDownstreamClient : downstreamPinnipedCLIClientID ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "happy LDAP login with dynamic client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) .
WithLDAP ( & upstreamLDAPIdentityProvider ) . // should pick this one
WithActiveDirectory ( & erroringUpstreamLDAPIdentityProvider ) ,
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : happyLDAPDecodedStateForDynamicClient ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : happyAuthcodeDownstreamRedirectLocationRegexp ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamClient : downstreamDynamicClientID ,
2022-04-29 23:01:51 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "happy AD login" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) .
WithLDAP ( & erroringUpstreamLDAPIdentityProvider ) .
WithActiveDirectory ( & upstreamActiveDirectoryIdentityProvider ) , // should pick this one
decodedState : happyActiveDirectoryDecodedState ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : happyAuthcodeDownstreamRedirectLocationRegexp ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
2022-07-21 16:26:00 +00:00
wantDownstreamClient : downstreamPinnipedCLIClientID ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyActiveDirectoryUpstreamCustomSession ,
} ,
{
name : "happy AD login with dynamic client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) .
WithLDAP ( & erroringUpstreamLDAPIdentityProvider ) .
WithActiveDirectory ( & upstreamActiveDirectoryIdentityProvider ) , // should pick this one
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : happyActiveDirectoryDecodedStateForDynamicClient ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : happyAuthcodeDownstreamRedirectLocationRegexp ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamClient : downstreamDynamicClientID ,
2022-04-29 23:01:51 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyActiveDirectoryUpstreamCustomSession ,
} ,
2022-05-04 19:12:14 +00:00
{
name : "happy LDAP login when downstream response_mode=form_post returns 200 with HTML+JS form" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "response_mode" : "form_post" } ,
) . Encode ( )
2022-05-04 19:12:14 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusOK ,
wantContentType : htmlContentType ,
wantBodyFormResponseRegexp : ` (?s)<html.*<script>.*To finish logging in, paste this authorization code ` +
` .*<form>.*<code id="manual-auth-code">(.+)</code>.*</html> ` , // "(?s)" means match "." across newlines
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
2022-07-21 16:26:00 +00:00
wantDownstreamClient : downstreamPinnipedCLIClientID ,
2022-05-04 19:12:14 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
2022-04-29 23:01:51 +00:00
{
name : "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "redirect_uri" : "http://127.0.0.1:4242/callback" } ,
) . Encode ( )
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : "http://127.0.0.1:4242/callback" + ` \?code=([^&]+)&scope=openid\+groups&state= ` + happyDownstreamState ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : "http://127.0.0.1:4242/callback" ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamClient : downstreamPinnipedCLIClientID ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "happy LDAP login when downstream redirect uri matches what is configured for client except for the port number with dynamic client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQueryForDynamicClient ,
map [ string ] string { "redirect_uri" : "http://127.0.0.1:4242/callback" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
2022-06-15 15:00:17 +00:00
wantRedirectLocationRegexp : "http://127.0.0.1:4242/callback" + ` \?code=([^&]+)&scope=openid\+groups&state= ` + happyDownstreamState ,
2022-04-29 23:01:51 +00:00
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : "http://127.0.0.1:4242/callback" ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
2022-07-21 16:26:00 +00:00
wantDownstreamClient : downstreamDynamicClientID ,
2022-04-29 23:01:51 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "happy LDAP login when there are additional allowed downstream requested scopes" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "scope" : "openid offline_access pinniped:request-audience" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state= ` + happyDownstreamState ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : [ ] string { "openid" , "offline_access" , "pinniped:request-audience" } ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : [ ] string { "openid" , "offline_access" , "pinniped:request-audience" } ,
wantDownstreamNonce : downstreamNonce ,
2022-07-21 16:26:00 +00:00
wantDownstreamClient : downstreamPinnipedCLIClientID ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "happy LDAP login when there are additional allowed downstream requested scopes with dynamic client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQueryForDynamicClient ,
map [ string ] string { "scope" : "openid offline_access pinniped:request-audience" } ,
) . Encode ( )
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=openid\+offline_access\+pinniped%3Arequest-audience&state= ` + happyDownstreamState ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : [ ] string { "openid" , "offline_access" , "pinniped:request-audience" } ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : [ ] string { "openid" , "offline_access" , "pinniped:request-audience" } ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamClient : downstreamDynamicClientID ,
2022-04-29 23:01:51 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "happy LDAP when downstream OIDC validations are skipped because the openid scope was not requested" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string {
"scope" : "email" ,
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
"prompt" : "none login" ,
} ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=&state= ` + happyDownstreamState , // no scopes granted
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : [ ] string { "email" } , // only email was requested
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : [ ] string { } , // no scopes granted
wantDownstreamNonce : downstreamNonce ,
2022-07-21 16:26:00 +00:00
wantDownstreamClient : downstreamPinnipedCLIClientID ,
2022-04-29 23:01:51 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
2022-06-15 15:00:17 +00:00
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "happy LDAP login when groups scope is not requested" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) .
WithLDAP ( & upstreamLDAPIdentityProvider ) . // should pick this one
WithActiveDirectory ( & erroringUpstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "scope" : "openid" } ,
) . Encode ( )
2022-06-15 15:00:17 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=openid&state= ` + happyDownstreamState ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamRequestedScopes : [ ] string { "openid" } ,
wantDownstreamRedirectURI : downstreamRedirectURI ,
wantDownstreamGrantedScopes : [ ] string { "openid" } ,
wantDownstreamNonce : downstreamNonce ,
2022-07-21 16:26:00 +00:00
wantDownstreamClient : downstreamPinnipedCLIClientID ,
2022-06-15 15:00:17 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
2022-04-29 23:01:51 +00:00
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyLDAPUpstreamCustomSession ,
} ,
{
name : "bad username LDAP login" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : happyLDAPDecodedState ,
formParams : url . Values { userParam : [ ] string { "wrong!" } , passParam : [ ] string { happyLDAPPassword } } ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectToLoginPageError : badUserPassErrParamValue ,
} ,
{
name : "bad password LDAP login" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : happyLDAPDecodedState ,
formParams : url . Values { userParam : [ ] string { happyLDAPUsername } , passParam : [ ] string { "wrong!" } } ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectToLoginPageError : badUserPassErrParamValue ,
} ,
{
name : "blank username LDAP login" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : happyLDAPDecodedState ,
formParams : url . Values { userParam : [ ] string { "" } , passParam : [ ] string { happyLDAPPassword } } ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectToLoginPageError : badUserPassErrParamValue ,
} ,
{
name : "blank password LDAP login" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : happyLDAPDecodedState ,
formParams : url . Values { userParam : [ ] string { happyLDAPUsername } , passParam : [ ] string { "" } } ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectToLoginPageError : badUserPassErrParamValue ,
} ,
{
name : "username and password sent as URI query params should be ignored since they are expected in form post body" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : happyLDAPDecodedState ,
reqURIQuery : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectToLoginPageError : badUserPassErrParamValue ,
} ,
{
name : "error during upstream LDAP authentication" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & erroringUpstreamLDAPIdentityProvider ) ,
decodedState : happyLDAPDecodedState ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectToLoginPageError : internalErrParamValue ,
} ,
{
name : "downstream redirect uri does not match what is configured for client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "redirect_uri" : "http://127.0.0.1/wrong_callback" } ,
) . Encode ( )
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "downstream redirect uri does not match what is configured for client with dynamic client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQueryForDynamicClient ,
map [ string ] string { "redirect_uri" : "http://127.0.0.1/wrong_callback" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "downstream client does not exist" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "client_id" : "wrong_client_id" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "downstream client is missing" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "client_id" : "" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "response type is unsupported" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "response_type" : "unsupported" } ,
) . Encode ( )
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "response type form_post is unsupported for dynamic clients" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQueryForDynamicClient ,
map [ string ] string { "response_type" : "form_post" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "response type is missing" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "response_type" : "" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "PKCE code_challenge is missing" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "code_challenge" : "" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationString : urlWithQuery ( downstreamRedirectURI , fositeMissingCodeChallengeErrorQuery ) ,
wantUnnecessaryStoredRecords : 2 , // fosite already stored the authcode and oidc session before it noticed the error
} ,
{
name : "PKCE code_challenge_method is invalid" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "code_challenge_method" : "this-is-not-a-valid-pkce-alg" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationString : urlWithQuery ( downstreamRedirectURI , fositeInvalidCodeChallengeErrorQuery ) ,
wantUnnecessaryStoredRecords : 2 , // fosite already stored the authcode and oidc session before it noticed the error
} ,
{
name : "PKCE code_challenge_method is `plain`" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "code_challenge_method" : "plain" } , // plain is not allowed
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationString : urlWithQuery ( downstreamRedirectURI , fositeMissingCodeChallengeMethodErrorQuery ) ,
wantUnnecessaryStoredRecords : 2 , // fosite already stored the authcode and oidc session before it noticed the error
} ,
{
name : "PKCE code_challenge_method is missing" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "code_challenge_method" : "" } ,
) . Encode ( )
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationString : urlWithQuery ( downstreamRedirectURI , fositeMissingCodeChallengeMethodErrorQuery ) ,
wantUnnecessaryStoredRecords : 2 , // fosite already stored the authcode and oidc session before it noticed the error
} ,
{
name : "PKCE code_challenge_method is missing with dynamic client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQueryForDynamicClient ,
map [ string ] string { "code_challenge_method" : "" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationString : urlWithQuery ( downstreamRedirectURI , fositeMissingCodeChallengeMethodErrorQuery ) ,
wantUnnecessaryStoredRecords : 2 , // fosite already stored the authcode and oidc session before it noticed the error
} ,
{
name : "prompt param is not allowed to have none and another legal value at the same time" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "prompt" : "none login" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationString : urlWithQuery ( downstreamRedirectURI , fositePromptHasNoneAndOtherValueErrorQuery ) ,
wantUnnecessaryStoredRecords : 1 , // fosite already stored the authcode before it noticed the error
} ,
{
name : "downstream state does not have enough entropy" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "state" : "short" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "downstream scopes do not match what is configured for client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
2022-07-21 16:26:00 +00:00
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery ,
map [ string ] string { "scope" : "openid offline_access pinniped:request-audience scope_not_allowed" } ,
) . Encode ( )
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "downstream scopes do not match what is configured for client with dynamic client" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
kubeResources : addFullyCapableDynamicClientAndSecretToKubeResources ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
data . AuthParams = shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQueryForDynamicClient ,
map [ string ] string { "scope" : "openid offline_access pinniped:request-audience scope_not_allowed" } ,
) . Encode ( )
2022-04-29 23:01:51 +00:00
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error using state downstream auth params" ,
} ,
{
name : "no upstream providers are configured or provider cannot be found by name" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) , // empty
decodedState : happyLDAPDecodedState ,
formParams : happyUsernamePasswordFormParams ,
wantErr : "error finding upstream provider: provider not found" ,
} ,
{
name : "upstream provider cannot be found by name and type" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : happyActiveDirectoryDecodedState , // correct upstream IDP name, but wrong upstream IDP type
formParams : happyUsernamePasswordFormParams ,
wantErr : "error finding upstream provider: provider not found" ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
2022-05-03 23:46:09 +00:00
t . Parallel ( )
2022-04-29 23:01:51 +00:00
kubeClient := fake . NewSimpleClientset ( )
2022-07-20 20:55:56 +00:00
supervisorClient := supervisorfake . NewSimpleClientset ( )
2022-04-29 23:01:51 +00:00
secretsClient := kubeClient . CoreV1 ( ) . Secrets ( "some-namespace" )
2022-07-20 20:55:56 +00:00
oidcClientsClient := supervisorClient . ConfigV1alpha1 ( ) . OIDCClients ( "some-namespace" )
2022-07-21 16:26:00 +00:00
if tt . kubeResources != nil {
tt . kubeResources ( t , supervisorClient , kubeClient )
2022-07-20 20:55:56 +00:00
}
2022-04-29 23:01:51 +00:00
// Configure fosite the same way that the production code would.
// Inject this into our test subject at the last second so we get a fresh storage for every test.
timeoutsConfiguration := oidc . DefaultOIDCTimeoutsConfiguration ( )
2022-07-20 20:55:56 +00:00
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
kubeOauthStore := oidc . NewKubeStorage ( secretsClient , oidcClientsClient , timeoutsConfiguration , bcrypt . MinCost )
2022-04-29 23:01:51 +00:00
hmacSecretFunc := func ( ) [ ] byte { return [ ] byte ( "some secret - must have at least 32 bytes" ) }
require . GreaterOrEqual ( t , len ( hmacSecretFunc ( ) ) , 32 , "fosite requires that hmac secrets have at least 32 bytes" )
jwksProviderIsUnused := jwks . NewDynamicJWKSProvider ( )
oauthHelper := oidc . FositeOauth2Helper ( kubeOauthStore , downstreamIssuer , hmacSecretFunc , jwksProviderIsUnused , timeoutsConfiguration )
req := httptest . NewRequest ( http . MethodPost , "/ignored" , strings . NewReader ( tt . formParams . Encode ( ) ) )
req . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
if tt . reqURIQuery != nil {
req . URL . RawQuery = tt . reqURIQuery . Encode ( )
}
rsp := httptest . NewRecorder ( )
subject := NewPostHandler ( downstreamIssuer , tt . idps . Build ( ) , oauthHelper )
err := subject ( rsp , req , happyEncodedUpstreamState , tt . decodedState )
if tt . wantErr != "" {
require . EqualError ( t , err , tt . wantErr )
2022-07-21 16:26:00 +00:00
require . Empty ( t , oidctestutil . FilterClientSecretCreateActions ( kubeClient . Actions ( ) ) )
2022-04-29 23:01:51 +00:00
return // the http response doesn't matter when the function returns an error, because the caller should handle the error
}
// Otherwise, expect no error.
require . NoError ( t , err )
require . Equal ( t , tt . wantStatus , rsp . Code )
testutil . RequireEqualContentType ( t , rsp . Header ( ) . Get ( "Content-Type" ) , tt . wantContentType )
actualLocation := rsp . Header ( ) . Get ( "Location" )
switch {
case tt . wantRedirectLocationRegexp != "" :
2022-05-04 19:12:14 +00:00
// Expecting a success redirect to the client.
require . Equal ( t , tt . wantBodyString , rsp . Body . String ( ) )
2022-04-29 23:01:51 +00:00
require . Len ( t , rsp . Header ( ) . Values ( "Location" ) , 1 )
oidctestutil . RequireAuthCodeRegexpMatch (
t ,
actualLocation ,
2022-05-03 23:46:09 +00:00
tt . wantRedirectLocationRegexp ,
2022-04-29 23:01:51 +00:00
kubeClient ,
secretsClient ,
kubeOauthStore ,
2022-05-03 23:46:09 +00:00
tt . wantDownstreamGrantedScopes ,
tt . wantDownstreamIDTokenSubject ,
tt . wantDownstreamIDTokenUsername ,
tt . wantDownstreamIDTokenGroups ,
tt . wantDownstreamRequestedScopes ,
tt . wantDownstreamPKCEChallenge ,
tt . wantDownstreamPKCEChallengeMethod ,
tt . wantDownstreamNonce ,
2022-07-21 16:26:00 +00:00
tt . wantDownstreamClient ,
2022-05-03 23:46:09 +00:00
tt . wantDownstreamRedirectURI ,
tt . wantDownstreamCustomSessionData ,
2022-04-29 23:01:51 +00:00
)
case tt . wantRedirectToLoginPageError != "" :
2022-05-04 19:12:14 +00:00
// Expecting an error redirect to the login UI page.
require . Equal ( t , tt . wantBodyString , rsp . Body . String ( ) )
2022-04-29 23:01:51 +00:00
expectedLocation := downstreamIssuer + oidc . PinnipedLoginPath +
"?err=" + tt . wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState
require . Equal ( t , expectedLocation , actualLocation )
2022-07-21 16:26:00 +00:00
require . Len ( t , oidctestutil . FilterClientSecretCreateActions ( kubeClient . Actions ( ) ) , tt . wantUnnecessaryStoredRecords )
2022-04-29 23:01:51 +00:00
case tt . wantRedirectLocationString != "" :
2022-05-04 19:12:14 +00:00
// Expecting an error redirect to the client.
require . Equal ( t , tt . wantBodyString , rsp . Body . String ( ) )
2022-04-29 23:01:51 +00:00
require . Equal ( t , tt . wantRedirectLocationString , actualLocation )
2022-07-21 16:26:00 +00:00
require . Len ( t , oidctestutil . FilterClientSecretCreateActions ( kubeClient . Actions ( ) ) , tt . wantUnnecessaryStoredRecords )
2022-05-04 19:12:14 +00:00
case tt . wantBodyFormResponseRegexp != "" :
// Expecting the body of the response to be a html page with a form (for "response_mode=form_post").
_ , hasLocationHeader := rsp . Header ( ) [ "Location" ]
require . False ( t , hasLocationHeader )
oidctestutil . RequireAuthCodeRegexpMatch (
t ,
rsp . Body . String ( ) ,
tt . wantBodyFormResponseRegexp ,
kubeClient ,
secretsClient ,
kubeOauthStore ,
tt . wantDownstreamGrantedScopes ,
tt . wantDownstreamIDTokenSubject ,
tt . wantDownstreamIDTokenUsername ,
tt . wantDownstreamIDTokenGroups ,
tt . wantDownstreamRequestedScopes ,
tt . wantDownstreamPKCEChallenge ,
tt . wantDownstreamPKCEChallengeMethod ,
tt . wantDownstreamNonce ,
2022-07-21 16:26:00 +00:00
tt . wantDownstreamClient ,
2022-05-04 19:12:14 +00:00
tt . wantDownstreamRedirectURI ,
tt . wantDownstreamCustomSessionData ,
)
2022-04-29 23:01:51 +00:00
default :
2022-05-04 19:12:14 +00:00
require . Failf ( t , "test should have expected a redirect or form body" ,
2022-04-29 23:01:51 +00:00
"actual location was %q" , actualLocation )
}
} )
}
}
2022-07-21 16:26:00 +00:00
func shallowCopyAndModifyQuery ( query url . Values , modifications map [ string ] string ) url . Values {
copied := url . Values { }
for key , value := range query {
copied [ key ] = value
}
for key , value := range modifications {
if value == "" {
copied . Del ( key )
} else {
copied [ key ] = [ ] string { value }
}
}
return copied
}