2022-01-07 23:04:58 +00:00
// Copyright 2020-2022 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"
2022-01-18 23:34:19 +00:00
"time"
2020-11-13 17:31:39 +00:00
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"
2022-07-20 20:55:56 +00:00
"golang.org/x/crypto/bcrypt"
2022-01-18 23:34:19 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2020-12-01 19:01:23 +00:00
"k8s.io/client-go/kubernetes/fake"
2020-11-13 23:59:51 +00:00
2022-07-14 16:51:11 +00:00
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
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"
2021-10-08 22:48:21 +00:00
"go.pinniped.dev/internal/psession"
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-12-04 23:40:17 +00:00
oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce"
2020-11-13 23:59:51 +00:00
)
const (
2021-10-08 22:48:21 +00:00
happyUpstreamIDPName = "upstream-idp-name"
happyUpstreamIDPResourceUID = "upstream-uid"
2020-11-19 19:19:01 +00:00
2021-08-13 00:53:14 +00:00
oidcUpstreamIssuer = "https://my-upstream-issuer.com"
2021-10-08 22:48:21 +00:00
oidcUpstreamRefreshToken = "test-refresh-token"
2022-01-11 19:00:54 +00:00
oidcUpstreamAccessToken = "test-access-token"
2021-08-13 00:53:14 +00:00
oidcUpstreamSubject = "abc123-some guid" // has a space character which should get escaped in URL
oidcUpstreamSubjectQueryEscaped = "abc123-some+guid"
oidcUpstreamUsername = "test-pinniped-username"
2020-11-19 19:19:01 +00:00
2021-08-13 00:53:14 +00:00
oidcUpstreamUsernameClaim = "the-user-claim"
oidcUpstreamGroupsClaim = "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"
2022-04-26 19:51:56 +00:00
happyDownstreamStateVersion = "2"
2020-11-20 14:41:49 +00:00
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 (
2021-08-13 00:53:14 +00:00
oidcUpstreamGroupMembership = [ ] string { "test-pinniped-group-0" , "test-pinniped-group-1" }
2022-06-15 15:00:17 +00:00
happyDownstreamScopesRequested = [ ] string { "openid" , "groups" }
happyDownstreamScopesGranted = [ ] string { "openid" , "groups" }
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 } ,
}
2021-10-08 22:48:21 +00:00
happyDownstreamRequestParams = happyDownstreamRequestParamsQuery . Encode ( )
happyDownstreamCustomSessionData = & psession . CustomSessionData {
ProviderUID : happyUpstreamIDPResourceUID ,
ProviderName : happyUpstreamIDPName ,
ProviderType : psession . ProviderTypeOIDC ,
2022-01-07 23:04:58 +00:00
OIDC : & psession . OIDCSessionData {
UpstreamRefreshToken : oidcUpstreamRefreshToken ,
UpstreamIssuer : oidcUpstreamIssuer ,
UpstreamSubject : oidcUpstreamSubject ,
} ,
2021-10-08 22:48:21 +00:00
}
2022-01-11 19:00:54 +00:00
happyDownstreamAccessTokenCustomSessionData = & psession . CustomSessionData {
ProviderUID : happyUpstreamIDPResourceUID ,
ProviderName : happyUpstreamIDPName ,
ProviderType : psession . ProviderTypeOIDC ,
OIDC : & psession . OIDCSessionData {
UpstreamAccessToken : oidcUpstreamAccessToken ,
UpstreamIssuer : oidcUpstreamIssuer ,
UpstreamSubject : oidcUpstreamSubject ,
} ,
}
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
2022-06-15 15:00:17 +00:00
happyDownstreamRedirectLocationRegexp := downstreamRedirectURI + ` \?code=([^&]+)&scope=openid\+groups&state= ` + happyDownstreamState
2020-11-19 16:08:21 +00:00
2020-11-13 17:31:39 +00:00
tests := [ ] struct {
name string
2022-07-20 20:55:56 +00:00
idps * oidctestutil . UpstreamIDPListerBuilder
kubeResources func ( t * testing . T , supervisorClient * supervisorfake . Clientset , kubeClient * fake . Clientset )
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
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData * psession . CustomSessionData
2020-11-19 15:20:46 +00:00
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall * expectedAuthcodeExchange
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-06-16 18:11:07 +00:00
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-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
2021-06-16 18:11:07 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2021-06-16 18:11:07 +00:00
} ,
2020-11-16 22:07:34 +00:00
{
2021-12-14 20:55:35 +00:00
name : "GET with good state and cookie and successful upstream token exchange returns 303 to downstream client callback with its state and code" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( 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 ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2020-11-20 14:41:49 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
2020-11-19 16:08:21 +00:00
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
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 ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 16:08:21 +00:00
} ,
2022-01-11 19:00:54 +00:00
{
2022-01-11 23:40:38 +00:00
name : "GET with authcode exchange that returns an access token but no refresh token when there is a userinfo endpoint returns 303 to downstream client callback with its state and code" ,
2022-01-18 23:34:19 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithEmptyRefreshToken ( ) . WithAccessToken ( oidcUpstreamAccessToken , metav1 . NewTime ( time . Now ( ) . Add ( 9 * time . Hour ) ) ) . WithUserInfoURL ( ) . Build ( ) ) ,
2022-01-11 19:00:54 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusSeeOther ,
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : happyDownstreamAccessTokenCustomSessionData ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
2022-06-15 15:00:17 +00:00
{
name : "form_post happy path with no groups scope requested" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState (
happyUpstreamStateParam ( ) . WithAuthorizeRequestParams (
shallowCopyAndModifyQuery (
happyDownstreamRequestParamsQuery ,
map [ string ] string {
"response_mode" : "form_post" ,
"scope" : "openid" ,
} ,
) . Encode ( ) ,
) . Build ( t , happyStateCodec ) ,
) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusOK ,
wantContentType : "text/html;charset=UTF-8" ,
wantBodyFormResponseRegexp : ` <code id="manual-auth-code">(.+)</code> ` ,
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamRequestedScopes : [ ] string { "openid" } ,
wantDownstreamGrantedScopes : [ ] string { "openid" } ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
2022-01-18 23:34:19 +00:00
{
name : "GET with authcode exchange that returns an access token but no refresh token but has a short token lifetime which is stored as a warning in the session" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithEmptyRefreshToken ( ) . WithAccessToken ( oidcUpstreamAccessToken , metav1 . NewTime ( time . Now ( ) . Add ( 1 * time . Hour ) ) ) . WithUserInfoURL ( ) . Build ( ) ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusSeeOther ,
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : & psession . CustomSessionData {
ProviderUID : happyUpstreamIDPResourceUID ,
ProviderName : happyUpstreamIDPName ,
ProviderType : psession . ProviderTypeOIDC ,
Warnings : [ ] string { "Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in." } ,
OIDC : & psession . OIDCSessionData {
UpstreamAccessToken : oidcUpstreamAccessToken ,
UpstreamIssuer : oidcUpstreamIssuer ,
UpstreamSubject : oidcUpstreamSubject ,
} ,
} ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
2020-11-19 16:08:21 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream IDP provides no username or group claim configuration, so we use default username claim and skip groups" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
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 ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2020-11-20 14:41:49 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
2020-11-20 01:57:07 +00:00
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
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 ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-20 01:57:07 +00:00
} ,
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithUsernameClaim ( "email" ) . WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) . Build ( ) ,
) ,
2021-01-25 17:53:52 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2021-01-25 17:53:52 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
2021-01-25 17:53:52 +00:00
wantDownstreamIDTokenUsername : "joe@whitehouse.gov" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
2021-01-25 17:53:52 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : 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 present with true value" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithUsernameClaim ( "email" ) .
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) .
WithIDTokenClaim ( "email_verified" , true ) . Build ( ) ,
) ,
2021-01-25 17:53:52 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2021-01-25 17:53:52 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
2021-01-25 17:53:52 +00:00
wantDownstreamIDTokenUsername : "joe@whitehouse.gov" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
2021-01-25 17:53:52 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-25 17:53:52 +00:00
} ,
{
name : "upstream IDP configures username claim as anything other than special claim `email` and `email_verified` upstream claim is present with false value" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithUsernameClaim ( "some-claim" ) .
WithIDTokenClaim ( "some-claim" , "joe" ) .
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) .
WithIDTokenClaim ( "email_verified" , false ) . Build ( ) ,
) ,
2021-01-25 17:53:52 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther , // succeed despite `email_verified=false` because we're not using the email claim for anything
2021-01-25 17:53:52 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
2021-01-25 17:53:52 +00:00
wantDownstreamIDTokenUsername : "joe" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
2021-01-25 17:53:52 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : 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 present with illegal value" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithUsernameClaim ( "email" ) .
2021-01-25 17:53:52 +00:00
WithIDTokenClaim ( "email" , "joe@whitehouse.gov" ) .
WithIDTokenClaim ( "email_verified" , "supposed to be boolean" ) . Build ( ) ,
2021-08-13 00:53:14 +00:00
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: email_verified claim in upstream ID token has invalid format\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-25 17:53:52 +00:00
} ,
2022-01-11 23:40:38 +00:00
{
name : "return an error when upstream IDP returned no refresh token with an access token when there is no userinfo endpoint" ,
2022-01-18 23:34:19 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithoutRefreshToken ( ) . WithAccessToken ( oidcUpstreamAccessToken , metav1 . NewTime ( time . Now ( ) . Add ( 9 * time . Hour ) ) ) . WithoutUserInfoURL ( ) . Build ( ) ) ,
2022-01-11 23:40:38 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: access token was returned by upstream provider but there was no userinfo endpoint\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
2021-10-08 22:48:21 +00:00
{
2022-01-11 19:00:54 +00:00
name : "return an error when upstream IDP returned no refresh token and no access token" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithoutRefreshToken ( ) . WithoutAccessToken ( ) . Build ( ) ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "return an error when upstream IDP returned an empty refresh token and empty access token" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithEmptyRefreshToken ( ) . WithEmptyAccessToken ( ) . Build ( ) ) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "return an error when upstream IDP returned no refresh token and empty access token" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithoutRefreshToken ( ) . WithEmptyAccessToken ( ) . Build ( ) ) ,
2021-10-08 22:48:21 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2022-01-11 19:00:54 +00:00
wantBody : "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n" ,
2021-10-08 22:48:21 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
2022-01-11 19:00:54 +00:00
name : "return an error when upstream IDP returned an empty refresh token and no access token" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . WithEmptyRefreshToken ( ) . WithoutAccessToken ( ) . Build ( ) ) ,
2021-10-08 22:48:21 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2022-01-11 19:00:54 +00:00
wantBody : "Unprocessable Entity: neither access token nor refresh token returned by upstream provider\n" ,
2021-10-08 22:48:21 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : 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 present with false value" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
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 ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: email_verified claim in upstream ID token has false value\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-25 17:53:52 +00:00
} ,
2020-11-20 01:57:07 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream IDP provides username claim configuration as `sub`, so the downstream token subject should be exactly what they asked for" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithUsernameClaim ( "sub" ) . Build ( ) ,
) ,
2020-11-20 01:57:07 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
2020-11-19 16:08:21 +00:00
csrfCookie : happyCSRFCookie ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2020-11-20 14:41:49 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
2020-11-19 15:20:46 +00:00
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamSubject ,
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
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 ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-16 22:07:34 +00:00
} ,
2020-12-15 16:34:24 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream IDP's configured groups claim in the ID token has a non-array value" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( oidcUpstreamGroupsClaim , "notAnArrayGroup1 notAnArrayGroup2" ) . Build ( ) ,
) ,
2020-12-15 16:34:24 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2020-12-15 16:34:24 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
2021-01-14 22:21:41 +00:00
wantDownstreamIDTokenGroups : [ ] string { "notAnArrayGroup1 notAnArrayGroup2" } ,
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-14 22:21:41 +00:00
} ,
{
2021-08-13 00:53:14 +00:00
name : "upstream IDP's configured groups claim in the ID token is a slice of interfaces" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( oidcUpstreamGroupsClaim , [ ] interface { } { "group1" , "group2" } ) . Build ( ) ,
) ,
2021-01-14 22:21:41 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2021-01-14 22:21:41 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
2021-01-14 22:21:41 +00:00
wantDownstreamIDTokenGroups : [ ] string { "group1" , "group2" } ,
2020-12-15 16:34:24 +00:00
wantDownstreamRequestedScopes : happyDownstreamScopesRequested ,
wantDownstreamGrantedScopes : happyDownstreamScopesGranted ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-12-15 16:34:24 +00:00
} ,
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2020-11-20 01:57:07 +00:00
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 ( ) ,
2021-08-13 00:53:14 +00:00
csrfCookie : happyCSRFCookie ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
wantStatus : http . StatusInternalServerError ,
wantContentType : htmlContentType ,
wantBody : "Internal Server Error: error while generating and saving authcode\n" ,
2020-11-20 01:57:07 +00:00
} ,
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2020-11-20 01:57:07 +00:00
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)" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2020-11-20 01:57:07 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2020-11-20 01:57:07 +00:00
method : http . MethodGet ,
2022-06-15 15:00:17 +00:00
path : newRequestPath ( ) .
WithState (
happyUpstreamStateParam ( ) .
WithAuthorizeRequestParams ( shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery , map [ string ] string { "scope" : "profile email groups" } ) . Encode ( ) ) .
Build ( t , happyStateCodec ) ,
) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusSeeOther ,
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=groups&state= ` + happyDownstreamState ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamRequestedScopes : [ ] string { "profile" , "email" , "groups" } ,
wantDownstreamGrantedScopes : [ ] string { "groups" } ,
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "state's downstream auth params does not contain openid or groups scope" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
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 ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2020-11-19 15:20:46 +00:00
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=&state= ` + happyDownstreamState ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
2020-11-20 01:57:07 +00:00
wantDownstreamRequestedScopes : [ ] string { "profile" , "email" } ,
2022-06-15 15:00:17 +00:00
wantDownstreamGrantedScopes : [ ] string { } ,
2020-11-20 13:38:23 +00:00
wantDownstreamNonce : downstreamNonce ,
2020-11-20 14:41:49 +00:00
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : 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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2020-12-08 01:22:34 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) .
WithState (
happyUpstreamStateParam ( ) .
2022-06-15 15:00:17 +00:00
WithAuthorizeRequestParams ( shallowCopyAndModifyQuery ( happyDownstreamRequestParamsQuery , map [ string ] string { "scope" : "openid offline_access groups" } ) . Encode ( ) ) .
2020-12-08 01:22:34 +00:00
Build ( t , happyStateCodec ) ,
) . String ( ) ,
csrfCookie : happyCSRFCookie ,
2021-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2022-06-15 15:00:17 +00:00
wantRedirectLocationRegexp : downstreamRedirectURI + ` \?code=([^&]+)&scope=openid\+offline_access\+groups&state= ` + happyDownstreamState ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
2022-06-15 15:00:17 +00:00
wantDownstreamRequestedScopes : [ ] string { "openid" , "offline_access" , "groups" } ,
wantDownstreamGrantedScopes : [ ] string { "openid" , "offline_access" , "groups" } ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenGroups : oidcUpstreamGroupMembership ,
2020-12-08 01:22:34 +00:00
wantDownstreamNonce : downstreamNonce ,
wantDownstreamPKCEChallenge : downstreamPKCEChallenge ,
wantDownstreamPKCEChallengeMethod : downstreamPKCEChallengeMethod ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-12-08 01:22:34 +00:00
} ,
2020-11-13 23:59:51 +00:00
{
2021-04-09 00:28:01 +00:00
name : "the OIDCIdentityProvider CRD has been deleted" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( & otherUpstreamOIDCIdentityProvider ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC ( happyUpstream ( ) . Build ( ) ) ,
2021-04-09 00:28:01 +00:00
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
{
2021-08-13 00:53:14 +00:00
name : "upstream auth code exchange fails" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithUpstreamAuthcodeExchangeError ( errors . New ( "some error" ) ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadGateway ,
wantBody : "Bad Gateway: error exchanging and validating upstream tokens\n" ,
wantContentType : htmlContentType ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 14:00:41 +00:00
} ,
2020-11-19 19:19:01 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token does not contain requested username claim" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithoutIDTokenClaim ( oidcUpstreamUsernameClaim ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
2021-08-17 20:14:09 +00:00
wantBody : "Unprocessable Entity: required claim in upstream ID token missing\n" ,
2021-08-13 00:53:14 +00:00
wantContentType : htmlContentType ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 19:19:01 +00:00
} ,
2020-11-19 20:53:21 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token does not contain requested groups claim" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithoutIDTokenClaim ( oidcUpstreamGroupsClaim ) . Build ( ) ,
) ,
2020-11-19 20:53:21 +00:00
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-12-10 22:22:36 +00:00
wantStatus : http . StatusSeeOther ,
2021-01-11 19:58:07 +00:00
wantRedirectLocationRegexp : happyDownstreamRedirectLocationRegexp ,
wantBody : "" ,
2021-08-13 00:53:14 +00:00
wantDownstreamIDTokenSubject : oidcUpstreamIssuer + "?sub=" + oidcUpstreamSubjectQueryEscaped ,
wantDownstreamIDTokenUsername : oidcUpstreamUsername ,
2021-01-11 19:58:07 +00:00
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 ,
2021-10-08 22:48:21 +00:00
wantDownstreamCustomSessionData : happyDownstreamCustomSessionData ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 20:53:21 +00:00
} ,
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token contains username claim with weird format" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( oidcUpstreamUsernameClaim , 42 ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2021-08-17 20:14:09 +00:00
wantBody : "Unprocessable Entity: required claim in upstream ID token has invalid format\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "upstream ID token contains username claim with empty string value" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( oidcUpstreamUsernameClaim , "" ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: required claim in upstream ID token is empty\n" ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 20:53:21 +00:00
} ,
2020-11-20 01:57:07 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token does not contain iss claim when using default username claim config" ,
2021-08-17 20:14:09 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithoutIDTokenClaim ( "iss" ) . WithoutUsernameClaim ( ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: required claim in upstream ID token missing\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "upstream ID token does has an empty string value for iss claim when using default username claim config" ,
2021-08-13 00:53:14 +00:00
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( "iss" , "" ) . WithoutUsernameClaim ( ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2021-08-17 20:14:09 +00:00
wantBody : "Unprocessable Entity: required claim in upstream ID token is empty\n" ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-20 01:57:07 +00:00
} ,
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token has an non-string iss claim when using default username claim config" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( "iss" , 42 ) . WithoutUsernameClaim ( ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2021-08-17 20:14:09 +00:00
wantBody : "Unprocessable Entity: required claim in upstream ID token has invalid format\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "upstream ID token does not contain sub claim when using default username claim config" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithoutIDTokenClaim ( "sub" ) . WithoutUsernameClaim ( ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: required claim in upstream ID token missing\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "upstream ID token does has an empty string value for sub claim when using default username claim config" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( "sub" , "" ) . WithoutUsernameClaim ( ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: required claim in upstream ID token is empty\n" ,
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
} ,
{
name : "upstream ID token has an non-string sub claim when using default username claim config" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( "sub" , 42 ) . WithoutUsernameClaim ( ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
wantBody : "Unprocessable Entity: required claim in upstream ID token has invalid format\n" ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-20 01:57:07 +00:00
} ,
2020-11-19 20:53:21 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token contains groups claim with weird format" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( oidcUpstreamGroupsClaim , 42 ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2021-08-18 19:06:46 +00:00
wantBody : "Unprocessable Entity: required claim in upstream ID token has invalid format\n" ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2020-11-19 20:53:21 +00:00
} ,
2021-01-14 22:21:41 +00:00
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token contains groups claim where one element is invalid" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( oidcUpstreamGroupsClaim , [ ] interface { } { "foo" , 7 } ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2021-08-18 19:06:46 +00:00
wantBody : "Unprocessable Entity: required claim in upstream ID token has invalid format\n" ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-14 22:21:41 +00:00
} ,
{
2021-08-13 00:53:14 +00:00
name : "upstream ID token contains groups claim with invalid null type" ,
idps : oidctestutil . NewUpstreamIDPListerBuilder ( ) . WithOIDC (
happyUpstream ( ) . WithIDTokenClaim ( oidcUpstreamGroupsClaim , nil ) . Build ( ) ,
) ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantContentType : htmlContentType ,
2021-08-18 19:06:46 +00:00
wantBody : "Unprocessable Entity: required claim in upstream ID token has invalid format\n" ,
2021-08-13 00:53:14 +00:00
wantAuthcodeExchangeCall : & expectedAuthcodeExchange {
performedByUpstreamName : happyUpstreamIDPName ,
args : happyExchangeAndValidateTokensArgs ,
} ,
2021-01-14 22:21:41 +00:00
} ,
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 ) {
2022-07-20 20:55:56 +00:00
kubeClient := fake . NewSimpleClientset ( )
supervisorClient := supervisorfake . NewSimpleClientset ( )
secrets := kubeClient . CoreV1 ( ) . Secrets ( "some-namespace" )
oidcClientsClient := supervisorClient . ConfigV1alpha1 ( ) . OIDCClients ( "some-namespace" )
if test . kubeResources != nil {
test . kubeResources ( t , supervisorClient , kubeClient )
}
2020-12-01 19:01:23 +00:00
// 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 ( )
2022-07-20 20:55:56 +00:00
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
oauthStore := oidc . NewKubeStorage ( secrets , oidcClientsClient , timeoutsConfiguration , bcrypt . MinCost )
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-08-13 00:53:14 +00:00
subject := NewHandler ( test . idps . Build ( ) , oauthHelper , happyStateCodec , happyCookieCodec , happyUpstreamRedirectURI )
reqContext := context . WithValue ( context . Background ( ) , struct { name string } { name : "test" } , "request-context" )
req := httptest . NewRequest ( test . method , test . path , nil ) . WithContext ( reqContext )
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
2022-05-05 20:12:06 +00:00
testutil . RequireSecurityHeadersWithFormPostPageCSPs ( t , rsp )
2020-12-14 23:28:32 +00:00
2021-08-13 00:53:14 +00:00
if test . wantAuthcodeExchangeCall != nil {
test . wantAuthcodeExchangeCall . args . Ctx = reqContext
test . idps . RequireExactlyOneCallToExchangeAuthcodeAndValidateTokens ( t ,
test . wantAuthcodeExchangeCall . performedByUpstreamName , test . wantAuthcodeExchangeCall . args ,
)
2020-11-19 16:35:23 +00:00
} else {
2021-08-13 00:53:14 +00:00
test . idps . RequireExactlyZeroCallsToExchangeAuthcodeAndValidateTokens ( t )
2020-11-19 16:35:23 +00:00
}
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 ,
2022-07-20 20:55:56 +00:00
kubeClient ,
2021-06-16 18:11:07 +00:00
secrets ,
oauthStore ,
test . wantDownstreamGrantedScopes ,
test . wantDownstreamIDTokenSubject ,
test . wantDownstreamIDTokenUsername ,
test . wantDownstreamIDTokenGroups ,
test . wantDownstreamRequestedScopes ,
test . wantDownstreamPKCEChallenge ,
test . wantDownstreamPKCEChallengeMethod ,
test . wantDownstreamNonce ,
downstreamClientID ,
downstreamRedirectURI ,
2021-10-08 22:48:21 +00:00
test . wantDownstreamCustomSessionData ,
2021-06-16 18:11:07 +00:00
)
// Otherwise, expect an empty response body.
default :
2020-11-19 01:15:01 +00:00
require . Empty ( t , rsp . Body . String ( ) )
}
2022-03-08 20:28:09 +00:00
if test . wantRedirectLocationRegexp != "" {
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 ,
2022-07-20 20:55:56 +00:00
kubeClient ,
2021-04-09 00:28:01 +00:00
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 ,
2021-10-08 22:48:21 +00:00
test . wantDownstreamCustomSessionData ,
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
2021-08-13 00:53:14 +00:00
type expectedAuthcodeExchange struct {
performedByUpstreamName string
args * oidctestutil . ExchangeAuthcodeAndValidateTokenArgs
}
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
2022-04-26 22:30:39 +00:00
func happyUpstreamStateParam ( ) * oidctestutil . UpstreamStateParamBuilder {
return & oidctestutil . UpstreamStateParamBuilder {
2020-11-20 21:33:08 +00:00
U : happyUpstreamIDPName ,
2020-11-20 14:41:49 +00:00
P : happyDownstreamRequestParams ,
2022-04-26 19:51:56 +00:00
T : "oidc" ,
2020-11-20 14:41:49 +00:00
N : happyDownstreamNonce ,
C : happyDownstreamCSRF ,
K : happyDownstreamPKCE ,
V : happyDownstreamStateVersion ,
2020-11-20 01:57:07 +00:00
}
}
2021-08-13 00:53:14 +00:00
func happyUpstream ( ) * oidctestutil . TestUpstreamOIDCIdentityProviderBuilder {
return oidctestutil . NewTestUpstreamOIDCIdentityProviderBuilder ( ) .
WithName ( happyUpstreamIDPName ) .
2021-10-08 22:48:21 +00:00
WithResourceUID ( happyUpstreamIDPResourceUID ) .
2021-08-13 00:53:14 +00:00
WithClientID ( "some-client-id" ) .
WithScopes ( [ ] string { "scope1" , "scope2" } ) .
WithUsernameClaim ( oidcUpstreamUsernameClaim ) .
WithGroupsClaim ( oidcUpstreamGroupsClaim ) .
WithIDTokenClaim ( "iss" , oidcUpstreamIssuer ) .
WithIDTokenClaim ( "sub" , oidcUpstreamSubject ) .
WithIDTokenClaim ( oidcUpstreamUsernameClaim , oidcUpstreamUsername ) .
WithIDTokenClaim ( oidcUpstreamGroupsClaim , oidcUpstreamGroupMembership ) .
WithIDTokenClaim ( "other-claim" , "should be ignored" ) .
WithAllowPasswordGrant ( false ) .
2021-10-08 22:48:21 +00:00
WithRefreshToken ( oidcUpstreamRefreshToken ) .
2021-08-13 00:53:14 +00:00
WithPasswordGrantError ( errors . New ( "the callback endpoint should not use password grants" ) )
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
}