token_handler_test.go: Refactor how we specify the expected results

- This is to make it easier for the token exchange branch to also edit
  this test without causing a lot of merge conflicts with the
  refresh token branch, to enable parallel development of closely
  related stories.
This commit is contained in:
Ryan Richard 2020-12-08 18:10:55 -08:00
parent 170982a688
commit ef4ef583dc

View File

@ -235,9 +235,10 @@ type authcodeExchangeInputs struct {
) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey)
wantStatus int
wantBodyFields []string
wantSuccessBodyFields []string
wantErrorResponseBody string
wantRequestedScopes []string
wantExactBody string
wantGrantedScopes []string
}
func TestTokenEndpoint(t *testing.T) {
@ -250,8 +251,9 @@ func TestTokenEndpoint(t *testing.T) {
name: "request is valid and tokens are issued",
authcodeExchange: authcodeExchangeInputs{
wantStatus: http.StatusOK,
wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in"}, // no refresh token
wantRequestedScopes: []string{"openid", "profile", "email"},
wantGrantedScopes: []string{"openid"},
},
},
{
@ -261,8 +263,9 @@ func TestTokenEndpoint(t *testing.T) {
authRequest.Form.Set("scope", "profile email")
},
wantStatus: http.StatusOK,
wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in"}, // no id or refresh tokens
wantSuccessBodyFields: []string{"access_token", "token_type", "scope", "expires_in"}, // no id or refresh tokens
wantRequestedScopes: []string{"profile", "email"},
wantGrantedScopes: []string{},
},
},
{
@ -272,8 +275,9 @@ func TestTokenEndpoint(t *testing.T) {
authRequest.Form.Set("scope", "openid offline_access")
},
wantStatus: http.StatusOK,
wantBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in", "refresh_token"}, // all possible tokens
wantSuccessBodyFields: []string{"id_token", "access_token", "token_type", "scope", "expires_in", "refresh_token"}, // all possible tokens
wantRequestedScopes: []string{"openid", "offline_access"},
wantGrantedScopes: []string{"openid", "offline_access"},
},
},
{
@ -283,8 +287,9 @@ func TestTokenEndpoint(t *testing.T) {
authRequest.Form.Set("scope", "offline_access")
},
wantStatus: http.StatusOK,
wantBodyFields: []string{"access_token", "token_type", "scope", "expires_in", "refresh_token"}, // no id token
wantSuccessBodyFields: []string{"access_token", "token_type", "scope", "expires_in", "refresh_token"}, // no id token
wantRequestedScopes: []string{"offline_access"},
wantGrantedScopes: []string{"offline_access"},
},
},
@ -294,7 +299,7 @@ func TestTokenEndpoint(t *testing.T) {
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodGet },
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("GET"),
wantErrorResponseBody: fositeInvalidMethodErrorBody("GET"),
},
},
{
@ -302,7 +307,7 @@ func TestTokenEndpoint(t *testing.T) {
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPut },
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("PUT"),
wantErrorResponseBody: fositeInvalidMethodErrorBody("PUT"),
},
},
{
@ -310,7 +315,7 @@ func TestTokenEndpoint(t *testing.T) {
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodPatch },
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("PATCH"),
wantErrorResponseBody: fositeInvalidMethodErrorBody("PATCH"),
},
},
{
@ -318,7 +323,7 @@ func TestTokenEndpoint(t *testing.T) {
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Method = http.MethodDelete },
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("DELETE"),
wantErrorResponseBody: fositeInvalidMethodErrorBody("DELETE"),
},
},
{
@ -326,7 +331,7 @@ func TestTokenEndpoint(t *testing.T) {
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Header.Set("Content-Type", "text/plain") },
wantStatus: http.StatusBadRequest,
wantExactBody: fositeEmptyPayloadErrorBody,
wantErrorResponseBody: fositeEmptyPayloadErrorBody,
},
},
{
@ -336,7 +341,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = ioutil.NopCloser(strings.NewReader("this newline character is not allowed in a form serialization: \n"))
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingGrantTypeErrorBody,
wantErrorResponseBody: fositeMissingGrantTypeErrorBody,
},
},
{
@ -344,7 +349,7 @@ func TestTokenEndpoint(t *testing.T) {
authcodeExchange: authcodeExchangeInputs{
modifyTokenRequest: func(r *http.Request, authCode string) { r.Body = nil },
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidPayloadErrorBody,
wantErrorResponseBody: fositeInvalidPayloadErrorBody,
},
},
{
@ -354,7 +359,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithGrantType("").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingGrantTypeErrorBody,
wantErrorResponseBody: fositeMissingGrantTypeErrorBody,
},
},
{
@ -364,7 +369,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithGrantType("bogus").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidRequestErrorBody,
wantErrorResponseBody: fositeInvalidRequestErrorBody,
},
},
{
@ -374,7 +379,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithClientID("").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingClientErrorBody,
wantErrorResponseBody: fositeMissingClientErrorBody,
},
},
{
@ -384,7 +389,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithClientID("bogus").ReadCloser()
},
wantStatus: http.StatusUnauthorized,
wantExactBody: fositeInvalidClientErrorBody,
wantErrorResponseBody: fositeInvalidClientErrorBody,
},
},
{
@ -394,7 +399,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithAuthCode("").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidAuthCodeErrorBody,
wantErrorResponseBody: fositeInvalidAuthCodeErrorBody,
},
},
{
@ -404,7 +409,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithAuthCode("bogus").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidAuthCodeErrorBody,
wantErrorResponseBody: fositeInvalidAuthCodeErrorBody,
},
},
{
@ -425,7 +430,7 @@ func TestTokenEndpoint(t *testing.T) {
require.NoError(t, err)
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeReusedAuthCodeErrorBody,
wantErrorResponseBody: fositeReusedAuthCodeErrorBody,
},
},
{
@ -435,7 +440,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithRedirectURI("").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidRedirectURIErrorBody,
wantErrorResponseBody: fositeInvalidRedirectURIErrorBody,
},
},
{
@ -445,7 +450,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithRedirectURI("bogus").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidRedirectURIErrorBody,
wantErrorResponseBody: fositeInvalidRedirectURIErrorBody,
},
},
{
@ -455,7 +460,7 @@ func TestTokenEndpoint(t *testing.T) {
r.Body = happyBody(authCode).WithPKCE("").ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeMissingPKCEVerifierErrorBody,
wantErrorResponseBody: fositeMissingPKCEVerifierErrorBody,
},
},
{
@ -467,7 +472,7 @@ func TestTokenEndpoint(t *testing.T) {
).ReadCloser()
},
wantStatus: http.StatusBadRequest,
wantExactBody: fositeWrongPKCEVerifierErrorBody,
wantErrorResponseBody: fositeWrongPKCEVerifierErrorBody,
},
},
{
@ -475,7 +480,7 @@ func TestTokenEndpoint(t *testing.T) {
authcodeExchange: authcodeExchangeInputs{
makeOathHelper: makeOauthHelperWithNilPrivateJWTSigningKey,
wantStatus: http.StatusServiceUnavailable,
wantExactBody: fositeTemporarilyUnavailableErrorBody,
wantErrorResponseBody: fositeTemporarilyUnavailableErrorBody,
},
},
}
@ -491,8 +496,6 @@ 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",
@ -501,11 +504,10 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) {
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"},
wantSuccessBodyFields: []string{"id_token", "refresh_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "offline_access", "profile", "email"},
wantGrantedScopes: []string{"openid", "offline_access"},
},
wantGrantedOpenidScope: true,
wantGrantedOfflineAccessScope: true,
},
}
for _, test := range tests {
@ -537,7 +539,7 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) {
// 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)
requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.authcodeExchange.wantRequestedScopes, test.authcodeExchange.wantGrantedScopes)
// 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)
@ -602,21 +604,23 @@ func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) (
t.Logf("response: %#v", rsp)
t.Logf("response body: %q", rsp.Body.String())
require.Equal(t, test.wantStatus, rsp.Code)
testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), "application/json")
if test.wantBodyFields != nil {
require.Equal(t, test.wantStatus, rsp.Code)
if test.wantStatus == http.StatusOK {
require.NotNil(t, test.wantSuccessBodyFields, "problem with test table setup: wanted success but did not specify expected response body")
var parsedResponseBody map[string]interface{}
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody))
require.ElementsMatch(t, test.wantBodyFields, getMapKeys(parsedResponseBody))
require.ElementsMatch(t, test.wantSuccessBodyFields, getMapKeys(parsedResponseBody))
wantIDToken := contains(test.wantBodyFields, "id_token")
wantRefreshToken := contains(test.wantBodyFields, "refresh_token")
wantIDToken := contains(test.wantSuccessBodyFields, "id_token")
wantRefreshToken := contains(test.wantSuccessBodyFields, "refresh_token")
code := req.PostForm.Get("code")
requireInvalidAuthCodeStorage(t, code, oauthStore)
requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, wantIDToken, wantRefreshToken)
requireInvalidPKCEStorage(t, code, oauthStore)
requireValidOIDCStorage(t, parsedResponseBody, code, oauthStore, test.wantRequestedScopes, wantIDToken, wantRefreshToken)
requireInvalidAuthCodeStorage(t, authCode, oauthStore)
requireValidAccessTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes)
requireInvalidPKCEStorage(t, authCode, oauthStore)
requireValidOIDCStorage(t, parsedResponseBody, authCode, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes)
expectedNumberOfRefreshTokenSessionsStored := 0
if wantRefreshToken {
@ -628,7 +632,7 @@ func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) (
requireValidIDToken(t, parsedResponseBody, jwtSigningKey)
}
if wantRefreshToken {
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, wantIDToken, wantRefreshToken)
requireValidRefreshTokenStorage(t, parsedResponseBody, oauthStore, test.wantRequestedScopes, test.wantGrantedScopes)
}
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1)
@ -638,7 +642,9 @@ func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) (
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, expectedNumberOfIDSessionsStored)
testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{}, 2+expectedNumberOfRefreshTokenSessionsStored+expectedNumberOfIDSessionsStored)
} else {
require.JSONEq(t, test.wantExactBody, rsp.Body.String())
require.NotNil(t, test.wantErrorResponseBody, "problem with test table setup: wanted failure but did not specify failure response body")
require.JSONEq(t, test.wantErrorResponseBody, rsp.Body.String())
}
return subject, rsp, authCode, secrets, oauthStore
@ -736,9 +742,8 @@ func makeOauthHelperWithNilPrivateJWTSigningKey(
return oauthHelper, authResponder.GetCode(), nil
}
func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Request, oauthHelper fosite.OAuth2Provider) fosite.AuthorizeResponder {
// Simulate the auth endpoint running so Fosite code will fill the store with realistic values.
//
func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Request, oauthHelper fosite.OAuth2Provider) fosite.AuthorizeResponder {
// We only set the fields in the session that Fosite wants us to set.
ctx := context.Background()
session := &openid.DefaultSession{
@ -797,8 +802,7 @@ func requireValidRefreshTokenStorage(
body map[string]interface{},
storage oauth2.CoreStorage,
wantRequestedScopes []string,
wantGrantedOpenidScope bool,
wantGrantedOfflineAccessScope bool,
wantGrantedScopes []string,
) {
t.Helper()
@ -817,8 +821,7 @@ func requireValidRefreshTokenStorage(
storedRequest,
storedRequest.Sanitize([]string{}).GetRequestForm(),
wantRequestedScopes,
wantGrantedOpenidScope,
wantGrantedOfflineAccessScope,
wantGrantedScopes,
true,
)
}
@ -828,8 +831,7 @@ func requireValidAccessTokenStorage(
body map[string]interface{},
storage oauth2.CoreStorage,
wantRequestedScopes []string,
wantGrantedOpenidScope bool,
wantGrantedOfflineAccessScope bool,
wantGrantedScopes []string,
) {
t.Helper()
@ -859,7 +861,7 @@ func requireValidAccessTokenStorage(
require.True(t, ok)
actualGrantedScopesString, ok := scopes.(string)
require.Truef(t, ok, "wanted scopes to be an string, but got %T", scopes)
require.Equal(t, strings.Join(wantGrantedScopes(wantGrantedOpenidScope, wantGrantedOfflineAccessScope), " "), actualGrantedScopesString)
require.Equal(t, strings.Join(wantGrantedScopes, " "), actualGrantedScopesString)
// Fosite stores access tokens without any of the original request form parameters.
requireValidStoredRequest(
@ -867,23 +869,11 @@ func requireValidAccessTokenStorage(
storedRequest,
storedRequest.Sanitize([]string{}).GetRequestForm(),
wantRequestedScopes,
wantGrantedOpenidScope,
wantGrantedOfflineAccessScope,
wantGrantedScopes,
true,
)
}
func wantGrantedScopes(wantGrantedOpenidScope, wantGrantedOfflineAccessScope bool) []string {
scopesWanted := []string{}
if wantGrantedOpenidScope {
scopesWanted = append(scopesWanted, "openid")
}
if wantGrantedOfflineAccessScope {
scopesWanted = append(scopesWanted, "offline_access")
}
return scopesWanted
}
func requireInvalidAccessTokenStorage(
t *testing.T,
body map[string]interface{},
@ -919,12 +909,11 @@ func requireValidOIDCStorage(
code string,
storage openid.OpenIDConnectRequestStorage,
wantRequestedScopes []string,
wantGrantedOpenidScope bool,
wantGrantedOfflineAccessScope bool,
wantGrantedScopes []string,
) {
t.Helper()
if wantGrantedOpenidScope {
if contains(wantGrantedScopes, "openid") {
// Make sure the OIDC session is still there. Note that Fosite stores OIDC sessions using the full auth code as a key.
storedRequest, err := storage.GetOpenIDConnectSession(context.Background(), code, nil)
require.NoError(t, err)
@ -941,8 +930,7 @@ func requireValidOIDCStorage(
storedRequest,
storedRequest.Sanitize([]string{"nonce"}).GetRequestForm(),
wantRequestedScopes,
wantGrantedOpenidScope,
wantGrantedOfflineAccessScope,
wantGrantedScopes,
false,
)
} else {
@ -956,8 +944,7 @@ func requireValidStoredRequest(
request fosite.Requester,
wantRequestForm url.Values,
wantRequestedScopes []string,
wantGrantedOpenidScope bool,
wantGrantedOfflineAccessScope bool,
wantGrantedScopes []string,
wantAccessTokenExpiresAt bool,
) {
t.Helper()
@ -967,7 +954,7 @@ func requireValidStoredRequest(
testutil.RequireTimeInDelta(t, request.GetRequestedAt(), time.Now().UTC(), timeComparisonFudgeSeconds*time.Second)
require.Equal(t, goodClient, request.GetClient().GetID())
require.Equal(t, fosite.Arguments(wantRequestedScopes), request.GetRequestedScopes())
require.Equal(t, fosite.Arguments(wantGrantedScopes(wantGrantedOpenidScope, wantGrantedOfflineAccessScope)), request.GetGrantedScopes())
require.Equal(t, fosite.Arguments(wantGrantedScopes), request.GetGrantedScopes())
require.Empty(t, request.GetRequestedAudience())
require.Empty(t, request.GetGrantedAudience())
require.Equal(t, wantRequestForm, request.GetRequestForm()) // Fosite stores access token request without form
@ -977,7 +964,7 @@ func requireValidStoredRequest(
require.Truef(t, ok, "could not cast %T to %T", request.GetSession(), &openid.DefaultSession{})
// Assert that the session claims are what we think they should be, but only if we are doing OIDC.
if wantGrantedOpenidScope {
if contains(wantGrantedScopes, "openid") {
claims := session.Claims
require.Empty(t, claims.JTI) // When claims.JTI is empty, Fosite will generate a UUID for this field.
require.Equal(t, goodSubject, claims.Subject)