2021-01-14 22:21:41 +00:00
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
2020-11-13 17:31:39 +00:00
// SPDX-License-Identifier: Apache-2.0
package callback
import (
2020-11-19 01:15:01 +00:00
"context"
2020-11-19 14:00:41 +00:00
"errors"
2020-11-13 17:31:39 +00:00
"net/http"
"net/http/httptest"
2020-11-13 23:59:51 +00:00
"net/url"
2020-11-19 01:15:01 +00:00
"strings"
2020-11-13 17:31:39 +00:00
"testing"
2020-11-13 23:59:51 +00:00
"github.com/gorilla/securecookie"
2020-11-13 17:31:39 +00:00
"github.com/stretchr/testify/require"
2020-12-01 19:01:23 +00:00
"k8s.io/client-go/kubernetes/fake"
2020-11-13 23:59:51 +00:00
2020-11-18 21:38:13 +00:00
"go.pinniped.dev/internal/oidc"
2020-12-04 15:06:55 +00:00
"go.pinniped.dev/internal/oidc/jwks"
2020-11-13 23:59:51 +00:00
"go.pinniped.dev/internal/testutil"
2021-04-09 00:28:01 +00:00
"go.pinniped.dev/internal/testutil/oidctestutil"
2020-11-20 23:13:25 +00:00
"go.pinniped.dev/pkg/oidcclient/nonce"
2020-11-30 23:02:03 +00:00
"go.pinniped.dev/pkg/oidcclient/oidctypes"
2020-12-04 23:40:17 +00:00
oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce"
2020-11-13 23:59:51 +00:00
)
const (
happyUpstreamIDPName = "upstream-idp-name"
2020-11-19 19:19:01 +00:00
2021-05-27 16:25:48 +00:00
upstreamIssuer = "https://my-upstream-issuer.com"
upstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
queryEscapedUpstreamSubject = "abc123-some+guid"
upstreamUsername = "test-pinniped-username"
2020-11-19 19:19:01 +00:00
upstreamUsernameClaim = "the-user-claim"
upstreamGroupsClaim = "the-groups-claim"
2020-11-20 01:57:07 +00:00
2021-04-09 00:28:01 +00:00
happyUpstreamAuthcode = "upstream-auth-code"
2020-12-02 16:36:07 +00:00
happyUpstreamRedirectURI = "https://example.com/callback"
2020-12-12 01:39:58 +00:00
happyDownstreamState = "8b-state"
2020-11-20 14:41:49 +00:00
happyDownstreamCSRF = "test-csrf"
happyDownstreamPKCE = "test-pkce"
happyDownstreamNonce = "test-nonce"
happyDownstreamStateVersion = "1"
downstreamIssuer = "https://my-downstream-issuer.com/path"
downstreamRedirectURI = "http://127.0.0.1/callback"
downstreamClientID = "pinniped-cli"
downstreamNonce = "some-nonce-value"
downstreamPKCEChallenge = "some-challenge"
downstreamPKCEChallengeMethod = "S256"
2020-11-20 01:57:07 +00:00
2021-04-09 00:28:01 +00:00
htmlContentType = "text/html; charset=utf-8"
2020-11-19 19:19:01 +00:00
)
var (
2020-11-20 01:57:07 +00:00
upstreamGroupMembership = [ ] string { "test-pinniped-group-0" , "test-pinniped-group-1" }
2020-12-08 01:22:34 +00:00
happyDownstreamScopesRequested = [ ] string { "openid" }
happyDownstreamScopesGranted = [ ] string { "openid" }
2020-11-20 01:57:07 +00:00
2020-11-20 14:41:49 +00:00
happyDownstreamRequestParamsQuery = url . Values {
2020-11-20 01:57:07 +00:00
"response_type" : [ ] string { "code" } ,
"scope" : [ ] string { strings . Join ( happyDownstreamScopesRequested , " " ) } ,
"client_id" : [ ] string { downstreamClientID } ,
"state" : [ ] string { happyDownstreamState } ,
2020-11-20 13:38:23 +00:00
"nonce" : [ ] string { downstreamNonce } ,
2020-11-20 14:41:49 +00:00
"code_challenge" : [ ] string { downstreamPKCEChallenge } ,
"code_challenge_method" : [ ] string { downstreamPKCEChallengeMethod } ,
2020-11-20 01:57:07 +00:00
"redirect_uri" : [ ] string { downstreamRedirectURI } ,
}
2020-11-20 14:41:49 +00:00
happyDownstreamRequestParams = happyDownstreamRequestParamsQuery . Encode ( )
2020-11-13 17:31:39 +00:00
)
func TestCallbackEndpoint ( t * testing . T ) {
2020-12-12 01:39:58 +00:00
require . Len ( t , happyDownstreamState , 8 , "we expect fosite to allow 8 byte state params, so we want to test that boundary case" )
2020-11-20 01:57:07 +00:00
otherUpstreamOIDCIdentityProvider := oidctestutil . TestUpstreamOIDCIdentityProvider {
2020-11-18 21:38:13 +00:00
Name : "other-upstream-idp-name" ,
ClientID : "other-some-client-id" ,
Scopes : [ ] string { "other-scope1" , "other-scope2" } ,
2020-11-13 23:59:51 +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 )
2020-11-16 19:41:00 +00:00
var happyStateCodec = securecookie . New ( stateEncoderHashKey , stateEncoderBlockKey )
happyStateCodec . SetSerializer ( securecookie . JSONEncoder { } )
var happyCookieCodec = securecookie . New ( cookieEncoderHashKey , cookieEncoderBlockKey )
happyCookieCodec . SetSerializer ( securecookie . JSONEncoder { } )
2020-11-20 01:57:07 +00:00
happyState := happyUpstreamStateParam ( ) . Build ( t , happyStateCodec )
2020-11-19 13:51:23 +00:00
2020-11-20 14:41:49 +00:00
encodedIncomingCookieCSRFValue , err := happyCookieCodec . Encode ( "csrf" , happyDownstreamCSRF )
2020-11-16 16:47:49 +00:00
require . NoError ( t , err )
happyCSRFCookie := "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue
2020-11-13 23:59:51 +00:00
2020-11-20 01:57:07 +00:00
happyExchangeAndValidateTokensArgs := & oidctestutil . ExchangeAuthcodeAndValidateTokenArgs {
2020-11-19 15:20:46 +00:00
Authcode : happyUpstreamAuthcode ,
2020-12-04 23:40:17 +00:00
PKCECodeVerifier : oidcpkce . Code ( happyDownstreamPKCE ) ,
2020-11-20 14:41:49 +00:00
ExpectedIDTokenNonce : nonce . Nonce ( happyDownstreamNonce ) ,
2020-12-02 16:36:07 +00:00
RedirectURI : happyUpstreamRedirectURI ,
2020-11-19 15:20:46 +00:00
}
2020-11-19 16:08:21 +00:00
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
2020-11-20 14:41:49 +00:00
happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + ` \?code=([^&]+)&scope=openid&state= ` + happyDownstreamState
2020-11-19 16:08:21 +00:00
2020-11-13 17:31:39 +00:00
tests := [ ] struct {
name string
2020-11-20 01:57:07 +00:00
idp oidctestutil . TestUpstreamOIDCIdentityProvider
2020-11-19 15:20:46 +00:00
method string
path string
csrfCookie string
2020-11-13 17:31:39 +00:00
2020-11-20 14:41:49 +00:00
wantStatus int
2021-04-09 00:28:01 +00:00
wantContentType string
2020-11-20 14:41:49 +00:00
wantBody string
wantRedirectLocationRegexp string
2021-06-16 18:11:07 +00:00
wantBodyFormResponseRegexp string
2020-12-08 01:22:34 +00:00
wantDownstreamGrantedScopes [ ] string
2020-11-20 14:41:49 +00:00
wantDownstreamIDTokenSubject string
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenUsername string
2021-01-14 22:21:41 +00:00
wantDownstreamIDTokenGroups [ ] string
2020-11-20 14:41:49 +00:00
wantDownstreamRequestedScopes [ ] string
wantDownstreamNonce string
wantDownstreamPKCEChallenge string
wantDownstreamPKCEChallengeMethod string
2020-11-19 15:20:46 +00:00
2020-11-20 01:57:07 +00:00
wantExchangeAndValidateTokensCall * oidctestutil . ExchangeAuthcodeAndValidateTokenArgs
2020-11-13 17:31:39 +00:00
} {
2021-06-16 18:11:07 +00:00
{
name : "GET with good state and cookie and successful upstream token exchange with response_mode=form_post returns 200 with HTML+JS form" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState (
happyUpstreamStateParam ( ) . WithAuthorizeRequestParams (
shallowCopyAndModifyQuery (
happyDownstreamRequestParamsQuery ,
map [ string ] string { "response_mode" : "form_post" } ,
) . Encode ( ) ,
) . Build ( t , happyStateCodec ) ,
) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusOK ,
wantContentType : "text/html;charset=UTF-8" ,
2021-06-30 16:50:01 +00:00
wantBodyFormResponseRegexp : ` <code id="manual-auth-code">(.+)</code> ` ,
2021-06-16 18:11:07 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
wantDownstreamIDTokenUsername : upstreamUsername ,
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-16 22:07:34 +00:00
{
2020-11-19 16:08:21 +00:00
name : "GET with good state and cookie and successful upstream token exchange returns 302 to downstream client callback with its state and code" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . Build ( ) ,
2020-11-19 16:08:21 +00:00
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 16:08:21 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
2020-11-20 14:41:49 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
2020-11-19 16:08:21 +00:00
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenUsername : upstreamUsername ,
2020-11-19 19:19:01 +00:00
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
2020-11-20 01:57:07 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
2020-12-08 01:22:34 +00:00
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
2020-11-20 13:38:23 +00:00
wantDownstreamNonce : downstreamNonce ,
2020-11-20 14:41:49 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2020-11-19 16:08:21 +00:00
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
2020-11-20 01:57:07 +00:00
name : "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . WithoutUsernameClaim ( ) . WithoutGroupsClaim ( ) . Build ( ) ,
2020-11-19 16:08:21 +00:00
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
2020-11-20 14:41:49 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
2020-11-20 01:57:07 +00:00
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
wantDownstreamIDTokenUsername : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2021-01-14 22:21:41 +00:00
wantDownstreamIDTokenGroups : [ ] string { } ,
2020-11-20 01:57:07 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
2020-12-08 01:22:34 +00:00
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
2020-11-20 13:38:23 +00:00
wantDownstreamNonce : downstreamNonce ,
2020-11-20 14:41:49 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2020-11-20 01:57:07 +00:00
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-25 17:53:52 +00:00
{
name : "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is missing" ,
idp : happyUpstream ( ) . WithUsernameClaim ( "email" ) .
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2021-01-25 17:53:52 +00:00
wantDownstreamIDTokenUsername : "joe@whitehouse.gov" ,
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with true value" ,
idp : happyUpstream ( ) . WithUsernameClaim ( "email" ) .
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) .
WithIDTokenClaim ( "email_verified" , true ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2021-01-25 17:53:52 +00:00
wantDownstreamIDTokenUsername : "joe@whitehouse.gov" ,
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value" ,
idp : happyUpstream ( ) . WithUsernameClaim ( "some-claim" ) .
WithIDTokenClaim ( "some-claim" , "joe" ) .
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) .
WithIDTokenClaim ( "email_verified" , false ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound , // succeed despite `email_verified=false` because we're not using the email claim for anything
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2021-01-25 17:53:52 +00:00
wantDownstreamIDTokenUsername : "joe" ,
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with illegal value" ,
idp : happyUpstream ( ) . WithUsernameClaim ( "email" ) .
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) .
WithIDTokenClaim ( "email_verified" , "supposed to be boolean" ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2021-01-25 17:53:52 +00:00
wantBody : "Unprocessable Entity: email_verified claim in upstream ID token has invalid format\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream IDP configures username claim as special claim `email` and `email_verified` upstream claim is present with false value" ,
idp : happyUpstream ( ) . WithUsernameClaim ( "email" ) .
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) .
WithIDTokenClaim ( "email_verified" , false ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2021-01-25 17:53:52 +00:00
wantBody : "Unprocessable Entity: email_verified claim in upstream ID token has false value\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-20 01:57:07 +00:00
{
name : "upstream IDP provides username claim configuration as `sub`, so the downstream token subject should be exactly what they asked for" ,
idp : happyUpstream ( ) . WithUsernameClaim ( "sub" ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 16:08:21 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
2020-11-20 14:41:49 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
2020-11-19 15:20:46 +00:00
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenUsername : upstreamSubject ,
2020-11-20 01:57:07 +00:00
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
2020-12-08 01:22:34 +00:00
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
2020-11-20 13:38:23 +00:00
wantDownstreamNonce : downstreamNonce ,
2020-11-20 14:41:49 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2020-11-19 15:20:46 +00:00
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
2020-11-16 22:07:34 +00:00
} ,
2020-12-15 16:34:24 +00:00
{
name : "upstream IDP's configured groups claim in the ID token has a non-array value" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( upstreamGroupsClaim , "notAnArrayGroup1 notAnArrayGroup2" ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2020-12-15 16:34:24 +00:00
wantDownstreamIDTokenUsername : upstreamUsername ,
2021-01-14 22:21:41 +00:00
wantDownstreamIDTokenGroups : [ ] string { "notAnArrayGroup1 notAnArrayGroup2" } ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream IDP's configured groups claim in the ID token is a slice of interfaces" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( upstreamGroupsClaim , [ ] interface { } { "group1" , "group2" } ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2021-01-14 22:21:41 +00:00
wantDownstreamIDTokenUsername : upstreamUsername ,
wantDownstreamIDTokenGroups : [ ] string { "group1" , "group2" } ,
2020-12-15 16:34:24 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-13 17:31:39 +00:00
// Pre-upstream-exchange verification
{
2021-04-09 00:28:01 +00:00
name : "PUT method is invalid" ,
method : http . MethodPut ,
path : newRequestPath ( ) . String ( ) ,
wantStatus : http . StatusMethodNotAllowed ,
wantContentType : htmlContentType ,
wantBody : "Method Not Allowed: PUT (try GET)\n" ,
2020-11-13 17:31:39 +00:00
} ,
2020-11-13 23:59:51 +00:00
{
2021-04-09 00:28:01 +00:00
name : "POST method is invalid" ,
method : http . MethodPost ,
path : newRequestPath ( ) . String ( ) ,
wantStatus : http . StatusMethodNotAllowed ,
wantContentType : htmlContentType ,
wantBody : "Method Not Allowed: POST (try GET)\n" ,
2020-11-13 23:59:51 +00:00
} ,
{
2021-04-09 00:28:01 +00:00
name : "PATCH method is invalid" ,
method : http . MethodPatch ,
path : newRequestPath ( ) . String ( ) ,
wantStatus : http . StatusMethodNotAllowed ,
wantContentType : htmlContentType ,
wantBody : "Method Not Allowed: PATCH (try GET)\n" ,
2020-11-13 23:59:51 +00:00
} ,
{
2021-04-09 00:28:01 +00:00
name : "DELETE method is invalid" ,
method : http . MethodDelete ,
path : newRequestPath ( ) . String ( ) ,
wantStatus : http . StatusMethodNotAllowed ,
wantContentType : htmlContentType ,
wantBody : "Method Not Allowed: DELETE (try GET)\n" ,
2020-11-13 23:59:51 +00:00
} ,
{
2021-04-09 00:28:01 +00:00
name : "code param was not included on request" ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . WithoutCode ( ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
wantContentType : htmlContentType ,
wantBody : "Bad Request: code param not found\n" ,
2020-11-13 23:59:51 +00:00
} ,
{
2021-04-09 00:28:01 +00:00
name : "state param was not included on request" ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithoutState ( ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
wantContentType : htmlContentType ,
wantBody : "Bad Request: state param not found\n" ,
2020-11-13 23:59:51 +00:00
} ,
{
2021-04-09 00:28:01 +00:00
name : "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( "this-will-not-decode" ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
wantContentType : htmlContentType ,
wantBody : "Bad Request: error reading state\n" ,
2020-11-16 19:41:00 +00:00
} ,
2020-11-20 01:57:07 +00:00
{
// This shouldn't happen in practice because the authorize endpoint should have already run the same
// validations, but we would like to test the error handling in this endpoint anyway.
name : "state param contains authorization request params which fail validation" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState (
happyUpstreamStateParam ( ) .
2020-11-20 14:41:49 +00:00
WithAuthorizeRequestParams ( shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery , map [ string ] string { "prompt" : "none login" } ) . Encode ( ) ) .
2020-11-20 01:57:07 +00:00
Build ( t , happyStateCodec ) ,
) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
wantStatus : http . StatusInternalServerError ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2020-11-20 01:57:07 +00:00
wantBody : "Internal Server Error: error while generating and saving authcode\n" ,
} ,
2020-11-16 19:41:00 +00:00
{
2021-04-09 00:28:01 +00:00
name : "state's internal version does not match what we want" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyUpstreamStateParam ( ) . WithStateVersion ( "wrong-state-version" ) . Build ( t , happyStateCodec ) ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: state format version is invalid\n" ,
2020-11-13 23:59:51 +00:00
} ,
2020-11-19 13:41:44 +00:00
{
2020-11-20 01:57:07 +00:00
name : "state's downstream auth params element is invalid" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyUpstreamStateParam ( ) .
WithAuthorizeRequestParams ( "the following is an invalid url encoding token, and therefore this is an invalid param: %z" ) .
Build ( t , happyStateCodec ) ) . String ( ) ,
2021-04-09 00:28:01 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
wantContentType : htmlContentType ,
wantBody : "Bad Request: error reading state downstream auth params\n" ,
2020-11-19 13:51:23 +00:00
} ,
{
2020-11-20 01:57:07 +00:00
name : "state's downstream auth params are missing required value (e.g., client_id)" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState (
happyUpstreamStateParam ( ) .
2020-11-20 14:41:49 +00:00
WithAuthorizeRequestParams ( shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery , map [ string ] string { "client_id" : "" } ) . Encode ( ) ) .
2020-11-20 01:57:07 +00:00
Build ( t , happyStateCodec ) ,
) . String ( ) ,
2021-04-09 00:28:01 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
wantContentType : htmlContentType ,
wantBody : "Bad Request: error using state downstream auth params\n" ,
2020-11-19 13:41:44 +00:00
} ,
2020-11-19 14:28:56 +00:00
{
2020-11-20 01:57:07 +00:00
name : "state's downstream auth params does not contain openid scope" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) .
WithState (
happyUpstreamStateParam ( ) .
2020-11-20 14:41:49 +00:00
WithAuthorizeRequestParams ( shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery , map [ string ] string { "scope" : "profile email" } ) . Encode ( ) ) .
2020-11-20 01:57:07 +00:00
Build ( t , happyStateCodec ) ,
) . String ( ) ,
2020-11-19 15:20:46 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=&state= ` + happyDownstreamState ,
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenUsername : upstreamUsername ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2020-11-20 01:57:07 +00:00
wantDownstreamRequestedScopes : [ ] string { "profile" , "email" } ,
2020-11-19 19:19:01 +00:00
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
2020-11-20 13:38:23 +00:00
wantDownstreamNonce : downstreamNonce ,
2020-11-20 14:41:49 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2020-11-19 15:20:46 +00:00
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
2020-11-19 14:28:56 +00:00
} ,
2020-12-08 01:22:34 +00:00
{
name : "state's downstream auth params also included offline_access scope" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) .
WithState (
happyUpstreamStateParam ( ) .
WithAuthorizeRequestParams ( shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery , map [ string ] string { "scope" : "openid offline_access" } ) . Encode ( ) ) .
Build ( t , happyStateCodec ) ,
) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusFound ,
2020-12-17 20:09:19 +00:00
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=openid\+offline_access&state= ` + happyDownstreamState ,
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenUsername : upstreamUsername ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2020-12-08 01:22:34 +00:00
wantDownstreamRequestedScopes : [ ] string { "openid" , "offline_access" } ,
wantDownstreamGrantedScopes : [ ] string { "openid" , "offline_access" } ,
wantDownstreamIDTokenGroups : upstreamGroupMembership ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-13 23:59:51 +00:00
{
2021-04-09 00:28:01 +00:00
name : "the OIDCIdentityProvider CRD has been deleted" ,
idp : otherUpstreamOIDCIdentityProvider ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: upstream provider not found\n" ,
2020-11-13 23:59:51 +00:00
} ,
2020-11-16 16:47:49 +00:00
{
2021-04-09 00:28:01 +00:00
name : "the CSRF cookie does not exist on request" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
wantStatus : http . StatusForbidden ,
wantContentType : htmlContentType ,
wantBody : "Forbidden: CSRF cookie is missing\n" ,
2020-11-16 16:47:49 +00:00
} ,
{
2021-04-09 00:28:01 +00:00
name : "cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped" ,
wantStatus : http . StatusForbidden ,
wantContentType : htmlContentType ,
wantBody : "Forbidden: error reading CSRF cookie\n" ,
2020-11-16 19:41:00 +00:00
} ,
{
2021-04-09 00:28:01 +00:00
name : "cookie csrf value does not match state csrf value" ,
idp : happyUpstream ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyUpstreamStateParam ( ) . WithCSRF ( "wrong-csrf-value" ) . Build ( t , happyStateCodec ) ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusForbidden ,
wantContentType : htmlContentType ,
wantBody : "Forbidden: CSRF value does not match\n" ,
2020-11-16 16:47:49 +00:00
} ,
2020-11-19 14:00:41 +00:00
// Upstream exchange
{
2020-11-19 15:20:46 +00:00
name : "upstream auth code exchange fails" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . WithoutUpstreamAuthcodeExchangeError ( errors . New ( "some error" ) ) . Build ( ) ,
2020-11-19 15:20:46 +00:00
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 15:20:46 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadGateway ,
wantBody : "Bad Gateway: error exchanging and validating upstream tokens\n" ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2020-11-19 15:20:46 +00:00
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
2020-11-19 14:00:41 +00:00
} ,
2020-11-19 19:19:01 +00:00
{
name : "upstream ID token does not contain requested username claim" ,
idp : happyUpstream ( ) . WithoutIDTokenClaim ( upstreamUsernameClaim ) . Build ( ) ,
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 19:19:01 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantBody : "Unprocessable Entity: no username claim in upstream ID token\n" ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2020-11-19 19:19:01 +00:00
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 20:53:21 +00:00
{
name : "upstream ID token does not contain requested groups claim" ,
idp : happyUpstream ( ) . WithoutIDTokenClaim ( upstreamGroupsClaim ) . Build ( ) ,
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 20:53:21 +00:00
csrfCookie : happyCSRFCookie ,
2021-01-11 19:58:07 +00:00
wantStatus : http . StatusFound ,
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-05-27 16:25:48 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + queryEscapedUpstreamSubject ,
2021-01-11 19:58:07 +00:00
wantDownstreamIDTokenUsername : upstreamUsername ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
2021-01-14 22:21:41 +00:00
wantDownstreamIDTokenGroups : [ ] string { } ,
2021-01-11 19:58:07 +00:00
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2020-11-19 20:53:21 +00:00
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream ID token contains username claim with weird format" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( upstreamUsernameClaim , 42 ) . Build ( ) ,
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 20:53:21 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2020-11-19 20:53:21 +00:00
wantBody : "Unprocessable Entity: username claim in upstream ID token has invalid format\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-20 01:57:07 +00:00
{
name : "upstream ID token does not contain iss claim when using default username claim config" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( "iss" , "" ) . WithoutUsernameClaim ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2020-11-20 01:57:07 +00:00
wantBody : "Unprocessable Entity: issuer claim in upstream ID token missing\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream ID token has an non-string iss claim when using default username claim config" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( "iss" , 42 ) . WithoutUsernameClaim ( ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2020-11-20 01:57:07 +00:00
wantBody : "Unprocessable Entity: issuer claim in upstream ID token has invalid format\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 20:53:21 +00:00
{
name : "upstream ID token contains groups claim with weird format" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( upstreamGroupsClaim , 42 ) . Build ( ) ,
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 20:53:21 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2020-11-19 20:53:21 +00:00
wantBody : "Unprocessable Entity: groups claim in upstream ID token has invalid format\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-14 22:21:41 +00:00
{
name : "upstream ID token contains groups claim where one element is invalid" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( upstreamGroupsClaim , [ ] interface { } { "foo" , 7 } ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2021-01-14 22:21:41 +00:00
wantBody : "Unprocessable Entity: groups claim in upstream ID token has invalid format\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
{
name : "upstream ID token contains groups claim with invalid null type" ,
idp : happyUpstream ( ) . WithIDTokenClaim ( upstreamGroupsClaim , nil ) . Build ( ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-04-09 00:28:01 +00:00
wantContentType : htmlContentType ,
2021-01-14 22:21:41 +00:00
wantBody : "Unprocessable Entity: groups claim in upstream ID token has invalid format\n" ,
wantExchangeAndValidateTokensCall : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-13 17:31:39 +00:00
}
for _ , test := range tests {
test := test
2020-11-20 01:57:07 +00:00
2020-11-13 17:31:39 +00:00
t . Run ( test . name , func ( t * testing . T ) {
2020-12-01 19:01:23 +00:00
client := fake . NewSimpleClientset ( )
secrets := client . CoreV1 ( ) . Secrets ( "some-namespace" )
// Configure fosite the same way that the production code would.
2020-11-19 14:28:56 +00:00
// Inject this into our test subject at the last second so we get a fresh storage for every test.
2020-12-10 20:15:40 +00:00
timeoutsConfiguration := oidc . DefaultOIDCTimeoutsConfiguration ( )
oauthStore := oidc . NewKubeStorage ( secrets , timeoutsConfiguration )
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-14 16:44:01 +00:00
oauthHelper := oidc . FositeOauth2Helper ( oauthStore , downstreamIssuer , hmacSecretFunc , jwksProviderIsUnused , timeoutsConfiguration )
2020-11-19 14:28:56 +00:00
2021-04-07 23:12:13 +00:00
idpLister := oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( & test . idp ) . Build ( )
subject := NewHandler ( idpLister , oauthHelper , happyStateCodec , happyCookieCodec , happyUpstreamRedirectURI )
2020-11-13 23:59:51 +00:00
req := httptest . NewRequest ( test . method , test . path , nil )
2020-11-16 16:47:49 +00:00
if test . csrfCookie != "" {
req . Header . Set ( "Cookie" , test . csrfCookie )
}
2020-11-13 17:31:39 +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-13 17:31:39 +00:00
2020-12-14 23:28:32 +00:00
testutil . RequireSecurityHeaders ( t , rsp )
2020-11-19 16:35:23 +00:00
if test . wantExchangeAndValidateTokensCall != nil {
require . Equal ( t , 1 , test . idp . ExchangeAuthcodeAndValidateTokensCallCount ( ) )
test . wantExchangeAndValidateTokensCall . Ctx = req . Context ( )
require . Equal ( t , test . wantExchangeAndValidateTokensCall , test . idp . ExchangeAuthcodeAndValidateTokensArgs ( 0 ) )
} else {
require . Equal ( t , 0 , test . idp . ExchangeAuthcodeAndValidateTokensCallCount ( ) )
}
2020-11-16 22:07:34 +00:00
2020-11-19 16:35:23 +00:00
require . Equal ( t , test . wantStatus , rsp . Code )
2021-04-09 00:28:01 +00:00
testutil . RequireEqualContentType ( t , rsp . Header ( ) . Get ( "Content-Type" ) , test . wantContentType )
2020-11-19 01:15:01 +00:00
2021-06-16 18:11:07 +00:00
switch {
// If we want a specific static response body, assert that.
case test . wantBody != "" :
2020-11-16 22:07:34 +00:00
require . Equal ( t , test . wantBody , rsp . Body . String ( ) )
2021-06-16 18:11:07 +00:00
// Else if we want a body that contains a regex-matched auth code, assert that (for "response_mode=form_post").
case test . wantBodyFormResponseRegexp != "" :
oidctestutil . RequireAuthCodeRegexpMatch (
t ,
rsp . Body . String ( ) ,
test . wantBodyFormResponseRegexp ,
client ,
secrets ,
oauthStore ,
test . wantDownstreamGrantedScopes ,
test . wantDownstreamIDTokenSubject ,
test . wantDownstreamIDTokenUsername ,
test . wantDownstreamIDTokenGroups ,
test . wantDownstreamRequestedScopes ,
test . wantDownstreamPKCEChallenge ,
test . wantDownstreamPKCEChallengeMethod ,
test . wantDownstreamNonce ,
downstreamClientID ,
downstreamRedirectURI ,
)
// Otherwise, expect an empty response body.
default :
2020-11-19 01:15:01 +00:00
require . Empty ( t , rsp . Body . String ( ) )
}
2020-11-20 01:57:07 +00:00
if test . wantRedirectLocationRegexp != "" { //nolint:nestif // don't mind have several sequential if statements in this test
2020-11-16 22:07:34 +00:00
require . Len ( t , rsp . Header ( ) . Values ( "Location" ) , 1 )
2021-06-16 18:11:07 +00:00
oidctestutil . RequireAuthCodeRegexpMatch (
2020-11-20 14:41:49 +00:00
t ,
2021-04-09 00:28:01 +00:00
rsp . Header ( ) . Get ( "Location" ) ,
test . wantRedirectLocationRegexp ,
client ,
secrets ,
2020-11-20 14:41:49 +00:00
oauthStore ,
2020-12-08 01:22:34 +00:00
test . wantDownstreamGrantedScopes ,
2020-11-20 14:41:49 +00:00
test . wantDownstreamIDTokenSubject ,
2020-12-15 01:05:53 +00:00
test . wantDownstreamIDTokenUsername ,
2020-11-20 14:41:49 +00:00
test . wantDownstreamIDTokenGroups ,
test . wantDownstreamRequestedScopes ,
test . wantDownstreamPKCEChallenge ,
test . wantDownstreamPKCEChallengeMethod ,
2021-04-09 00:28:01 +00:00
test . wantDownstreamNonce ,
downstreamClientID ,
downstreamRedirectURI ,
2020-11-20 14:41:49 +00:00
)
2020-11-16 22:07:34 +00:00
}
2020-11-13 17:31:39 +00:00
} )
}
}
2020-11-13 23:59:51 +00:00
type requestPath struct {
2020-11-20 21:33:08 +00:00
code , state * string
2020-11-13 23:59:51 +00:00
}
func newRequestPath ( ) * requestPath {
2020-11-20 01:57:07 +00:00
c := happyUpstreamAuthcode
2020-11-13 23:59:51 +00:00
s := "4321"
return & requestPath {
2020-11-20 21:33:08 +00:00
code : & c ,
state : & s ,
2020-11-13 23:59:51 +00:00
}
}
func ( r * requestPath ) WithCode ( code string ) * requestPath {
r . code = & code
return r
}
func ( r * requestPath ) WithoutCode ( ) * requestPath {
r . code = nil
return r
}
func ( r * requestPath ) WithState ( state string ) * requestPath {
r . state = & state
return r
}
func ( r * requestPath ) WithoutState ( ) * requestPath {
r . state = nil
return r
}
func ( r * requestPath ) String ( ) string {
2020-11-20 21:33:08 +00:00
path := "/downstream-provider-name/callback?"
2020-11-13 23:59:51 +00:00
params := url . Values { }
if r . code != nil {
params . Add ( "code" , * r . code )
}
if r . state != nil {
params . Add ( "state" , * r . state )
}
return path + params . Encode ( )
}
2020-11-19 13:51:23 +00:00
2020-11-20 01:57:07 +00:00
type upstreamStateParamBuilder oidctestutil . ExpectedUpstreamStateParamFormat
func happyUpstreamStateParam ( ) * upstreamStateParamBuilder {
return & upstreamStateParamBuilder {
2020-11-20 21:33:08 +00:00
U : happyUpstreamIDPName ,
2020-11-20 14:41:49 +00:00
P : happyDownstreamRequestParams ,
N : happyDownstreamNonce ,
C : happyDownstreamCSRF ,
K : happyDownstreamPKCE ,
V : happyDownstreamStateVersion ,
2020-11-20 01:57:07 +00:00
}
}
func ( b upstreamStateParamBuilder ) Build ( t * testing . T , stateEncoder * securecookie . SecureCookie ) string {
state , err := stateEncoder . Encode ( "s" , b )
require . NoError ( t , err )
return state
}
func ( b * upstreamStateParamBuilder ) WithAuthorizeRequestParams ( params string ) * upstreamStateParamBuilder {
b . P = params
return b
}
func ( b * upstreamStateParamBuilder ) WithNonce ( nonce string ) * upstreamStateParamBuilder {
b . N = nonce
return b
}
func ( b * upstreamStateParamBuilder ) WithCSRF ( csrf string ) * upstreamStateParamBuilder {
b . C = csrf
return b
}
func ( b * upstreamStateParamBuilder ) WithPKCVE ( pkce string ) * upstreamStateParamBuilder {
b . K = pkce
return b
}
func ( b * upstreamStateParamBuilder ) WithStateVersion ( version string ) * upstreamStateParamBuilder {
b . V = version
return b
}
2020-11-19 19:19:01 +00:00
type upstreamOIDCIdentityProviderBuilder struct {
idToken map [ string ] interface { }
usernameClaim , groupsClaim string
authcodeExchangeErr error
}
func happyUpstream ( ) * upstreamOIDCIdentityProviderBuilder {
return & upstreamOIDCIdentityProviderBuilder {
usernameClaim : upstreamUsernameClaim ,
groupsClaim : upstreamGroupsClaim ,
idToken : map [ string ] interface { } {
2020-11-20 01:57:07 +00:00
"iss" : upstreamIssuer ,
2020-11-19 19:19:01 +00:00
"sub" : upstreamSubject ,
upstreamUsernameClaim : upstreamUsername ,
upstreamGroupsClaim : upstreamGroupMembership ,
"other-claim" : "should be ignored" ,
} ,
}
}
2020-12-15 16:34:24 +00:00
func ( u * upstreamOIDCIdentityProviderBuilder ) WithUsernameClaim ( value string ) * upstreamOIDCIdentityProviderBuilder {
u . usernameClaim = value
2020-11-20 01:57:07 +00:00
return u
}
2020-11-19 19:19:01 +00:00
func ( u * upstreamOIDCIdentityProviderBuilder ) WithoutUsernameClaim ( ) * upstreamOIDCIdentityProviderBuilder {
u . usernameClaim = ""
return u
}
func ( u * upstreamOIDCIdentityProviderBuilder ) WithoutGroupsClaim ( ) * upstreamOIDCIdentityProviderBuilder {
u . groupsClaim = ""
return u
}
2020-11-19 20:53:21 +00:00
func ( u * upstreamOIDCIdentityProviderBuilder ) WithIDTokenClaim ( name string , value interface { } ) * upstreamOIDCIdentityProviderBuilder {
2020-11-19 19:19:01 +00:00
u . idToken [ name ] = value
return u
}
func ( u * upstreamOIDCIdentityProviderBuilder ) WithoutIDTokenClaim ( claim string ) * upstreamOIDCIdentityProviderBuilder {
delete ( u . idToken , claim )
return u
}
func ( u * upstreamOIDCIdentityProviderBuilder ) WithoutUpstreamAuthcodeExchangeError ( err error ) * upstreamOIDCIdentityProviderBuilder {
u . authcodeExchangeErr = err
return u
}
2020-11-20 01:57:07 +00:00
func ( u * upstreamOIDCIdentityProviderBuilder ) Build ( ) oidctestutil . TestUpstreamOIDCIdentityProvider {
return oidctestutil . TestUpstreamOIDCIdentityProvider {
2020-11-19 19:19:01 +00:00
Name : happyUpstreamIDPName ,
ClientID : "some-client-id" ,
UsernameClaim : u . usernameClaim ,
GroupsClaim : u . groupsClaim ,
Scopes : [ ] string { "scope1" , "scope2" } ,
2020-12-04 23:52:49 +00:00
ExchangeAuthcodeAndValidateTokensFunc : func ( ctx context . Context , authcode string , pkceCodeVerifier oidcpkce . Code , expectedIDTokenNonce nonce . Nonce ) ( * oidctypes . Token , error ) {
2020-12-04 21:33:36 +00:00
if u . authcodeExchangeErr != nil {
return nil , u . authcodeExchangeErr
}
return & oidctypes . Token { IDToken : & oidctypes . IDToken { Claims : u . idToken } } , nil
2020-11-19 19:19:01 +00:00
} ,
}
}
2020-11-19 14:28:56 +00:00
func shallowCopyAndModifyQuery ( query url . Values , modifications map [ string ] string ) url . Values {
2020-11-19 13:51:23 +00:00
copied := url . Values { }
for key , value := range query {
2020-11-20 01:57:07 +00:00
copied [ key ] = value
}
for key , value := range modifications {
if value == "" {
copied . Del ( key )
2020-11-19 14:28:56 +00:00
} else {
2020-11-20 01:57:07 +00:00
copied [ key ] = [ ] string { value }
2020-11-19 13:51:23 +00:00
}
}
return copied
}