2020-11-04 00:17:38 +00:00
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package auth
import (
"fmt"
2020-11-04 20:19:07 +00:00
"html"
2020-11-04 00:17:38 +00:00
"net/http"
"net/http/httptest"
"net/url"
2020-11-12 23:36:59 +00:00
"regexp"
2020-11-04 00:17:38 +00:00
"strings"
"testing"
2020-11-11 01:58:00 +00:00
"github.com/gorilla/securecookie"
2020-11-04 00:17:38 +00:00
"github.com/stretchr/testify/require"
2020-11-04 15:15:19 +00:00
"go.pinniped.dev/internal/here"
2020-11-04 23:04:50 +00:00
"go.pinniped.dev/internal/oidc"
2020-11-11 01:58:00 +00:00
"go.pinniped.dev/internal/oidc/csrftoken"
2020-12-04 15:06:55 +00:00
"go.pinniped.dev/internal/oidc/jwks"
2020-11-20 01:57:07 +00:00
"go.pinniped.dev/internal/oidc/oidctestutil"
2020-11-04 00:17:38 +00:00
"go.pinniped.dev/internal/oidc/provider"
2020-12-04 15:06:55 +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"
2020-11-04 00:17:38 +00:00
)
func TestAuthorizationEndpoint ( t * testing . T ) {
2020-11-04 15:15:19 +00:00
const (
2020-11-19 16:35:23 +00:00
downstreamIssuer = "https://my-downstream-issuer.com/some-path"
2020-11-11 20:29:14 +00:00
downstreamRedirectURI = "http://127.0.0.1/callback"
downstreamRedirectURIWithDifferentPort = "http://127.0.0.1:42/callback"
2020-12-12 01:39:58 +00:00
happyState = "8b-state"
2020-11-04 15:15:19 +00:00
)
2020-12-12 01:39:58 +00:00
require . Len ( t , happyState , 8 , "we expect fosite to allow 8 byte state params, so we want to test that boundary case" )
2020-11-04 15:15:19 +00:00
var (
fositeInvalidClientErrorBody = here . Doc ( `
2020-11-04 16:29:33 +00:00
{
"error" : "invalid_client" ,
2020-12-17 20:09:19 +00:00
"error_description" : "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The requested OAuth 2.0 Client does not exist."
2020-11-04 16:29:33 +00:00
}
2020-11-04 16:43:45 +00:00
` )
2020-11-04 15:30:53 +00:00
fositeInvalidRedirectURIErrorBody = here . Doc ( `
2020-11-04 16:29:33 +00:00
{
"error" : "invalid_request" ,
2020-12-17 20:09:19 +00:00
"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 'redirect_uri' parameter does not match any of the OAuth 2.0 Client's pre-registered redirect urls."
2020-11-04 16:29:33 +00:00
}
2020-11-04 16:43:45 +00:00
` )
2020-11-04 16:12:26 +00:00
2020-11-06 22:44:58 +00:00
fositePromptHasNoneAndOtherValueErrorQuery = map [ string ] string {
"error" : "invalid_request" ,
2020-12-17 20:09:19 +00:00
"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." ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-06 22:44:58 +00:00
}
2020-11-04 20:19:07 +00:00
fositeMissingCodeChallengeErrorQuery = map [ string ] string {
"error" : "invalid_request" ,
2020-12-17 20:09:19 +00:00
"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." ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-04 20:19:07 +00:00
}
fositeMissingCodeChallengeMethodErrorQuery = map [ string ] string {
"error" : "invalid_request" ,
2020-12-17 20:09:19 +00:00
"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." ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-04 20:19:07 +00:00
}
2020-11-04 20:29:43 +00:00
fositeInvalidCodeChallengeErrorQuery = map [ string ] string {
"error" : "invalid_request" ,
2020-12-17 20:09:19 +00:00
"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." ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-04 20:29:43 +00:00
}
2020-11-04 17:58:40 +00:00
fositeUnsupportedResponseTypeErrorQuery = map [ string ] string {
"error" : "unsupported_response_type" ,
2020-12-17 20:09:19 +00:00
"error_description" : "The authorization server does not support obtaining a token using this method. The client is not allowed to request response_type 'unsupported'." ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-04 17:58:40 +00:00
}
fositeInvalidScopeErrorQuery = map [ string ] string {
"error" : "invalid_scope" ,
2020-12-17 20:09:19 +00:00
"error_description" : "The requested scope is invalid, unknown, or malformed. The OAuth 2.0 Client is not allowed to request scope 'tuna'." ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-04 17:58:40 +00:00
}
fositeInvalidStateErrorQuery = map [ string ] string {
"error" : "invalid_state" ,
2020-12-17 20:09:19 +00:00
"error_description" : "The state is missing or does not have enough characters and is therefore considered too weak. Request parameter 'state' must be at least be 8 characters long to ensure sufficient entropy." ,
2020-11-04 17:58:40 +00:00
"state" : "short" ,
}
fositeMissingResponseTypeErrorQuery = map [ string ] string {
"error" : "unsupported_response_type" ,
2020-12-17 20:09:19 +00:00
"error_description" : "The authorization server does not support obtaining a token using this method. `The request is missing the 'response_type' parameter." ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-04 17:58:40 +00:00
}
2020-11-04 15:15:19 +00:00
)
2020-11-04 00:17:38 +00:00
upstreamAuthURL , err := url . Parse ( "https://some-upstream-idp:8443/auth" )
require . NoError ( t , err )
2020-11-20 01:57:07 +00:00
upstreamOIDCIdentityProvider := oidctestutil . TestUpstreamOIDCIdentityProvider {
2020-11-04 00:17:38 +00:00
Name : "some-idp" ,
ClientID : "some-client-id" ,
AuthorizationURL : * upstreamAuthURL ,
2020-12-08 01:22:34 +00:00
Scopes : [ ] string { "scope1" , "scope2" } , // the scopes to request when starting the upstream authorization flow
2020-11-04 00:17:38 +00:00
}
2020-11-18 21:38:13 +00:00
// Configure fosite the same way that the production code would, using NullStorage to turn off storage.
2020-11-11 22:49:24 +00:00
oauthStore := oidc . NullStorage { }
2020-12-11 16:01:07 +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" )
2020-12-04 15:06:55 +00:00
jwksProviderIsUnused := jwks . NewDynamicJWKSProvider ( )
2020-12-11 16:01:07 +00:00
oauthHelper := oidc . FositeOauth2Helper ( oauthStore , downstreamIssuer , hmacSecretFunc , jwksProviderIsUnused , oidc . DefaultOIDCTimeoutsConfiguration ( ) )
2020-11-04 15:15:19 +00:00
2020-11-11 01:58:00 +00:00
happyCSRF := "test-csrf"
happyPKCE := "test-pkce"
2020-12-12 01:39:58 +00:00
happyNonce := "test-nonce"
2020-11-11 01:58:00 +00:00
happyCSRFGenerator := func ( ) ( csrftoken . CSRFToken , error ) { return csrftoken . CSRFToken ( happyCSRF ) , nil }
happyPKCEGenerator := func ( ) ( pkce . Code , error ) { return pkce . Code ( happyPKCE ) , nil }
happyNonceGenerator := func ( ) ( nonce . Nonce , error ) { return nonce . Nonce ( happyNonce ) , nil }
2020-11-04 00:17:38 +00:00
// This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example:
// $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1
2020-11-04 20:19:07 +00:00
expectedUpstreamCodeChallenge := "VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"
2020-11-04 00:17:38 +00:00
2020-11-12 23:36:59 +00:00
var stateEncoderHashKey = [ ] byte ( "fake-hash-secret" )
var stateEncoderBlockKey = [ ] byte ( "0123456789ABCDEF" ) // block encryption requires 16/24/32 bytes for AES
var cookieEncoderHashKey = [ ] byte ( "fake-hash-secret2" )
var cookieEncoderBlockKey = [ ] byte ( "0123456789ABCDE2" ) // block encryption requires 16/24/32 bytes for AES
require . NotEqual ( t , stateEncoderHashKey , cookieEncoderHashKey )
require . NotEqual ( t , stateEncoderBlockKey , cookieEncoderBlockKey )
var happyStateEncoder = securecookie . New ( stateEncoderHashKey , stateEncoderBlockKey )
happyStateEncoder . SetSerializer ( securecookie . JSONEncoder { } )
var happyCookieEncoder = securecookie . New ( cookieEncoderHashKey , cookieEncoderBlockKey )
happyCookieEncoder . SetSerializer ( securecookie . JSONEncoder { } )
2020-11-11 01:58:00 +00:00
encodeQuery := func ( query map [ string ] string ) string {
2020-11-04 17:58:40 +00:00
values := url . Values { }
for k , v := range query {
values [ k ] = [ ] string { v }
}
2020-11-11 01:58:00 +00:00
return values . Encode ( )
}
pathWithQuery := func ( path string , query map [ string ] string ) string {
pathToReturn := fmt . Sprintf ( "%s?%s" , path , encodeQuery ( query ) )
2020-11-04 17:58:40 +00:00
require . NotRegexp ( t , "^http" , pathToReturn , "pathWithQuery helper was used to create a URL" )
return pathToReturn
}
urlWithQuery := func ( baseURL string , query map [ string ] string ) string {
2020-11-11 01:58:00 +00:00
urlToReturn := fmt . Sprintf ( "%s?%s" , baseURL , encodeQuery ( query ) )
2020-11-04 17:58:40 +00:00
_ , err := url . Parse ( urlToReturn )
require . NoError ( t , err , "urlWithQuery helper was used to create an illegal URL" )
return urlToReturn
}
2020-11-11 21:13:57 +00:00
happyGetRequestQueryMap := map [ string ] string {
"response_type" : "code" ,
"scope" : "openid profile email" ,
"client_id" : "pinniped-cli" ,
2020-12-12 01:39:58 +00:00
"state" : happyState ,
2020-11-11 21:13:57 +00:00
"nonce" : "some-nonce-value" ,
"code_challenge" : "some-challenge" ,
"code_challenge_method" : "S256" ,
"redirect_uri" : downstreamRedirectURI ,
2020-11-04 17:58:40 +00:00
}
2020-11-11 21:13:57 +00:00
happyGetRequestPath := pathWithQuery ( "/some/path" , happyGetRequestQueryMap )
2020-11-04 17:58:40 +00:00
2020-11-11 21:13:57 +00:00
modifiedHappyGetRequestQueryMap := func ( queryOverrides map [ string ] string ) map [ string ] string {
2020-11-04 17:58:40 +00:00
copyOfHappyGetRequestQueryMap := map [ string ] string { }
2020-11-11 21:13:57 +00:00
for k , v := range happyGetRequestQueryMap {
2020-11-04 17:58:40 +00:00
copyOfHappyGetRequestQueryMap [ k ] = v
}
for k , v := range queryOverrides {
_ , hasKey := copyOfHappyGetRequestQueryMap [ k ]
if v == "" && hasKey {
delete ( copyOfHappyGetRequestQueryMap , k )
} else {
copyOfHappyGetRequestQueryMap [ k ] = v
}
}
2020-11-11 21:13:57 +00:00
return copyOfHappyGetRequestQueryMap
}
modifiedHappyGetRequestPath := func ( queryOverrides map [ string ] string ) string {
return pathWithQuery ( "/some/path" , modifiedHappyGetRequestQueryMap ( queryOverrides ) )
2020-11-04 17:58:40 +00:00
}
2020-11-20 21:14:45 +00:00
expectedUpstreamStateParam := func ( queryOverrides map [ string ] string , csrfValueOverride , upstreamNameOverride string ) string {
2020-11-12 23:36:59 +00:00
csrf := happyCSRF
if csrfValueOverride != "" {
csrf = csrfValueOverride
}
2020-11-20 21:14:45 +00:00
upstreamName := upstreamOIDCIdentityProvider . Name
if upstreamNameOverride != "" {
upstreamName = upstreamNameOverride
}
2020-11-12 23:36:59 +00:00
encoded , err := happyStateEncoder . Encode ( "s" ,
2020-11-20 01:57:07 +00:00
oidctestutil . ExpectedUpstreamStateParamFormat {
2020-11-11 21:13:57 +00:00
P : encodeQuery ( modifiedHappyGetRequestQueryMap ( queryOverrides ) ) ,
2020-11-20 21:14:45 +00:00
U : upstreamName ,
2020-11-11 20:29:14 +00:00
N : happyNonce ,
2020-11-12 23:36:59 +00:00
C : csrf ,
2020-11-11 20:29:14 +00:00
K : happyPKCE ,
V : "1" ,
} ,
)
require . NoError ( t , err )
return encoded
}
2020-11-11 01:58:00 +00:00
2020-12-12 01:13:27 +00:00
expectedRedirectLocation := func ( expectedUpstreamState string , expectedPrompt string ) string {
query := map [ string ] string {
2020-11-04 17:58:40 +00:00
"response_type" : "code" ,
"access_type" : "offline" ,
"scope" : "scope1 scope2" ,
"client_id" : "some-client-id" ,
2020-11-11 21:13:57 +00:00
"state" : expectedUpstreamState ,
2020-11-11 01:58:00 +00:00
"nonce" : happyNonce ,
2020-11-04 20:19:07 +00:00
"code_challenge" : expectedUpstreamCodeChallenge ,
2020-11-04 17:58:40 +00:00
"code_challenge_method" : "S256" ,
2020-11-20 21:14:45 +00:00
"redirect_uri" : downstreamIssuer + "/callback" ,
2020-12-12 01:13:27 +00:00
}
if expectedPrompt != "" {
query [ "prompt" ] = expectedPrompt
}
return urlWithQuery ( upstreamAuthURL . String ( ) , query )
2020-11-11 20:29:14 +00:00
}
2020-11-04 00:17:38 +00:00
2020-11-12 23:36:59 +00:00
incomingCookieCSRFValue := "csrf-value-from-cookie"
encodedIncomingCookieCSRFValue , err := happyCookieEncoder . Encode ( "csrf" , incomingCookieCSRFValue )
require . NoError ( t , err )
2020-11-11 01:58:00 +00:00
2020-11-04 00:17:38 +00:00
type testCase struct {
name string
issuer string
idpListGetter provider . DynamicUpstreamIDPProvider
2020-11-11 01:58:00 +00:00
generateCSRF func ( ) ( csrftoken . CSRFToken , error )
2020-11-04 00:17:38 +00:00
generatePKCE func ( ) ( pkce . Code , error )
generateNonce func ( ) ( nonce . Nonce , error )
2020-11-16 19:41:00 +00:00
stateEncoder oidc . Codec
cookieEncoder oidc . Codec
2020-11-04 00:17:38 +00:00
method string
path string
2020-11-04 15:15:19 +00:00
contentType string
2020-11-04 00:17:38 +00:00
body string
2020-11-12 23:36:59 +00:00
csrfCookie string
2020-11-04 00:17:38 +00:00
2020-11-12 23:36:59 +00:00
wantStatus int
wantContentType string
wantBodyString string
wantBodyJSON string
wantLocationHeader string
wantCSRFValueInCookieHeader string
2020-11-04 00:17:38 +00:00
2020-11-11 20:29:14 +00:00
wantUpstreamStateParamInLocationHeader bool
2020-11-12 17:13:58 +00:00
wantBodyStringWithLocationInHref bool
2020-11-11 20:29:14 +00:00
}
2020-11-04 00:17:38 +00:00
tests := [ ] testCase {
{
2020-11-12 23:36:59 +00:00
name : "happy path using GET without a CSRF cookie" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-12 17:13:58 +00:00
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-12 17:13:58 +00:00
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusFound ,
wantContentType : "text/html; charset=utf-8" ,
2020-11-12 23:36:59 +00:00
wantCSRFValueInCookieHeader : happyCSRF ,
2020-12-12 01:13:27 +00:00
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam ( nil , "" , "" ) , "" ) ,
2020-11-12 23:36:59 +00:00
wantUpstreamStateParamInLocationHeader : true ,
wantBodyStringWithLocationInHref : true ,
} ,
{
name : "happy path using GET with a CSRF cookie" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-12 23:36:59 +00:00
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
method : http . MethodGet ,
path : happyGetRequestPath ,
2020-12-01 23:01:22 +00:00
csrfCookie : "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " " ,
2020-11-12 23:36:59 +00:00
wantStatus : http . StatusFound ,
wantContentType : "text/html; charset=utf-8" ,
2020-12-12 01:13:27 +00:00
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam ( nil , incomingCookieCSRFValue , "" ) , "" ) ,
2020-11-11 20:29:14 +00:00
wantUpstreamStateParamInLocationHeader : true ,
2020-11-12 17:13:58 +00:00
wantBodyStringWithLocationInHref : true ,
2020-11-11 20:29:14 +00:00
} ,
{
name : "happy path using POST" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 20:29:14 +00:00
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-11 20:29:14 +00:00
method : http . MethodPost ,
path : "/some/path" ,
contentType : "application/x-www-form-urlencoded" ,
2020-11-11 21:13:57 +00:00
body : encodeQuery ( happyGetRequestQueryMap ) ,
2020-11-11 20:29:14 +00:00
wantStatus : http . StatusFound ,
wantContentType : "" ,
wantBodyString : "" ,
2020-11-12 23:36:59 +00:00
wantCSRFValueInCookieHeader : happyCSRF ,
2020-12-12 01:13:27 +00:00
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam ( nil , "" , "" ) , "" ) ,
wantUpstreamStateParamInLocationHeader : true ,
} ,
{
name : "happy path with prompt param login passed through to redirect uri" ,
issuer : downstreamIssuer ,
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "prompt" : "login" } ) ,
contentType : "application/x-www-form-urlencoded" ,
body : encodeQuery ( happyGetRequestQueryMap ) ,
wantStatus : http . StatusFound ,
wantContentType : "text/html; charset=utf-8" ,
wantBodyStringWithLocationInHref : true ,
wantCSRFValueInCookieHeader : happyCSRF ,
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam ( map [ string ] string { "prompt" : "login" } , "" , "" ) , "login" ) ,
2020-11-11 20:29:14 +00:00
wantUpstreamStateParamInLocationHeader : true ,
2020-11-04 00:17:38 +00:00
} ,
2020-11-20 21:56:35 +00:00
{
name : "error while decoding CSRF cookie just generates a new cookie and succeeds as usual" ,
issuer : downstreamIssuer ,
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
method : http . MethodGet ,
path : happyGetRequestPath ,
csrfCookie : "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped" ,
wantStatus : http . StatusFound ,
wantContentType : "text/html; charset=utf-8" ,
// Generated a new CSRF cookie and set it in the response.
wantCSRFValueInCookieHeader : happyCSRF ,
2020-12-12 01:13:27 +00:00
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam ( nil , "" , "" ) , "" ) ,
2020-11-20 21:56:35 +00:00
wantUpstreamStateParamInLocationHeader : true ,
wantBodyStringWithLocationInHref : true ,
} ,
2020-11-04 00:17:38 +00:00
{
2020-11-11 20:29:14 +00:00
name : "happy path when downstream redirect uri matches what is configured for client except for the port number" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 00:17:38 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-11 20:29:14 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string {
"redirect_uri" : downstreamRedirectURIWithDifferentPort , // not the same port number that is registered for the client
} ) ,
2020-11-12 23:36:59 +00:00
wantStatus : http . StatusFound ,
wantContentType : "text/html; charset=utf-8" ,
wantCSRFValueInCookieHeader : happyCSRF ,
2020-11-11 21:13:57 +00:00
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam ( map [ string ] string {
"redirect_uri" : downstreamRedirectURIWithDifferentPort , // not the same port number that is registered for the client
2020-12-12 01:13:27 +00:00
} , "" , "" ) , "" ) ,
2020-11-11 20:29:14 +00:00
wantUpstreamStateParamInLocationHeader : true ,
2020-11-12 17:13:58 +00:00
wantBodyStringWithLocationInHref : true ,
2020-11-04 15:30:53 +00:00
} ,
2020-12-08 01:22:34 +00:00
{
name : "happy path when downstream requested scopes include offline_access" ,
issuer : downstreamIssuer ,
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "scope" : "openid offline_access" } ) ,
wantStatus : http . StatusFound ,
wantContentType : "text/html; charset=utf-8" ,
wantCSRFValueInCookieHeader : happyCSRF ,
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam ( map [ string ] string {
"scope" : "openid offline_access" ,
2020-12-12 01:13:27 +00:00
} , "" , "" ) , "" ) ,
2020-12-08 01:22:34 +00:00
wantUpstreamStateParamInLocationHeader : true ,
wantBodyStringWithLocationInHref : true ,
} ,
2020-11-04 15:30:53 +00:00
{
name : "downstream redirect uri does not match what is configured for client" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 15:30:53 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 15:30:53 +00:00
method : http . MethodGet ,
2020-11-04 17:58:40 +00:00
path : modifiedHappyGetRequestPath ( map [ string ] string {
"redirect_uri" : "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client" ,
} ) ,
2020-11-04 15:30:53 +00:00
wantStatus : http . StatusBadRequest ,
wantContentType : "application/json; charset=utf-8" ,
wantBodyJSON : fositeInvalidRedirectURIErrorBody ,
2020-11-04 15:15:19 +00:00
} ,
2020-11-04 23:04:50 +00:00
{
2020-11-11 20:29:14 +00:00
name : "downstream client does not exist" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 20:29:14 +00:00
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-11 20:29:14 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "client_id" : "invalid-client" } ) ,
wantStatus : http . StatusUnauthorized ,
wantContentType : "application/json; charset=utf-8" ,
wantBodyJSON : fositeInvalidClientErrorBody ,
2020-11-04 23:04:50 +00:00
} ,
2020-11-04 16:12:26 +00:00
{
2020-11-04 17:58:40 +00:00
name : "response type is unsupported" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 17:58:40 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 17:58:40 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "response_type" : "unsupported" } ) ,
2020-11-04 16:12:26 +00:00
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
2020-11-04 17:58:40 +00:00
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeUnsupportedResponseTypeErrorQuery ) ,
2020-11-04 20:19:07 +00:00
wantBodyString : "" ,
2020-11-04 16:12:26 +00:00
} ,
{
2020-11-04 17:58:40 +00:00
name : "downstream scopes do not match what is configured for client" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 17:58:40 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 17:58:40 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "scope" : "openid profile email tuna" } ) ,
2020-11-04 16:12:26 +00:00
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
2020-11-04 17:58:40 +00:00
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeInvalidScopeErrorQuery ) ,
2020-11-04 20:19:07 +00:00
wantBodyString : "" ,
2020-11-04 16:12:26 +00:00
} ,
{
2020-11-04 17:58:40 +00:00
name : "missing response type in request" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 17:58:40 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 17:58:40 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "response_type" : "" } ) ,
2020-11-04 16:12:26 +00:00
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
2020-11-04 17:58:40 +00:00
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeMissingResponseTypeErrorQuery ) ,
2020-11-04 20:19:07 +00:00
wantBodyString : "" ,
2020-11-04 16:12:26 +00:00
} ,
{
2020-11-04 17:58:40 +00:00
name : "missing client id in request" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 17:58:40 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 17:58:40 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "client_id" : "" } ) ,
2020-11-04 16:12:26 +00:00
wantStatus : http . StatusUnauthorized ,
wantContentType : "application/json; charset=utf-8" ,
wantBodyJSON : fositeInvalidClientErrorBody ,
} ,
2020-11-04 20:19:07 +00:00
{
name : "missing PKCE code_challenge in request" , // See https://tools.ietf.org/html/rfc7636#section-4.4.1
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 20:19:07 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 20:19:07 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "code_challenge" : "" } ) ,
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeMissingCodeChallengeErrorQuery ) ,
wantBodyString : "" ,
} ,
2020-11-04 20:29:43 +00:00
{
name : "invalid value for PKCE code_challenge_method in request" , // https://tools.ietf.org/html/rfc7636#section-4.3
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 20:29:43 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 20:29:43 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "code_challenge_method" : "this-is-not-a-valid-pkce-alg" } ) ,
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeInvalidCodeChallengeErrorQuery ) ,
wantBodyString : "" ,
} ,
{
name : "when PKCE code_challenge_method in request is `plain`" , // https://tools.ietf.org/html/rfc7636#section-4.3
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 20:29:43 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 20:29:43 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "code_challenge_method" : "plain" } ) ,
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeMissingCodeChallengeMethodErrorQuery ) ,
wantBodyString : "" ,
} ,
2020-11-04 20:19:07 +00:00
{
name : "missing PKCE code_challenge_method in request" , // See https://tools.ietf.org/html/rfc7636#section-4.4.1
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 20:19:07 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 20:19:07 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "code_challenge_method" : "" } ) ,
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeMissingCodeChallengeMethodErrorQuery ) ,
wantBodyString : "" ,
} ,
2020-11-06 22:44:58 +00:00
{
// This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running
// through that part of the fosite library.
name : "prompt param is not allowed to have none and another legal value at the same time" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-06 22:44:58 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-06 22:44:58 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "prompt" : "none login" } ) ,
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositePromptHasNoneAndOtherValueErrorQuery ) ,
wantBodyString : "" ,
} ,
2020-11-11 21:13:57 +00:00
{
name : "OIDC validations are skipped when the openid scope was not requested" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 21:13:57 +00:00
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-11 21:13:57 +00:00
method : http . MethodGet ,
// The following prompt value is illegal when openid is requested, but note that openid is not requested.
2020-11-12 23:36:59 +00:00
path : modifiedHappyGetRequestPath ( map [ string ] string { "prompt" : "none login" , "scope" : "email" } ) ,
wantStatus : http . StatusFound ,
wantContentType : "text/html; charset=utf-8" ,
wantCSRFValueInCookieHeader : happyCSRF ,
wantLocationHeader : expectedRedirectLocation ( expectedUpstreamStateParam (
2020-11-20 21:14:45 +00:00
map [ string ] string { "prompt" : "none login" , "scope" : "email" } , "" , "" ,
2020-12-12 01:13:27 +00:00
) , "" ) ,
2020-11-11 21:13:57 +00:00
wantUpstreamStateParamInLocationHeader : true ,
2020-11-12 17:13:58 +00:00
wantBodyStringWithLocationInHref : true ,
2020-11-11 21:13:57 +00:00
} ,
2020-11-04 16:12:26 +00:00
{
2020-11-04 17:58:40 +00:00
name : "state does not have enough entropy" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 17:58:40 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 17:58:40 +00:00
method : http . MethodGet ,
path : modifiedHappyGetRequestPath ( map [ string ] string { "state" : "short" } ) ,
2020-11-04 16:12:26 +00:00
wantStatus : http . StatusFound ,
wantContentType : "application/json; charset=utf-8" ,
2020-11-04 17:58:40 +00:00
wantLocationHeader : urlWithQuery ( downstreamRedirectURI , fositeInvalidStateErrorQuery ) ,
2020-11-04 20:19:07 +00:00
wantBodyString : "" ,
2020-11-04 16:12:26 +00:00
} ,
2020-11-11 20:29:14 +00:00
{
name : "error while encoding upstream state param" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 20:29:14 +00:00
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : & errorReturningEncoder { } ,
cookieEncoder : happyCookieEncoder ,
2020-11-11 20:29:14 +00:00
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusInternalServerError ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Internal Server Error: error encoding upstream state param\n" ,
} ,
2020-11-12 23:36:59 +00:00
{
name : "error while encoding CSRF cookie value for new cookie" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-12 23:36:59 +00:00
generateCSRF : happyCSRFGenerator ,
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
stateEncoder : happyStateEncoder ,
cookieEncoder : & errorReturningEncoder { } ,
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusInternalServerError ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Internal Server Error: error encoding CSRF cookie\n" ,
} ,
2020-11-04 00:17:38 +00:00
{
2020-11-11 01:58:00 +00:00
name : "error while generating CSRF token" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : func ( ) ( csrftoken . CSRFToken , error ) { return "" , fmt . Errorf ( "some csrf generator error" ) } ,
2020-11-04 00:17:38 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 00:17:38 +00:00
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusInternalServerError ,
wantContentType : "text/plain; charset=utf-8" ,
2020-11-11 01:58:00 +00:00
wantBodyString : "Internal Server Error: error generating CSRF token\n" ,
2020-11-04 00:17:38 +00:00
} ,
{
name : "error while generating nonce" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 00:17:38 +00:00
generatePKCE : happyPKCEGenerator ,
generateNonce : func ( ) ( nonce . Nonce , error ) { return "" , fmt . Errorf ( "some nonce generator error" ) } ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 00:17:38 +00:00
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusInternalServerError ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Internal Server Error: error generating nonce param\n" ,
} ,
{
name : "error while generating PKCE" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-11 01:58:00 +00:00
generateCSRF : happyCSRFGenerator ,
2020-11-04 00:17:38 +00:00
generatePKCE : func ( ) ( pkce . Code , error ) { return "" , fmt . Errorf ( "some PKCE generator error" ) } ,
generateNonce : happyNonceGenerator ,
2020-11-12 23:36:59 +00:00
stateEncoder : happyStateEncoder ,
cookieEncoder : happyCookieEncoder ,
2020-11-04 00:17:38 +00:00
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusInternalServerError ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Internal Server Error: error generating PKCE param\n" ,
} ,
{
name : "no upstream providers are configured" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( ) , // empty
2020-11-04 00:17:38 +00:00
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Unprocessable Entity: No upstream providers are configured\n" ,
} ,
{
name : "too many upstream providers are configured" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider , & upstreamOIDCIdentityProvider ) , // more than one not allowed
2020-11-04 00:17:38 +00:00
method : http . MethodGet ,
path : happyGetRequestPath ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n" ,
} ,
{
name : "PUT is a bad method" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-04 00:17:38 +00:00
method : http . MethodPut ,
path : "/some/path" ,
wantStatus : http . StatusMethodNotAllowed ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Method Not Allowed: PUT (try GET or POST)\n" ,
} ,
{
name : "PATCH is a bad method" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-04 00:17:38 +00:00
method : http . MethodPatch ,
path : "/some/path" ,
wantStatus : http . StatusMethodNotAllowed ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Method Not Allowed: PATCH (try GET or POST)\n" ,
} ,
{
name : "DELETE is a bad method" ,
2020-11-19 16:35:23 +00:00
issuer : downstreamIssuer ,
2020-11-20 01:57:07 +00:00
idpListGetter : oidctestutil . NewIDPListGetter ( & upstreamOIDCIdentityProvider ) ,
2020-11-04 00:17:38 +00:00
method : http . MethodDelete ,
path : "/some/path" ,
wantStatus : http . StatusMethodNotAllowed ,
wantContentType : "text/plain; charset=utf-8" ,
wantBodyString : "Method Not Allowed: DELETE (try GET or POST)\n" ,
} ,
}
runOneTestCase := func ( t * testing . T , test testCase , subject http . Handler ) {
req := httptest . NewRequest ( test . method , test . path , strings . NewReader ( test . body ) )
2020-11-04 15:15:19 +00:00
req . Header . Set ( "Content-Type" , test . contentType )
2020-11-12 23:36:59 +00:00
if test . csrfCookie != "" {
req . Header . Set ( "Cookie" , test . csrfCookie )
}
2020-11-04 00:17:38 +00:00
rsp := httptest . NewRecorder ( )
subject . ServeHTTP ( rsp , req )
2020-11-19 14:28:56 +00:00
t . Logf ( "response: %#v" , rsp )
t . Logf ( "response body: %q" , rsp . Body . String ( ) )
2020-11-04 00:17:38 +00:00
require . Equal ( t , test . wantStatus , rsp . Code )
2020-12-04 15:06:55 +00:00
testutil . RequireEqualContentType ( t , rsp . Header ( ) . Get ( "Content-Type" ) , test . wantContentType )
2020-12-14 23:28:32 +00:00
testutil . RequireSecurityHeaders ( t , rsp )
2020-11-04 00:17:38 +00:00
2020-11-12 17:13:58 +00:00
actualLocation := rsp . Header ( ) . Get ( "Location" )
2020-11-04 00:17:38 +00:00
if test . wantLocationHeader != "" {
2020-11-11 20:29:14 +00:00
if test . wantUpstreamStateParamInLocationHeader {
2020-11-12 23:36:59 +00:00
requireEqualDecodedStateParams ( t , actualLocation , test . wantLocationHeader , test . stateEncoder )
2020-11-11 20:29:14 +00:00
}
2020-11-12 17:13:58 +00:00
// The upstream state param is encoded using a timestamp at the beginning so we don't want to
// compare those states since they may be different, but we do want to compare the downstream
// state param that should be exactly the same.
requireEqualURLs ( t , actualLocation , test . wantLocationHeader , test . wantUpstreamStateParamInLocationHeader )
2020-11-11 01:58:00 +00:00
} else {
require . Empty ( t , rsp . Header ( ) . Values ( "Location" ) )
}
2020-11-12 17:24:40 +00:00
switch {
case test . wantBodyJSON != "" :
2020-11-11 20:29:14 +00:00
require . JSONEq ( t , test . wantBodyJSON , rsp . Body . String ( ) )
2020-11-12 17:24:40 +00:00
case test . wantBodyStringWithLocationInHref :
2020-11-12 17:13:58 +00:00
anchorTagWithLocationHref := fmt . Sprintf ( "<a href=\"%s\">Found</a>.\n\n" , html . EscapeString ( actualLocation ) )
require . Equal ( t , anchorTagWithLocationHref , rsp . Body . String ( ) )
2020-11-12 17:24:40 +00:00
default :
2020-11-11 20:29:14 +00:00
require . Equal ( t , test . wantBodyString , rsp . Body . String ( ) )
}
2020-11-12 23:36:59 +00:00
if test . wantCSRFValueInCookieHeader != "" {
2020-11-11 01:58:00 +00:00
require . Len ( t , rsp . Header ( ) . Values ( "Set-Cookie" ) , 1 )
actualCookie := rsp . Header ( ) . Get ( "Set-Cookie" )
2020-12-04 03:23:58 +00:00
regex := regexp . MustCompile ( "__Host-pinniped-csrf=([^;]+); Path=/; HttpOnly; Secure; SameSite=Lax" )
2020-11-12 23:36:59 +00:00
submatches := regex . FindStringSubmatch ( actualCookie )
require . Len ( t , submatches , 2 )
captured := submatches [ 1 ]
var decodedCSRFCookieValue string
err := test . cookieEncoder . Decode ( "csrf" , captured , & decodedCSRFCookieValue )
require . NoError ( t , err )
require . Equal ( t , test . wantCSRFValueInCookieHeader , decodedCSRFCookieValue )
2020-11-11 01:58:00 +00:00
} else {
require . Empty ( t , rsp . Header ( ) . Values ( "Set-Cookie" ) )
2020-11-04 00:17:38 +00:00
}
}
for _ , test := range tests {
test := test
t . Run ( test . name , func ( t * testing . T ) {
2020-11-12 23:36:59 +00:00
subject := NewHandler ( test . issuer , test . idpListGetter , oauthHelper , test . generateCSRF , test . generatePKCE , test . generateNonce , test . stateEncoder , test . cookieEncoder )
2020-11-04 00:17:38 +00:00
runOneTestCase ( t , test , subject )
} )
}
t . Run ( "allows upstream provider configuration to change between requests" , func ( t * testing . T ) {
test := tests [ 0 ]
2020-11-12 23:36:59 +00:00
require . Equal ( t , "happy path using GET without a CSRF cookie" , test . name ) // re-use the happy path test case
2020-11-04 00:17:38 +00:00
2020-11-12 23:36:59 +00:00
subject := NewHandler ( test . issuer , test . idpListGetter , oauthHelper , test . generateCSRF , test . generatePKCE , test . generateNonce , test . stateEncoder , test . cookieEncoder )
2020-11-04 00:17:38 +00:00
runOneTestCase ( t , test , subject )
// Call the setter to change the upstream IDP settings.
2020-11-20 01:57:07 +00:00
newProviderSettings := oidctestutil . TestUpstreamOIDCIdentityProvider {
2020-11-04 00:17:38 +00:00
Name : "some-other-idp" ,
ClientID : "some-other-client-id" ,
AuthorizationURL : * upstreamAuthURL ,
Scopes : [ ] string { "other-scope1" , "other-scope2" } ,
}
2020-11-18 21:38:13 +00:00
test . idpListGetter . SetIDPList ( [ ] provider . UpstreamOIDCIdentityProviderI { provider . UpstreamOIDCIdentityProviderI ( & newProviderSettings ) } )
2020-11-04 00:17:38 +00:00
// Update the expectations of the test case to match the new upstream IDP settings.
2020-11-04 17:58:40 +00:00
test . wantLocationHeader = urlWithQuery ( upstreamAuthURL . String ( ) ,
map [ string ] string {
"response_type" : "code" ,
"access_type" : "offline" ,
"scope" : "other-scope1 other-scope2" ,
"client_id" : "some-other-client-id" ,
2020-11-20 21:14:45 +00:00
"state" : expectedUpstreamStateParam ( nil , "" , newProviderSettings . Name ) ,
2020-11-11 01:58:00 +00:00
"nonce" : happyNonce ,
2020-11-04 20:19:07 +00:00
"code_challenge" : expectedUpstreamCodeChallenge ,
2020-11-04 17:58:40 +00:00
"code_challenge_method" : "S256" ,
2020-11-20 21:14:45 +00:00
"redirect_uri" : downstreamIssuer + "/callback" ,
2020-11-04 17:58:40 +00:00
} ,
2020-11-04 00:17:38 +00:00
)
2020-11-04 20:19:07 +00:00
test . wantBodyString = fmt . Sprintf ( ` <a href="%s">Found</a>.%s ` ,
html . EscapeString ( test . wantLocationHeader ) ,
"\n\n" ,
)
2020-11-04 00:17:38 +00:00
// Run again on the same instance of the subject with the modified upstream IDP settings and the
// modified expectations. This should ensure that the implementation is using the in-memory cache
// of upstream IDP settings appropriately in terms of always getting the values from the cache
// on every request.
runOneTestCase ( t , test , subject )
} )
}
2020-11-11 20:29:14 +00:00
type errorReturningEncoder struct {
2020-11-16 19:41:00 +00:00
oidc . Codec
2020-11-11 20:29:14 +00:00
}
func ( * errorReturningEncoder ) Encode ( _ string , _ interface { } ) ( string , error ) {
return "" , fmt . Errorf ( "some encoding error" )
}
2020-11-16 19:41:00 +00:00
func requireEqualDecodedStateParams ( t * testing . T , actualURL string , expectedURL string , stateParamDecoder oidc . Codec ) {
2020-11-11 20:29:14 +00:00
t . Helper ( )
actualLocationURL , err := url . Parse ( actualURL )
require . NoError ( t , err )
expectedLocationURL , err := url . Parse ( expectedURL )
require . NoError ( t , err )
expectedQueryStateParam := expectedLocationURL . Query ( ) . Get ( "state" )
require . NotEmpty ( t , expectedQueryStateParam )
2020-11-20 01:57:07 +00:00
var expectedDecodedStateParam oidctestutil . ExpectedUpstreamStateParamFormat
2020-11-11 20:29:14 +00:00
err = stateParamDecoder . Decode ( "s" , expectedQueryStateParam , & expectedDecodedStateParam )
require . NoError ( t , err )
actualQueryStateParam := actualLocationURL . Query ( ) . Get ( "state" )
require . NotEmpty ( t , actualQueryStateParam )
2020-11-20 01:57:07 +00:00
var actualDecodedStateParam oidctestutil . ExpectedUpstreamStateParamFormat
2020-11-11 20:29:14 +00:00
err = stateParamDecoder . Decode ( "s" , actualQueryStateParam , & actualDecodedStateParam )
require . NoError ( t , err )
require . Equal ( t , expectedDecodedStateParam , actualDecodedStateParam )
}
2020-11-12 17:13:58 +00:00
func requireEqualURLs ( t * testing . T , actualURL string , expectedURL string , ignoreState bool ) {
2020-11-04 15:15:19 +00:00
t . Helper ( )
2020-11-04 00:17:38 +00:00
actualLocationURL , err := url . Parse ( actualURL )
require . NoError ( t , err )
expectedLocationURL , err := url . Parse ( expectedURL )
require . NoError ( t , err )
2020-11-20 21:56:35 +00:00
require . Equal ( t , expectedLocationURL . Scheme , actualLocationURL . Scheme ,
"schemes were not equal: expected %s but got %s" , expectedURL , actualURL ,
)
require . Equal ( t , expectedLocationURL . User , actualLocationURL . User ,
"users were not equal: expected %s but got %s" , expectedURL , actualURL ,
)
require . Equal ( t , expectedLocationURL . Host , actualLocationURL . Host ,
"hosts were not equal: expected %s but got %s" , expectedURL , actualURL ,
)
require . Equal ( t , expectedLocationURL . Path , actualLocationURL . Path ,
"paths were not equal: expected %s but got %s" , expectedURL , actualURL ,
)
2020-11-12 17:13:58 +00:00
expectedLocationQuery := expectedLocationURL . Query ( )
actualLocationQuery := actualLocationURL . Query ( )
// Let the caller ignore the state, since it may contain a digest at the end that is difficult to
// predict because it depends on a time.Now() timestamp.
if ignoreState {
expectedLocationQuery . Del ( "state" )
actualLocationQuery . Del ( "state" )
}
require . Equal ( t , expectedLocationQuery , actualLocationQuery )
2020-11-04 00:17:38 +00:00
}