refactor token_handler_test.go: easier to make more requests after initial authcode exchange

- This refactor will allow us to add new test tables for the
  refresh and token exchange requests, which both must come after
  an initial successful authcode exchange has already happened

Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
Ryan Richard 2020-12-08 16:54:58 -08:00 committed by Margo Crawford
parent a9111f39af
commit 170982a688

View File

@ -28,6 +28,7 @@ import (
"gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
"go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/crud"
"go.pinniped.dev/internal/fositestorage/accesstoken" "go.pinniped.dev/internal/fositestorage/accesstoken"
@ -192,10 +193,8 @@ var (
"status_code": 503 "status_code": 503
} }
`) `)
)
func TestTokenEndpoint(t *testing.T) { happyAuthRequest = &http.Request{
happyAuthRequest := &http.Request{
Form: url.Values{ Form: url.Values{
"response_type": {"code"}, "response_type": {"code"},
"scope": {"openid profile email"}, "scope": {"openid profile email"},
@ -207,10 +206,9 @@ func TestTokenEndpoint(t *testing.T) {
"redirect_uri": {goodRedirectURI}, "redirect_uri": {goodRedirectURI},
}, },
} }
)
tests := []struct { type authcodeExchangeInputs struct {
name string
modifyAuthRequest func(authRequest *http.Request) modifyAuthRequest func(authRequest *http.Request)
modifyTokenRequest func(r *http.Request, authCode string) modifyTokenRequest func(r *http.Request, authCode string)
modifyStorage func( modifyStorage func(
@ -238,18 +236,27 @@ func TestTokenEndpoint(t *testing.T) {
wantStatus int wantStatus int
wantBodyFields []string wantBodyFields []string
wantExactBody string
wantRequestedScopes []string wantRequestedScopes []string
wantExactBody string
}
func TestTokenEndpoint(t *testing.T) {
tests := []struct {
name string
authcodeExchange authcodeExchangeInputs
}{ }{
// happy path // happy path
{ {
name: "request is valid and tokens are issued", name: "request is valid and tokens are issued",
authcodeExchange: authcodeExchangeInputs{
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "profile", "email"}, wantRequestedScopes: []string{"openid", "profile", "email"},
}, },
},
{ {
name: "openid scope was not requested from authorize endpoint", name: "openid scope was not requested from authorize endpoint",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) { modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "profile email") authRequest.Form.Set("scope", "profile email")
}, },
@ -257,8 +264,10 @@ func TestTokenEndpoint(t *testing.T) {
wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in"}, // no id or refresh tokens wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in"}, // no id or refresh tokens
wantRequestedScopes: []string{"profile", "email"}, wantRequestedScopes: []string{"profile", "email"},
}, },
},
{ {
name: "offline_access and openid scopes were requested and granted from authorize endpoint", name: "offline_access and openid scopes were requested and granted from authorize endpoint",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) { modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid offline_access") authRequest.Form.Set("scope", "openid offline_access")
}, },
@ -266,8 +275,10 @@ func TestTokenEndpoint(t *testing.T) {
wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in", "refresh_token"}, // all possible tokens wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in", "refresh_token"}, // all possible tokens
wantRequestedScopes: []string{"openid", "offline_access"}, wantRequestedScopes: []string{"openid", "offline_access"},
}, },
},
{ {
name: "offline_access (without openid scope) was requested and granted from authorize endpoint", name: "offline_access (without openid scope) was requested and granted from authorize endpoint",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) { modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "offline_access") authRequest.Form.Set("scope", "offline_access")
}, },
@ -275,102 +286,130 @@ func TestTokenEndpoint(t *testing.T) {
wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in", "refresh_token"}, // no id token wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in", "refresh_token"}, // no id token
wantRequestedScopes: []string{"offline_access"}, wantRequestedScopes: []string{"offline_access"},
}, },
},
// sad path // sad path
{ {
name: "GET method is wrong", name: "GET method is wrong",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodGet }, modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodGet },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("GET"), wantExactBody: fositeInvalidMethodErrorBody("GET"),
}, },
},
{ {
name: "PUT method is wrong", name: "PUT method is wrong",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPut }, modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPut },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("PUT"), wantExactBody: fositeInvalidMethodErrorBody("PUT"),
}, },
},
{ {
name: "PATCH method is wrong", name: "PATCH method is wrong",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPatch }, modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPatch },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("PATCH"), wantExactBody: fositeInvalidMethodErrorBody("PATCH"),
}, },
},
{ {
name: "DELETE method is wrong", name: "DELETE method is wrong",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodDelete }, modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodDelete },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("DELETE"), wantExactBody: fositeInvalidMethodErrorBody("DELETE"),
}, },
},
{ {
name: "content type is invalid", name: "content type is invalid",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Header.Set("Content-Type", "text/plain") }, modifyTokenRequest: func(r *http.Request, authCode string) { r.Header.Set("Content-Type", "text/plain") },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeEmptyPayloadErrorBody, wantExactBody: fositeEmptyPayloadErrorBody,
}, },
},
{ {
name: "payload is not valid form serialization", name: "payload is not valid form serialization",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = ioutil.NopCloser(strings.NewReader("this newline character is not allowed in a form serialization: \n")) r.Body = ioutil.NopCloser(strings.NewReader("this newline character is not allowed in a form serialization: \n"))
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingGrantTypeErrorBody, wantExactBody: fositeMissingGrantTypeErrorBody,
}, },
},
{ {
name: "payload is empty", name: "payload is empty",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Body = nil }, modifyTokenRequest: func(r *http.Request, authCode string) { r.Body = nil },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidPayloadErrorBody, wantExactBody: fositeInvalidPayloadErrorBody,
}, },
},
{ {
name: "grant type is missing in request", name: "grant type is missing in request",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithGrantType("").ReadCloser() r.Body = happyBody(authCode).WithGrantType("").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingGrantTypeErrorBody, wantExactBody: fositeMissingGrantTypeErrorBody,
}, },
},
{ {
name: "grant type is not authorization_code", name: "grant type is not authorization_code",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithGrantType("bogus").ReadCloser() r.Body = happyBody(authCode).WithGrantType("bogus").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidRequestErrorBody, wantExactBody: fositeInvalidRequestErrorBody,
}, },
},
{ {
name: "client id is missing in request", name: "client id is missing in request",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithClientID("").ReadCloser() r.Body = happyBody(authCode).WithClientID("").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingClientErrorBody, wantExactBody: fositeMissingClientErrorBody,
}, },
},
{ {
name: "client id is wrong", name: "client id is wrong",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithClientID("bogus").ReadCloser() r.Body = happyBody(authCode).WithClientID("bogus").ReadCloser()
}, },
wantStatus: http.StatusUnauthorized, wantStatus: http.StatusUnauthorized,
wantExactBody: fositeInvalidClientErrorBody, wantExactBody: fositeInvalidClientErrorBody,
}, },
},
{ {
name: "auth code is missing in request", name: "auth code is missing in request",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithAuthCode("").ReadCloser() r.Body = happyBody(authCode).WithAuthCode("").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidAuthCodeErrorBody, wantExactBody: fositeInvalidAuthCodeErrorBody,
}, },
},
{ {
name: "auth code has never been valid", name: "auth code has never been valid",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithAuthCode("bogus").ReadCloser() r.Body = happyBody(authCode).WithAuthCode("bogus").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidAuthCodeErrorBody, wantExactBody: fositeInvalidAuthCodeErrorBody,
}, },
},
{ {
name: "auth code is invalidated", name: "auth code is invalidated",
authcodeExchange: authcodeExchangeInputs{
modifyStorage: func( modifyStorage: func(
t *testing.T, t *testing.T,
s interface { s interface {
@ -388,32 +427,40 @@ func TestTokenEndpoint(t *testing.T) {
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeReusedAuthCodeErrorBody, wantExactBody: fositeReusedAuthCodeErrorBody,
}, },
},
{ {
name: "redirect uri is missing in request", name: "redirect uri is missing in request",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithRedirectURI("").ReadCloser() r.Body = happyBody(authCode).WithRedirectURI("").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidRedirectURIErrorBody, wantExactBody: fositeInvalidRedirectURIErrorBody,
}, },
},
{ {
name: "redirect uri is wrong", name: "redirect uri is wrong",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithRedirectURI("bogus").ReadCloser() r.Body = happyBody(authCode).WithRedirectURI("bogus").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidRedirectURIErrorBody, wantExactBody: fositeInvalidRedirectURIErrorBody,
}, },
},
{ {
name: "pkce is missing in request", name: "pkce is missing in request",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithPKCE("").ReadCloser() r.Body = happyBody(authCode).WithPKCE("").ReadCloser()
}, },
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingPKCEVerifierErrorBody, wantExactBody: fositeMissingPKCEVerifierErrorBody,
}, },
},
{ {
name: "pkce is wrong", name: "pkce is wrong",
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { modifyTokenRequest: func(r *http.Request, authCode string) {
r.Body = happyBody(authCode).WithPKCE( r.Body = happyBody(authCode).WithPKCE(
"bogus-verifier-that-is-at-least-43-characters-for-the-sake-of-entropy", "bogus-verifier-that-is-at-least-43-characters-for-the-sake-of-entropy",
@ -422,29 +469,106 @@ func TestTokenEndpoint(t *testing.T) {
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeWrongPKCEVerifierErrorBody, wantExactBody: fositeWrongPKCEVerifierErrorBody,
}, },
},
{ {
name: "private signing key for JWTs has not yet been provided by the controller who is responsible for dynamically providing it", name: "private signing key for JWTs has not yet been provided by the controller who is responsible for dynamically providing it",
authcodeExchange: authcodeExchangeInputs{
makeOathHelper: makeOauthHelperWithNilPrivateJWTSigningKey, makeOathHelper: makeOauthHelperWithNilPrivateJWTSigningKey,
wantStatus: http.StatusServiceUnavailable, wantStatus: http.StatusServiceUnavailable,
wantExactBody: fositeTemporarilyUnavailableErrorBody, wantExactBody: fositeTemporarilyUnavailableErrorBody,
}, },
},
} }
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
exchangeAuthcodeForTokens(t, test.authcodeExchange)
})
}
}
func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) {
tests := []struct {
name string
authcodeExchange authcodeExchangeInputs
wantGrantedOpenidScope bool
wantGrantedOfflineAccessScope bool
}{
{
name: "authcode exchange succeeds once and then fails when the same authcode is used again",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid offline_access profile email")
},
wantStatus: http.StatusOK,
wantBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "profile", "email"},
},
wantGrantedOpenidScope: true,
wantGrantedOfflineAccessScope: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
// First call - should be successful.
subject, rsp, authCode, secrets, oauthStore := exchangeAuthcodeForTokens(t, test.authcodeExchange)
var parsedResponseBody map[string]interface{}
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody))
// Second call - should be unsuccessful since auth code was already used.
//
// Fosite will also revoke the access token as is recommended by the OIDC spec. Currently, we don't
// delete the OIDC storage...but we probably should.
req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rsp1 := httptest.NewRecorder()
subject.ServeHTTP(rsp1, req)
t.Logf("second response: %#v", rsp1)
t.Logf("second response body: %q", rsp1.Body.String())
require.Equal(t, http.StatusBadRequest, rsp1.Code)
testutil.RequireEqualContentType(t, rsp1.Header().Get("Content-Type"), "application/json")
require.JSONEq(t, fositeReusedAuthCodeErrorBody, rsp1.Body.String())
// This was previously invalidated by the first request, so it remains invalidated
requireInvalidAuthCodeStorage(t, authCode, oauthStore)
// Has now invalidated the access token that was previously handed out by the first request
requireInvalidAccessTokenStorage(t, parsedResponseBody, oauthStore)
// This was previously invalidated by the first request, so it remains invalidated
requireInvalidPKCEStorage(t, authCode, oauthStore)
// Fosite never cleans up OpenID Connect session storage, so it is still there
requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.authcodeExchange.wantRequestedScopes, test.wantGrantedOpenidScope, test.wantGrantedOfflineAccessScope)
// Check that the access token and refresh token storage were both deleted, and the number of other storage objects did not change.
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 0)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 0)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2)
})
}
}
func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) (
subject http.Handler,
rsp *httptest.ResponseRecorder,
authCode string,
secrets v1.SecretInterface,
oauthStore *oidc.KubeStorage,
) {
authRequest := deepCopyRequestForm(happyAuthRequest) authRequest := deepCopyRequestForm(happyAuthRequest)
if test.modifyAuthRequest != nil { if test.modifyAuthRequest != nil {
test.modifyAuthRequest(authRequest) test.modifyAuthRequest(authRequest)
} }
client := fake.NewSimpleClientset() client := fake.NewSimpleClientset()
secrets := client.CoreV1().Secrets("some-namespace") secrets = client.CoreV1().Secrets("some-namespace")
var oauthHelper fosite.OAuth2Provider var oauthHelper fosite.OAuth2Provider
var authCode string
var jwtSigningKey *ecdsa.PrivateKey var jwtSigningKey *ecdsa.PrivateKey
oauthStore := oidc.NewKubeStorage(secrets) oauthStore = oidc.NewKubeStorage(secrets)
if test.makeOathHelper != nil { if test.makeOathHelper != nil {
oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore) oauthHelper, authCode, jwtSigningKey = test.makeOathHelper(t, authRequest, oauthStore)
} else { } else {
@ -454,7 +578,7 @@ func TestTokenEndpoint(t *testing.T) {
if test.modifyStorage != nil { if test.modifyStorage != nil {
test.modifyStorage(t, oauthStore, authCode) test.modifyStorage(t, oauthStore, authCode)
} }
subject := NewHandler(oauthHelper) subject = NewHandler(oauthHelper)
authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid") authorizeEndpointGrantedOpenIDScope := strings.Contains(authRequest.Form.Get("scope"), "openid")
expectedNumberOfIDSessionsStored := 0 expectedNumberOfIDSessionsStored := 0
@ -472,7 +596,7 @@ func TestTokenEndpoint(t *testing.T) {
if test.modifyTokenRequest != nil { if test.modifyTokenRequest != nil {
test.modifyTokenRequest(req, authCode) test.modifyTokenRequest(req, authCode)
} }
rsp := httptest.NewRecorder() rsp = httptest.NewRecorder()
subject.ServeHTTP(rsp, req) subject.ServeHTTP(rsp, req)
t.Logf("response: %#v", rsp) t.Logf("response: %#v", rsp)
@ -516,90 +640,8 @@ func TestTokenEndpoint(t *testing.T) {
} else { } else {
require.JSONEq(t, test.wantExactBody, rsp.Body.String()) require.JSONEq(t, test.wantExactBody, rsp.Body.String())
} }
})
}
t.Run("auth code is used twice", func(t *testing.T) { return subject, rsp, authCode, secrets, oauthStore
authRequest := deepCopyRequestForm(happyAuthRequest)
authRequest.Form.Set("scope", "openid offline_access profile email")
wantRequestedScopes := []string{"openid", "offline_access", "profile", "email"}
wantBodyFields := []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"}
wantGrantedOpenidScope := true
wantGrantedOfflineAccessScope := true
client := fake.NewSimpleClientset()
secrets := client.CoreV1().Secrets("some-namespace")
oauthStore := oidc.NewKubeStorage(secrets)
oauthHelper, authCode, jwtSigningKey := makeHappyOauthHelper(t, authRequest, oauthStore)
subject := NewHandler(oauthHelper)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 3)
req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// First call - should be successful.
rsp0 := httptest.NewRecorder()
subject.ServeHTTP(rsp0, req)
t.Logf("response 0: %#v", rsp0)
t.Logf("response 0 body: %q", rsp0.Body.String())
testutil.RequireEqualContentType(t, rsp0.Header().Get("Content-Type"), "application/json")
require.Equal(t, http.StatusOK, rsp0.Code)
var parsedResponseBody map[string]interface{}
require.NoError(t, json.Unmarshal(rsp0.Body.Bytes(), &parsedResponseBody))
require.ElementsMatch(t, wantBodyFields, getMapKeys(parsedResponseBody))
requireValidIDToken(t, parsedResponseBody, jwtSigningKey)
code := req.PostForm.Get("code")
requireInvalidAuthCodeStorage(t, code, oauthStore)
requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, wantRequestedScopes, wantGrantedOpenidScope, wantGrantedOfflineAccessScope)
requireInvalidPKCEStorage(t, code, oauthStore)
requireValidOIDCStorage(t, parsedResponseBody, code, oauthStore, wantRequestedScopes, wantGrantedOpenidScope, wantGrantedOfflineAccessScope)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 4)
// Second call - should be unsuccessful since auth code was already used.
//
// Fosite will also revoke the access token as is recommended by the OIDC spec. Currently, we don't
// delete the OIDC storage...but we probably should.
rsp1 := httptest.NewRecorder()
subject.ServeHTTP(rsp1, req)
t.Logf("response 1: %#v", rsp1)
t.Logf("response 1 body: %q", rsp1.Body.String())
require.Equal(t, http.StatusBadRequest, rsp1.Code)
testutil.RequireEqualContentType(t, rsp1.Header().Get("Content-Type"), "application/json")
require.JSONEq(t, fositeReusedAuthCodeErrorBody, rsp1.Body.String())
// This was previously invalidated by the first request, so it remains invalidated
requireInvalidAuthCodeStorage(t, code, oauthStore)
// Has now invalidated the access token that was previously handed out by the first request
requireInvalidAccessTokenStorage(t, parsedResponseBody, oauthStore)
// This was previously invalidated by the first request, so it remains invalidated
requireInvalidPKCEStorage(t, code, oauthStore)
// Fosite never cleans up OpenID Connect session storage, so it is still there
requireValidOIDCStorage(t, parsedResponseBody, code, oauthStore, wantRequestedScopes, wantGrantedOpenidScope, wantGrantedOfflineAccessScope)
// Check that the access token and refresh token storage were both deleted, and the number of other storage objects did not change.
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: accesstoken.TypeLabelValue}, 0)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: refreshtoken.TypeLabelValue}, 0)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: storagepkce.TypeLabelValue}, 0)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2)
})
} }
type body url.Values type body url.Values