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"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/fake"
"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"
downstreamClientID = "pinniped-cli"
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 ,
}
)
happyDownstreamScopesRequested := [ ] string { "openid" }
happyDownstreamScopesGranted := [ ] string { "openid" }
happyDownstreamRequestParamsQuery := url . Values {
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { strings . Join ( happyDownstreamScopesRequested , " " ) } ,
"client_id" : [ ] string { downstreamClientID } ,
"state" : [ ] string { happyDownstreamState } ,
"nonce" : [ ] string { downstreamNonce } ,
"code_challenge" : [ ] string { downstreamPKCEChallenge } ,
"code_challenge_method" : [ ] string { downstreamPKCEChallengeMethod } ,
"redirect_uri" : [ ] string { downstreamRedirectURI } ,
}
happyDownstreamRequestParams := happyDownstreamRequestParamsQuery . Encode ( )
copyOfHappyDownstreamRequestParamsQuery := func ( ) url . Values {
params := url . Values { }
for k , v := range happyDownstreamRequestParamsQuery {
params [ k ] = make ( [ ] string , len ( v ) )
copy ( params [ k ] , v )
}
return params
}
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
}
happyActiveDirectoryDecodedState := & oidc . UpstreamStateParamData {
AuthParams : happyDownstreamRequestParams ,
UpstreamName : activeDirectoryUpstreamName ,
UpstreamType : activeDirectoryUpstreamType ,
Nonce : happyDownstreamNonce ,
CSRFToken : happyDownstreamCSRF ,
PKCECode : happyDownstreamPKCE ,
FormatVersion : happyDownstreamStateVersion ,
}
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
happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + ` \?code=([^&]+)&scope=openid&state= ` + happyDownstreamState
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
}
tests := [ ] struct {
name string
idps * oidctestutil . UpstreamIDPListerBuilder
decodedState * oidc . UpstreamStateParamData
formParams url . Values
reqURIQuery url . Values
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
wantDownstreamRedirectURI string
wantDownstreamGrantedScopes [ ] string
wantDownstreamIDTokenSubject string
wantDownstreamIDTokenUsername string
wantDownstreamIDTokenGroups [ ] string
wantDownstreamRequestedScopes [ ] string
wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string
wantDownstreamNonce string
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 ,
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 ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : expectedHappyActiveDirectoryUpstreamCustomSession ,
} ,
{
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "redirect_uri" ] = [ ] string { "http://127.0.0.1:4242/callback" }
data . AuthParams = query . Encode ( )
} ) ,
formParams : happyUsernamePasswordFormParams ,
wantStatus : http . StatusSeeOther ,
wantContentType : htmlContentType ,
wantBodyString : "" ,
wantRedirectLocationRegexp : "http://127.0.0.1:4242/callback" + ` \?code=([^&]+)&scope=openid&state= ` + happyDownstreamState ,
wantDownstreamIDTokenSubject : upstreamLDAPURL + "&sub=" + happyLDAPUID ,
wantDownstreamIDTokenUsername : happyLDAPUsernameFromAuthenticator ,
wantDownstreamIDTokenGroups : happyLDAPGroups ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamRedirectURI : "http://127.0.0.1:4242/callback" ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "scope" ] = [ ] string { "openid offline_access pinniped:request-audience" }
data . AuthParams = query . 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 ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "scope" ] = [ ] string { "email" }
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
query [ "prompt" ] = [ ] string { "none login" }
data . AuthParams = query . Encode ( )
} ) ,
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 ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "redirect_uri" ] = [ ] string { "http://127.0.0.1/wrong_callback" }
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "client_id" ] = [ ] string { "wrong_client_id" }
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
delete ( query , "client_id" )
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "response_type" ] = [ ] string { "unsupported" }
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
delete ( query , "response_type" )
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
delete ( query , "code_challenge" )
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "code_challenge_method" ] = [ ] string { "this-is-not-a-valid-pkce-alg" }
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "code_challenge_method" ] = [ ] string { "plain" } // plain is not allowed
data . AuthParams = query . 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" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithLDAP ( & upstreamLDAPIdentityProvider ) ,
decodedState : modifyHappyLDAPDecodedState ( func ( data * oidc . UpstreamStateParamData ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
delete ( query , "code_challenge_method" )
data . AuthParams = query . 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 : "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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "prompt" ] = [ ] string { "none login" }
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "state" ] = [ ] string { "short" }
data . AuthParams = query . Encode ( )
} ) ,
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 ) {
query := copyOfHappyDownstreamRequestParamsQuery ( )
query [ "scope" ] = [ ] string { "openid offline_access pinniped:request-audience scope_not_allowed" }
data . AuthParams = query . Encode ( )
} ) ,
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 ( )
secretsClient := kubeClient . CoreV1 ( ) . Secrets ( "some-namespace" )
// 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 ( )
kubeOauthStore := oidc . NewKubeStorage ( secretsClient , timeoutsConfiguration )
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 )
require . Empty ( t , kubeClient . Actions ( ) )
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 )
2022-05-03 23:46:09 +00:00
require . Equal ( t , tt . wantBodyString , rsp . Body . String ( ) )
2022-04-29 23:01:51 +00:00
actualLocation := rsp . Header ( ) . Get ( "Location" )
switch {
case tt . wantRedirectLocationRegexp != "" :
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-04-29 23:01:51 +00:00
downstreamClientID ,
2022-05-03 23:46:09 +00:00
tt . wantDownstreamRedirectURI ,
tt . wantDownstreamCustomSessionData ,
2022-04-29 23:01:51 +00:00
)
case tt . wantRedirectToLoginPageError != "" :
expectedLocation := downstreamIssuer + oidc . PinnipedLoginPath +
"?err=" + tt . wantRedirectToLoginPageError + "&state=" + happyEncodedUpstreamState
require . Equal ( t , expectedLocation , actualLocation )
2022-05-03 23:46:09 +00:00
require . Len ( t , kubeClient . Actions ( ) , tt . wantUnnecessaryStoredRecords )
2022-04-29 23:01:51 +00:00
case tt . wantRedirectLocationString != "" :
require . Equal ( t , tt . wantRedirectLocationString , actualLocation )
2022-05-03 23:46:09 +00:00
require . Len ( t , kubeClient . Actions ( ) , tt . wantUnnecessaryStoredRecords )
2022-04-29 23:01:51 +00:00
default :
require . Failf ( t , "test should have expected a redirect" ,
"actual location was %q" , actualLocation )
}
} )
}
}