token_handler.go: complete some TODOs and strengthen double auth code test

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
Andrew Keesler 2020-12-02 15:14:01 -05:00
parent 8e4c85d816
commit 09e6c86c46
No known key found for this signature in database
GPG Key ID: 27CE0444346F9413
2 changed files with 77 additions and 38 deletions

View File

@ -18,8 +18,6 @@ func NewHandler(
oauthHelper fosite.OAuth2Provider, oauthHelper fosite.OAuth2Provider,
) http.Handler { ) http.Handler {
return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
// check csrf cookie?
var session openid.DefaultSession var session openid.DefaultSession
accessRequest, err := oauthHelper.NewAccessRequest(r.Context(), r, &session) accessRequest, err := oauthHelper.NewAccessRequest(r.Context(), r, &session)
if err != nil { if err != nil {
@ -28,8 +26,6 @@ func NewHandler(
return nil return nil
} }
// TODO: do we need to grant the openid scope here? Or should it be already granted?
accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest) accessResponse, err := oauthHelper.NewAccessResponse(r.Context(), accessRequest)
if err != nil { if err != nil {
plog.Info("token response error", fositeErrorForLog(err)...) plog.Info("token response error", fositeErrorForLog(err)...)

View File

@ -50,6 +50,9 @@ const (
) )
var ( var (
goodAuthTime = time.Date(1, 2, 3, 4, 5, 6, 7, time.Local)
goodRequestedAtTime = time.Date(7, 6, 5, 4, 3, 2, 1, time.Local)
fositeInvalidMethodErrorBody = func(actual string) string { fositeInvalidMethodErrorBody = func(actual string) string {
return here.Docf(` return here.Docf(`
{ {
@ -172,7 +175,7 @@ var (
) )
func TestTokenEndpoint(t *testing.T) { 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"},
@ -236,7 +239,6 @@ func TestTokenEndpoint(t *testing.T) {
wantStatus: http.StatusBadRequest, wantStatus: http.StatusBadRequest,
wantExactBody: fositeInvalidMethodErrorBody("DELETE"), wantExactBody: fositeInvalidMethodErrorBody("DELETE"),
}, },
// TODO: add test for when csrf cookie is invalid...maybe?
{ {
name: "content type is invalid", name: "content type is invalid",
request: func(r *http.Request, authCode string) { r.Header.Set("Content-Type", "text/plain") }, request: func(r *http.Request, authCode string) { r.Header.Set("Content-Type", "text/plain") },
@ -352,13 +354,13 @@ func TestTokenEndpoint(t *testing.T) {
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) {
authRequest := happyAuthRequest authRequest := deepCopyRequestForm(happyAuthRequest)
if test.authRequest != nil { if test.authRequest != nil {
test.authRequest(&authRequest) test.authRequest(authRequest)
} }
oauthStore := storage.NewMemoryStore() oauthStore := storage.NewMemoryStore()
oauthHelper, authCode := makeHappyOauthHelper(t, &authRequest, oauthStore) oauthHelper, authCode := makeHappyOauthHelper(t, authRequest, oauthStore)
if test.storage != nil { if test.storage != nil {
test.storage(t, oauthStore, authCode) test.storage(t, oauthStore, authCode)
} }
@ -369,9 +371,6 @@ func TestTokenEndpoint(t *testing.T) {
if test.request != nil { if test.request != nil {
test.request(req, authCode) test.request(req, authCode)
} }
// if test.csrfCookie != "" {
// req.Header.Set("Cookie", test.csrfCookie)
// }
rsp := httptest.NewRecorder() rsp := httptest.NewRecorder()
subject.ServeHTTP(rsp, req) subject.ServeHTTP(rsp, req)
@ -387,9 +386,9 @@ func TestTokenEndpoint(t *testing.T) {
code := req.PostForm.Get("code") code := req.PostForm.Get("code")
wantOpenidScope := contains(test.wantBodyFields, "id_token") wantOpenidScope := contains(test.wantBodyFields, "id_token")
requireValidAuthCodeStorage(t, code, oauthStore) requireInvalidAuthCodeStorage(t, code, oauthStore)
requireValidAccessTokenStorage(t, m, oauthStore, wantOpenidScope) requireValidAccessTokenStorage(t, m, oauthStore, wantOpenidScope)
requireValidPKCEStorage(t, code, oauthStore) requireInvalidPKCEStorage(t, code, oauthStore)
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope) requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
} else { } else {
require.JSONEq(t, test.wantExactBody, rsp.Body.String()) require.JSONEq(t, test.wantExactBody, rsp.Body.String())
@ -398,25 +397,39 @@ func TestTokenEndpoint(t *testing.T) {
} }
t.Run("auth code is used twice", func(t *testing.T) { t.Run("auth code is used twice", func(t *testing.T) {
authRequest := happyAuthRequest authRequest := deepCopyRequestForm(happyAuthRequest)
oauthStore := storage.NewMemoryStore() oauthStore := storage.NewMemoryStore()
oauthHelper, authCode := makeHappyOauthHelper(t, &authRequest, oauthStore) oauthHelper, authCode := makeHappyOauthHelper(t, authRequest, oauthStore)
subject := NewHandler(oauthHelper) subject := NewHandler(oauthHelper)
req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser()) req := httptest.NewRequest("POST", "/path/shouldn't/matter", happyBody(authCode).ReadCloser())
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// if test.csrfCookie != "" {
// req.Header.Set("Cookie", test.csrfCookie)
// }
// First call - should be successful. // First call - should be successful.
rsp0 := httptest.NewRecorder() rsp0 := httptest.NewRecorder()
subject.ServeHTTP(rsp0, req) subject.ServeHTTP(rsp0, req)
t.Logf("response 0: %#v", rsp0) t.Logf("response 0: %#v", rsp0)
t.Logf("response 0 body: %q", rsp0.Body.String()) t.Logf("response 0 body: %q", rsp0.Body.String())
requireEqualContentType(t, rsp0.Header().Get("Content-Type"), "application/json")
require.Equal(t, http.StatusOK, rsp0.Code) require.Equal(t, http.StatusOK, rsp0.Code)
var m map[string]interface{}
require.NoError(t, json.Unmarshal(rsp0.Body.Bytes(), &m))
wantBodyFields := []string{"id_token", "access_token", "token_type", "expires_in", "scope"}
require.ElementsMatch(t, wantBodyFields, getMapKeys(m))
code := req.PostForm.Get("code")
wantOpenidScope := true
requireInvalidAuthCodeStorage(t, code, oauthStore)
requireValidAccessTokenStorage(t, m, oauthStore, wantOpenidScope)
requireInvalidPKCEStorage(t, code, oauthStore)
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
// Second call - should be unsuccessful since auth code was already used. // 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() rsp1 := httptest.NewRecorder()
subject.ServeHTTP(rsp1, req) subject.ServeHTTP(rsp1, req)
t.Logf("response 1: %#v", rsp1) t.Logf("response 1: %#v", rsp1)
@ -424,6 +437,11 @@ func TestTokenEndpoint(t *testing.T) {
require.Equal(t, http.StatusBadRequest, rsp1.Code) require.Equal(t, http.StatusBadRequest, rsp1.Code)
requireEqualContentType(t, rsp1.Header().Get("Content-Type"), "application/json") requireEqualContentType(t, rsp1.Header().Get("Content-Type"), "application/json")
require.JSONEq(t, fositeReusedAuthCodeErrorBody, rsp1.Body.String()) require.JSONEq(t, fositeReusedAuthCodeErrorBody, rsp1.Body.String())
requireInvalidAuthCodeStorage(t, code, oauthStore)
requireInvalidAccessTokenStorage(t, m, oauthStore)
requireInvalidPKCEStorage(t, code, oauthStore)
requireValidOIDCStorage(t, m, code, oauthStore, wantOpenidScope)
}) })
} }
@ -495,10 +513,14 @@ func makeHappyOauthHelper(
store.Clients[goodClient] = oidc.PinnipedCLIOIDCClient() store.Clients[goodClient] = oidc.PinnipedCLIOIDCClient()
// Simulate the auth endpoint running so Fosite code will fill the store with realistic values. // Simulate the auth endpoint running so Fosite code will fill the store with realistic values.
//
// We only set the fields in the session that Fosite wants us to set.
ctx := context.Background() ctx := context.Background()
session := &openid.DefaultSession{ session := &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{ Claims: &jwt.IDTokenClaims{
Subject: goodSubject, Subject: goodSubject,
AuthTime: goodAuthTime,
RequestedAt: goodRequestedAtTime,
}, },
Subject: goodSubject, Subject: goodSubject,
Username: goodUsername, Username: goodUsername,
@ -536,7 +558,7 @@ func doSHA256(s string) string {
return base64.RawURLEncoding.EncodeToString(b[:]) return base64.RawURLEncoding.EncodeToString(b[:])
} }
func requireValidAuthCodeStorage( func requireInvalidAuthCodeStorage(
t *testing.T, t *testing.T,
code string, code string,
storage *storage.MemoryStore, storage *storage.MemoryStore,
@ -585,7 +607,7 @@ func requireValidAccessTokenStorage(
if wantGrantedOpenidScope { if wantGrantedOpenidScope {
wantScopes += "openid" wantScopes += "openid"
} }
require.Equal(t, wantScopes, scopesString) // TODO: do we need that extra scope that mo put forth? require.Equal(t, wantScopes, scopesString)
// Fosite stores access tokens without any of the original request form pararmeters. // Fosite stores access tokens without any of the original request form pararmeters.
requireValidAuthRequest( requireValidAuthRequest(
@ -597,7 +619,23 @@ func requireValidAccessTokenStorage(
) )
} }
func requireValidPKCEStorage( func requireInvalidAccessTokenStorage(
t *testing.T,
body map[string]interface{},
storage *storage.MemoryStore,
) {
t.Helper()
// Get the access token, and make sure we can use it to perform a lookup on the storage.
accessToken, ok := body["access_token"]
require.True(t, ok)
accessTokenString, ok := accessToken.(string)
require.Truef(t, ok, "wanted access_token to be a string, but got %T", accessToken)
_, err := storage.GetAccessTokenSession(context.Background(), getFositeDataSignature(t, accessTokenString), nil)
require.Equal(t, fosite.ErrNotFound, err)
}
func requireInvalidPKCEStorage(
t *testing.T, t *testing.T,
code string, code string,
storage *storage.MemoryStore, storage *storage.MemoryStore,
@ -674,7 +712,7 @@ func requireValidAuthRequest(
// Assert that the session claims are what we think they should be, but only if we are doing OIDC. // Assert that the session claims are what we think they should be, but only if we are doing OIDC.
if wantGrantedOpenidScope { if wantGrantedOpenidScope {
claims := session.Claims claims := session.Claims
// require.NotEmpty(t, claims.JTI) // TODO: why is this not passing? require.Empty(t, claims.JTI) // When claims.JTI is empty, Fosite will generate a UUID for this field.
require.Equal(t, goodIssuer, claims.Issuer) require.Equal(t, goodIssuer, claims.Issuer)
require.Equal(t, goodSubject, claims.Subject) require.Equal(t, goodSubject, claims.Subject)
require.Equal(t, []string{goodClient}, claims.Audience) require.Equal(t, []string{goodClient}, claims.Audience)
@ -686,13 +724,18 @@ func requireValidAuthRequest(
timeComparisonFudgeSeconds*time.Second, timeComparisonFudgeSeconds*time.Second,
) )
requireTimeInDelta(t, time.Now().UTC(), claims.IssuedAt, timeComparisonFudgeSeconds*time.Second) requireTimeInDelta(t, time.Now().UTC(), claims.IssuedAt, timeComparisonFudgeSeconds*time.Second)
// requireTimeInDelta(t, time.Now().UTC(), claims.RequestedAt, timeComparisonFudgeSeconds*time.Second) // TODO: why is this not passing?
requireTimeInDelta(t, time.Now().UTC(), claims.AuthTime, timeComparisonFudgeSeconds*time.Second)
require.Equal(t, wantAccessTokenHash, claims.AccessTokenHash) require.Equal(t, wantAccessTokenHash, claims.AccessTokenHash)
require.Empty(t, claims.AuthenticationContextClassReference) // TODO: huh?
require.Empty(t, claims.AuthenticationMethodsReference) // TODO: huh? // We are in charge of setting these fields. For the purpose of testing, we ensure that the
require.Empty(t, claims.CodeHash) // TODO: huh? // sentinel test value is set correctly.
require.Empty(t, claims.Extra) // TODO: huh? require.Equal(t, goodRequestedAtTime, claims.RequestedAt)
require.Equal(t, goodAuthTime, claims.AuthTime)
// At this time, we don't use any of these optional (per the OIDC spec) fields.
require.Empty(t, claims.AuthenticationContextClassReference)
require.Empty(t, claims.AuthenticationMethodsReference)
require.Empty(t, claims.CodeHash)
require.Empty(t, claims.Extra)
} }
// Assert that the session headers are what we think they should be. // Assert that the session headers are what we think they should be.
@ -716,14 +759,6 @@ func requireValidAuthRequest(
accessTokenExpiresAt, accessTokenExpiresAt,
timeComparisonFudgeSeconds*time.Second, timeComparisonFudgeSeconds*time.Second,
) )
// idTokenExpiresAt, ok := session.ExpiresAt[fosite.IDToken] // TODO: why is this failing?
// require.True(t, ok, "expected session to hold expiration time for id token")
// requireTimeInDelta(
// t,
// time.Now().UTC().Add(idTokenExpirationSeconds*time.Second),
// idTokenExpiresAt,
// timeComparisonFudgeSeconds*time.Second,
// )
// Assert that the session's username and subject are correct. // Assert that the session's username and subject are correct.
require.Equal(t, goodUsername, session.Username) require.Equal(t, goodUsername, session.Username)
@ -763,6 +798,14 @@ func requireTimeInDelta(t *testing.T, t1 time.Time, t2 time.Time, delta time.Dur
) )
} }
func deepCopyRequestForm(r *http.Request) *http.Request {
copied := url.Values{}
for k, v := range r.Form {
copied[k] = v
}
return &http.Request{Form: copied}
}
func getMapKeys(m map[string]interface{}) []string { func getMapKeys(m map[string]interface{}) []string {
keys := make([]string, 0) keys := make([]string, 0)
for key := range m { for key := range m {