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-16 22:07:34 +00:00
"regexp"
2020-11-19 01:15:01 +00:00
"strings"
2020-11-13 17:31:39 +00:00
"testing"
2020-11-20 01:57:07 +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-18 21:38:13 +00:00
"github.com/ory/fosite"
2020-11-19 01:15:01 +00:00
"github.com/ory/fosite/handler/openid"
2020-11-13 17:31:39 +00:00
"github.com/stretchr/testify/require"
2020-12-04 23:40:17 +00:00
"k8s.io/apimachinery/pkg/labels"
2020-12-01 19:01:23 +00:00
"k8s.io/client-go/kubernetes/fake"
2020-11-13 23:59:51 +00:00
2020-12-04 23:40:17 +00:00
"go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage/authorizationcode"
"go.pinniped.dev/internal/fositestorage/openidconnect"
"go.pinniped.dev/internal/fositestorage/pkce"
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-20 01:57:07 +00:00
"go.pinniped.dev/internal/oidc/oidctestutil"
2020-11-13 23:59:51 +00:00
"go.pinniped.dev/internal/testutil"
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
2020-11-20 01:57:07 +00:00
upstreamIssuer = "https://my-upstream-issuer.com"
2020-11-19 19:19:01 +00:00
upstreamSubject = "abc123-some-guid"
upstreamUsername = "test-pinniped-username"
upstreamUsernameClaim = "the-user-claim"
upstreamGroupsClaim = "the-groups-claim"
2020-11-20 01:57:07 +00:00
happyUpstreamAuthcode = "upstream-auth-code"
2020-11-20 14:41:49 +00:00
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
2020-12-10 18:14:54 +00:00
authCodeExpirationSeconds = 10 * 60 // Current, we set our auth code expiration to 10 minutes
2020-11-20 01:57:07 +00:00
timeComparisonFudgeFactor = time . Second * 15
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
wantBody string
wantRedirectLocationRegexp 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
} {
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 : "" ,
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 : "" ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenUsername : upstreamIssuer + "?sub=" + upstreamSubject ,
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 : "" ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 : "" ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 : "" ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 ,
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 ,
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 : "" ,
2020-12-15 01:05:53 +00:00
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 : "" ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 : "" ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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
{
name : "PUT method is invalid" ,
method : http . MethodPut ,
2020-11-13 23:59:51 +00:00
path : newRequestPath ( ) . String ( ) ,
2020-11-13 17:31:39 +00:00
wantStatus : http . StatusMethodNotAllowed ,
wantBody : "Method Not Allowed: PUT (try GET)\n" ,
} ,
2020-11-13 23:59:51 +00:00
{
name : "POST method is invalid" ,
method : http . MethodPost ,
path : newRequestPath ( ) . String ( ) ,
wantStatus : http . StatusMethodNotAllowed ,
wantBody : "Method Not Allowed: POST (try GET)\n" ,
} ,
{
name : "PATCH method is invalid" ,
method : http . MethodPatch ,
path : newRequestPath ( ) . String ( ) ,
wantStatus : http . StatusMethodNotAllowed ,
wantBody : "Method Not Allowed: PATCH (try GET)\n" ,
} ,
{
name : "DELETE method is invalid" ,
method : http . MethodDelete ,
path : newRequestPath ( ) . String ( ) ,
wantStatus : http . StatusMethodNotAllowed ,
wantBody : "Method Not Allowed: DELETE (try GET)\n" ,
} ,
{
2020-11-16 19:41:00 +00:00
name : "code param was not included on request" ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . WithoutCode ( ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
wantBody : "Bad Request: code param not found\n" ,
2020-11-13 23:59:51 +00:00
} ,
{
2020-11-16 19:41:00 +00:00
name : "state param was not included on request" ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithoutState ( ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
wantBody : "Bad Request: state param not found\n" ,
2020-11-13 23:59:51 +00:00
} ,
{
2020-11-19 15:20:46 +00:00
name : "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . Build ( ) ,
2020-11-19 15:20:46 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( "this-will-not-decode" ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
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 ,
wantBody : "Internal Server Error: error while generating and saving authcode\n" ,
} ,
2020-11-16 19:41:00 +00:00
{
2020-11-19 15:20:46 +00:00
name : "state's internal version does not match what we want" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . Build ( ) ,
2020-11-19 15:20:46 +00:00
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyUpstreamStateParam ( ) . WithStateVersion ( "wrong-state-version" ) . Build ( t , happyStateCodec ) ) . String ( ) ,
2020-11-19 15:20:46 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
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 ( ) ,
2020-11-19 15:20:46 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
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 ( ) ,
2020-11-19 15:20:46 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusBadRequest ,
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 ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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
{
2020-12-16 22:27:09 +00:00
name : "the OIDCIdentityProvider CRD has been deleted" ,
2020-11-19 15:20:46 +00:00
idp : otherUpstreamOIDCIdentityProvider ,
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusUnprocessableEntity ,
wantBody : "Unprocessable Entity: upstream provider not found\n" ,
2020-11-13 23:59:51 +00:00
} ,
2020-11-16 16:47:49 +00:00
{
2020-11-19 15:20:46 +00:00
name : "the CSRF cookie does not exist on request" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . Build ( ) ,
2020-11-19 15:20:46 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
wantStatus : http . StatusForbidden ,
wantBody : "Forbidden: CSRF cookie is missing\n" ,
2020-11-16 16:47:49 +00:00
} ,
{
2020-11-19 15:20:46 +00:00
name : "cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . Build ( ) ,
2020-11-19 15:20:46 +00:00
method : http . MethodGet ,
path : newRequestPath ( ) . WithState ( happyState ) . String ( ) ,
csrfCookie : "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped" ,
wantStatus : http . StatusForbidden ,
wantBody : "Forbidden: error reading CSRF cookie\n" ,
2020-11-16 19:41:00 +00:00
} ,
{
2020-11-19 15:20:46 +00:00
name : "cookie csrf value does not match state csrf value" ,
2020-11-19 19:19:01 +00:00
idp : happyUpstream ( ) . Build ( ) ,
2020-11-19 15:20:46 +00:00
method : http . MethodGet ,
2020-11-20 01:57:07 +00:00
path : newRequestPath ( ) . WithState ( happyUpstreamStateParam ( ) . WithCSRF ( "wrong-csrf-value" ) . Build ( t , happyStateCodec ) ) . String ( ) ,
2020-11-19 15:20:46 +00:00
csrfCookie : happyCSRFCookie ,
wantStatus : http . StatusForbidden ,
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" ,
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" ,
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 : "" ,
wantDownstreamIDTokenSubject : upstreamIssuer + "?sub=" + upstreamSubject ,
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 ,
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 ,
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 ,
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 ,
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 ,
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 ,
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
2020-11-20 01:57:07 +00:00
idpListGetter := oidctestutil . NewIDPListGetter ( & test . idp )
2020-12-02 16:36:07 +00:00
subject := NewHandler ( idpListGetter , 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 )
2020-11-19 01:15:01 +00:00
if test . wantBody != "" {
2020-11-16 22:07:34 +00:00
require . Equal ( t , test . wantBody , rsp . Body . String ( ) )
2020-11-19 01:15:01 +00:00
} else {
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
// Assert that Location header matches regular expression.
require . Len ( t , rsp . Header ( ) . Values ( "Location" ) , 1 )
actualLocation := rsp . Header ( ) . Get ( "Location" )
regex := regexp . MustCompile ( test . wantRedirectLocationRegexp )
submatches := regex . FindStringSubmatch ( actualLocation )
require . Lenf ( t , submatches , 2 , "no regexp match in actualLocation: %q" , actualLocation )
capturedAuthCode := submatches [ 1 ]
2020-11-19 01:15:01 +00:00
// fosite authcodes are in the format `data.signature`, so grab the signature part, which is the lookup key in the storage interface
authcodeDataAndSignature := strings . Split ( capturedAuthCode , "." )
require . Len ( t , authcodeDataAndSignature , 2 )
2020-12-01 19:01:23 +00:00
// Several Secrets should have been created
expectedNumberOfCreatedSecrets := 2
2020-12-08 01:22:34 +00:00
if includesOpenIDScope ( test . wantDownstreamGrantedScopes ) {
2020-12-01 19:01:23 +00:00
expectedNumberOfCreatedSecrets ++
}
require . Len ( t , client . Actions ( ) , expectedNumberOfCreatedSecrets )
// One authcode should have been stored.
2020-12-04 23:40:17 +00:00
testutil . RequireNumberOfSecretsMatchingLabelSelector ( t , secrets , labels . Set { crud . SecretLabelKey : authorizationcode . TypeLabelValue } , 1 )
2020-12-01 19:01:23 +00:00
2020-11-20 14:41:49 +00:00
storedRequestFromAuthcode , storedSessionFromAuthcode := validateAuthcodeStorage (
t ,
oauthStore ,
authcodeDataAndSignature [ 1 ] , // Authcode store key is authcode signature
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 ,
)
2020-12-01 19:01:23 +00:00
// One PKCE should have been stored.
2020-12-04 23:40:17 +00:00
testutil . RequireNumberOfSecretsMatchingLabelSelector ( t , secrets , labels . Set { crud . SecretLabelKey : pkce . TypeLabelValue } , 1 )
2020-12-01 19:01:23 +00:00
2020-11-20 14:41:49 +00:00
validatePKCEStorage (
t ,
oauthStore ,
authcodeDataAndSignature [ 1 ] , // PKCE store key is authcode signature
storedRequestFromAuthcode ,
storedSessionFromAuthcode ,
test . wantDownstreamPKCEChallenge ,
test . wantDownstreamPKCEChallengeMethod ,
)
2020-12-01 19:01:23 +00:00
// One IDSession should have been stored, if the downstream actually requested the "openid" scope
2020-12-08 01:22:34 +00:00
if includesOpenIDScope ( test . wantDownstreamGrantedScopes ) {
2020-12-04 23:40:17 +00:00
testutil . RequireNumberOfSecretsMatchingLabelSelector ( t , secrets , labels . Set { crud . SecretLabelKey : openidconnect . TypeLabelValue } , 1 )
2020-12-01 19:01:23 +00:00
validateIDSessionStorage (
t ,
oauthStore ,
capturedAuthCode , // IDSession store key is full authcode
storedRequestFromAuthcode ,
storedSessionFromAuthcode ,
test . wantDownstreamNonce ,
)
}
2020-11-16 22:07:34 +00:00
}
2020-11-13 17:31:39 +00:00
} )
}
}
2020-11-13 23:59:51 +00:00
2020-12-08 01:22:34 +00:00
func includesOpenIDScope ( scopes [ ] string ) bool {
for _ , scope := range scopes {
if scope == "openid" {
return true
}
}
return false
}
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
}
2020-11-20 14:41:49 +00:00
func validateAuthcodeStorage (
t * testing . T ,
2020-12-01 19:01:23 +00:00
oauthStore * oidc . KubeStorage ,
2020-11-20 14:41:49 +00:00
storeKey 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 ,
) ( * fosite . Request , * openid . DefaultSession ) {
t . Helper ( )
// Get the authcode session back from storage so we can require that it was stored correctly.
storedAuthorizeRequestFromAuthcode , err := oauthStore . GetAuthorizeCodeSession ( context . Background ( ) , storeKey , nil )
require . NoError ( t , err )
// Check that storage returned the expected concrete data types.
storedRequestFromAuthcode , storedSessionFromAuthcode := castStoredAuthorizeRequest ( t , storedAuthorizeRequestFromAuthcode )
// Check which scopes were granted.
2020-12-08 01:22:34 +00:00
require . ElementsMatch ( t , wantDownstreamGrantedScopes , storedRequestFromAuthcode . GetGrantedScopes ( ) )
2020-11-20 14:41:49 +00:00
// Check all the other fields of the stored request.
require . NotEmpty ( t , storedRequestFromAuthcode . ID )
require . Equal ( t , downstreamClientID , storedRequestFromAuthcode . Client . GetID ( ) )
require . ElementsMatch ( t , wantDownstreamRequestedScopes , storedRequestFromAuthcode . RequestedScope )
require . Nil ( t , storedRequestFromAuthcode . RequestedAudience )
require . Empty ( t , storedRequestFromAuthcode . GrantedAudience )
require . Equal ( t , url . Values { "redirect_uri" : [ ] string { downstreamRedirectURI } } , storedRequestFromAuthcode . Form )
testutil . RequireTimeInDelta ( t , time . Now ( ) , storedRequestFromAuthcode . RequestedAt , timeComparisonFudgeFactor )
// We're not using these fields yet, so confirm that we did not set them (for now).
require . Empty ( t , storedSessionFromAuthcode . Subject )
require . Empty ( t , storedSessionFromAuthcode . Username )
require . Empty ( t , storedSessionFromAuthcode . Headers )
2020-11-20 23:50:26 +00:00
// The authcode that we are issuing should be good for the length of time that we declare in the fosite config.
2020-12-10 18:14:54 +00:00
testutil . RequireTimeInDelta ( t , time . Now ( ) . Add ( authCodeExpirationSeconds * time . Second ) , storedSessionFromAuthcode . ExpiresAt [ fosite . AuthorizeCode ] , timeComparisonFudgeFactor )
2020-11-20 14:41:49 +00:00
require . Len ( t , storedSessionFromAuthcode . ExpiresAt , 1 )
// Now confirm the ID token claims.
actualClaims := storedSessionFromAuthcode . Claims
2020-12-15 01:05:53 +00:00
// Check the user's identity, which are put into the downstream ID token's subject, username and groups claims.
2020-11-20 14:41:49 +00:00
require . Equal ( t , wantDownstreamIDTokenSubject , actualClaims . Subject )
2020-12-15 01:05:53 +00:00
require . Equal ( t , wantDownstreamIDTokenUsername , actualClaims . Extra [ "username" ] )
2021-01-14 22:21:41 +00:00
require . Len ( t , actualClaims . Extra , 2 )
actualDownstreamIDTokenGroups := actualClaims . Extra [ "groups" ]
require . NotNil ( t , actualDownstreamIDTokenGroups )
require . ElementsMatch ( t , wantDownstreamIDTokenGroups , actualDownstreamIDTokenGroups )
2020-11-20 14:41:49 +00:00
2020-11-20 20:36:51 +00:00
// Check the rest of the downstream ID token's claims. Fosite wants us to set these (in UTC time).
testutil . RequireTimeInDelta ( t , time . Now ( ) . UTC ( ) , actualClaims . RequestedAt , timeComparisonFudgeFactor )
testutil . RequireTimeInDelta ( t , time . Now ( ) . UTC ( ) , actualClaims . AuthTime , timeComparisonFudgeFactor )
requestedAtZone , _ := actualClaims . RequestedAt . Zone ( )
require . Equal ( t , "UTC" , requestedAtZone )
authTimeZone , _ := actualClaims . AuthTime . Zone ( )
require . Equal ( t , "UTC" , authTimeZone )
// Fosite will set these fields for us in the token endpoint based on the store session
// information. Therefore, we assert that they are empty because we want the library to do the
// lifting for us.
require . Empty ( t , actualClaims . Issuer )
require . Nil ( t , actualClaims . Audience )
require . Empty ( t , actualClaims . Nonce )
require . Zero ( t , actualClaims . ExpiresAt )
require . Zero ( t , actualClaims . IssuedAt )
2020-11-20 14:41:49 +00:00
// These are not needed yet.
require . Empty ( t , actualClaims . JTI )
require . Empty ( t , actualClaims . CodeHash )
require . Empty ( t , actualClaims . AccessTokenHash )
require . Empty ( t , actualClaims . AuthenticationContextClassReference )
require . Empty ( t , actualClaims . AuthenticationMethodsReference )
return storedRequestFromAuthcode , storedSessionFromAuthcode
}
func validatePKCEStorage (
t * testing . T ,
2020-12-01 19:01:23 +00:00
oauthStore * oidc . KubeStorage ,
2020-11-20 14:41:49 +00:00
storeKey string ,
storedRequestFromAuthcode * fosite . Request ,
storedSessionFromAuthcode * openid . DefaultSession ,
wantDownstreamPKCEChallenge , wantDownstreamPKCEChallengeMethod string ,
) {
t . Helper ( )
storedAuthorizeRequestFromPKCE , err := oauthStore . GetPKCERequestSession ( context . Background ( ) , storeKey , nil )
require . NoError ( t , err )
// Check that storage returned the expected concrete data types.
storedRequestFromPKCE , storedSessionFromPKCE := castStoredAuthorizeRequest ( t , storedAuthorizeRequestFromPKCE )
// The stored PKCE request should be the same as the stored authcode request.
require . Equal ( t , storedRequestFromAuthcode . ID , storedRequestFromPKCE . ID )
require . Equal ( t , storedSessionFromAuthcode , storedSessionFromPKCE )
// The stored PKCE request should also contain the PKCE challenge that the downstream sent us.
require . Equal ( t , wantDownstreamPKCEChallenge , storedRequestFromPKCE . Form . Get ( "code_challenge" ) )
require . Equal ( t , wantDownstreamPKCEChallengeMethod , storedRequestFromPKCE . Form . Get ( "code_challenge_method" ) )
}
func validateIDSessionStorage (
t * testing . T ,
2020-12-01 19:01:23 +00:00
oauthStore * oidc . KubeStorage ,
2020-11-20 14:41:49 +00:00
storeKey string ,
storedRequestFromAuthcode * fosite . Request ,
storedSessionFromAuthcode * openid . DefaultSession ,
wantDownstreamNonce string ,
) {
t . Helper ( )
2020-12-01 19:01:23 +00:00
storedAuthorizeRequestFromIDSession , err := oauthStore . GetOpenIDConnectSession ( context . Background ( ) , storeKey , nil )
require . NoError ( t , err )
2020-11-20 14:41:49 +00:00
2020-12-01 19:01:23 +00:00
// Check that storage returned the expected concrete data types.
storedRequestFromIDSession , storedSessionFromIDSession := castStoredAuthorizeRequest ( t , storedAuthorizeRequestFromIDSession )
2020-11-20 14:41:49 +00:00
2020-12-01 19:01:23 +00:00
// The stored IDSession request should be the same as the stored authcode request.
require . Equal ( t , storedRequestFromAuthcode . ID , storedRequestFromIDSession . ID )
require . Equal ( t , storedSessionFromAuthcode , storedSessionFromIDSession )
2020-11-20 14:41:49 +00:00
2020-12-01 19:01:23 +00:00
// The stored IDSession request should also contain the nonce that the downstream sent us.
require . Equal ( t , wantDownstreamNonce , storedRequestFromIDSession . Form . Get ( "nonce" ) )
2020-11-20 14:41:49 +00:00
}
func castStoredAuthorizeRequest ( t * testing . T , storedAuthorizeRequest fosite . Requester ) ( * fosite . Request , * openid . DefaultSession ) {
t . Helper ( )
storedRequest , ok := storedAuthorizeRequest . ( * fosite . Request )
require . Truef ( t , ok , "could not cast %T to %T" , storedAuthorizeRequest , & fosite . Request { } )
storedSession , ok := storedAuthorizeRequest . GetSession ( ) . ( * openid . DefaultSession )
require . Truef ( t , ok , "could not cast %T to %T" , storedAuthorizeRequest . GetSession ( ) , & openid . DefaultSession { } )
return storedRequest , storedSession
}