Add check for grant type in tokenexchangehandler,

- also started writing a test for the tokenexchangehandler, skipping for
now

Signed-off-by: Ryan Richard <rrichard@vmware.com>
This commit is contained in:
Margo Crawford 2020-12-08 17:33:08 -08:00 committed by Ryan Richard
parent ef3f837800
commit f103c02408
4 changed files with 76 additions and 5 deletions

View File

@ -27,7 +27,7 @@ func TestNullStorage_GetClient(t *testing.T) {
Public: true,
RedirectURIs: []string{"http://127.0.0.1/callback"},
ResponseTypes: []string{"code"},
GrantTypes: []string{"authorization_code", "refresh_token"},
GrantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"},
Scopes: []string{"openid", "offline_access", "profile", "email"},
},
TokenEndpointAuthMethod: "none",

View File

@ -84,8 +84,8 @@ func PinnipedCLIOIDCClient() *fosite.DefaultOpenIDConnectClient {
Public: true,
RedirectURIs: []string{"http://127.0.0.1/callback"},
ResponseTypes: []string{"code"},
GrantTypes: []string{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"},
Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email"},
GrantTypes: []string{"authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"},
Scopes: []string{coreosoidc.ScopeOpenID, coreosoidc.ScopeOfflineAccess, "profile", "email", "pinniped.sts.unrestricted"},
},
TokenEndpointAuthMethod: "none",
}
@ -126,12 +126,12 @@ func FositeOauth2Helper(
OpenIDConnectTokenStrategy: newDynamicOpenIDConnectECDSAStrategy(oauthConfig, jwksProvider),
},
nil, // hasher, defaults to using BCrypt when nil. Used for hashing client secrets.
TokenExchangeFactory,
compose.OAuth2AuthorizeExplicitFactory,
// compose.OAuth2RefreshTokenGrantFactory,
compose.OpenIDConnectExplicitFactory,
// compose.OpenIDConnectRefreshFactory,
compose.OAuth2PKCEFactory,
TokenExchangeFactory,
)
}

View File

@ -206,6 +206,19 @@ var (
"redirect_uri": {goodRedirectURI},
},
}
happyTokenExchangeRequest = func(audience string, subjectToken string) *http.Request {
return &http.Request{
Form: url.Values{
"grant_type": {"urn:ietf:params:oauth:grant-type:token-exchange"},
"audience": {audience},
"subject_token": {subjectToken},
"subject_token_type": {"urn:ietf:params:oauth:token-type:access_token"},
"requested_token_type": {"urn:ietf:params:oauth:token-type:jwt"},
"client_id": {goodClient},
},
}
}
)
type authcodeExchangeInputs struct {
@ -550,6 +563,55 @@ func TestTokenEndpointWhenAuthcodeIsUsedTwice(t *testing.T) {
}
}
func TestTokenExchange(t *testing.T) {
// TODO write this test
t.Skip()
tests := []struct {
name string
authcodeExchange authcodeExchangeInputs
wantStatus int
requestedAudience string
modifyTokenExchangeRequest func(r *http.Request)
}{
{
name: "token exchange happy path",
authcodeExchange: authcodeExchangeInputs{
modifyAuthRequest: func(authRequest *http.Request) {
authRequest.Form.Set("scope", "openid pinniped.sts.unrestricted")
},
wantStatus: http.StatusOK,
wantBodyFields: []string{"id_token", "access_token", "token_type", "expires_in", "scope"},
wantRequestedScopes: []string{"openid", "pinniped.sts.unrestricted"},
},
wantStatus: http.StatusOK,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
subject, rsp, _, _, _ := exchangeAuthcodeForTokens(t, test.authcodeExchange)
var parsedResponseBody map[string]interface{}
require.NoError(t, json.Unmarshal(rsp.Body.Bytes(), &parsedResponseBody))
request := happyTokenExchangeRequest("foo-cluster", parsedResponseBody["access_token"].(string))
req := httptest.NewRequest("POST", "/path/shouldn't/matter", body(request.Form).ReadCloser())
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if test.modifyTokenExchangeRequest != nil {
test.modifyTokenExchangeRequest(req)
}
rsp = httptest.NewRecorder()
subject.ServeHTTP(rsp, req)
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")
})
}
}
func exchangeAuthcodeForTokens(t *testing.T, test authcodeExchangeInputs) (
subject http.Handler,
rsp *httptest.ResponseRecorder,
@ -758,6 +820,9 @@ func simulateAuthEndpointHavingAlreadyRun(t *testing.T, authRequest *http.Reques
if strings.Contains(authRequest.Form.Get("scope"), "offline_access") {
authRequester.GrantScope("offline_access")
}
if strings.Contains(authRequest.Form.Get("scope"), "pinniped.sts.unrestricted") {
authRequester.GrantScope("pinniped.sts.unrestricted")
}
authResponder, err := oauthHelper.NewAuthorizeResponse(ctx, authRequester, session)
require.NoError(t, err)
return authResponder

View File

@ -1,3 +1,6 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package oidc
import (
@ -35,6 +38,9 @@ func (t *TokenExchangeHandler) HandleTokenEndpointRequest(ctx context.Context, r
}
func (t *TokenExchangeHandler) PopulateTokenEndpointResponse(ctx context.Context, requester fosite.AccessRequester, responder fosite.AccessResponder) error {
if !(requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange")) {
return errors.WithStack(fosite.ErrUnknownRequest)
}
params := requester.GetRequestForm()
accessToken := params.Get("subject_token")
if err := t.accessTokenStrategy.ValidateAccessToken(ctx, requester, accessToken); err != nil {
@ -49,7 +55,7 @@ func (t *TokenExchangeHandler) PopulateTokenEndpointResponse(ctx context.Context
return errors.WithStack(fosite.ErrScopeNotGranted)
}
// TODO check the other requester fields
scopedDownRequester := fosite.NewAccessRequest(requester.GetSession())
scopedDownRequester := fosite.NewAccessRequest(accessTokenSession.GetSession())
scopedDownRequester.GrantedAudience = []string{params.Get("audience")}
newToken, err := t.idTokenStrategy.GenerateIDToken(ctx, scopedDownRequester)
if err != nil {