diff --git a/access_request.go b/access_request.go index 2ef86c3f..4c7e9f47 100644 --- a/access_request.go +++ b/access_request.go @@ -23,3 +23,22 @@ func NewAccessRequest(session Session) *AccessRequest { func (a *AccessRequest) GetGrantTypes() Arguments { return a.GrantTypes } + +func (a *AccessRequest) SetGrantedScopes(scopes Arguments) { + a.GrantedScope = scopes +} + +func (a *AccessRequest) SanitizeRestoreRefreshTokenOriginalRequester(requester Requester) Requester { + r := a.Sanitize(nil).(*Request) + + ar := &AccessRequest{ + Request: *r, + } + + ar.SetID(requester.GetID()) + + ar.SetRequestedScopes(requester.GetRequestedScopes()) + ar.SetGrantedScopes(requester.GetGrantedScopes()) + + return ar +} diff --git a/handler/oauth2/flow_refresh.go b/handler/oauth2/flow_refresh.go index 5bf68070..f9064407 100644 --- a/handler/oauth2/flow_refresh.go +++ b/handler/oauth2/flow_refresh.go @@ -10,7 +10,6 @@ import ( "time" "github.com/ory/x/errorsx" - "github.com/pkg/errors" "github.com/ory/fosite" @@ -30,6 +29,11 @@ type RefreshTokenGrantHandler struct { fosite.AudienceStrategyProvider fosite.RefreshTokenScopesProvider } + + // IgnoreRequestedScopeNotInOriginalGrant determines the action to take when the requested scopes in the refresh + // flow were not originally granted. If false which is the default the handler will automatically return an error. + // If true the handler will filter out / ignore the scopes which were not originally granted. + IgnoreRequestedScopeNotInOriginalGrant bool } // HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-6 @@ -69,7 +73,6 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex scopeNames := strings.Join(c.Config.GetRefreshTokenScopes(ctx), " or ") hint := fmt.Sprintf("The OAuth 2.0 Client was not granted scope %s and may thus not perform the 'refresh_token' authorization grant.", scopeNames) return errorsx.WithStack(fosite.ErrScopeNotGranted.WithHint(hint)) - } // The authorization server MUST ... and ensure that the refresh token was issued to the authenticated client @@ -79,13 +82,50 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex request.SetID(originalRequest.GetID()) request.SetSession(originalRequest.GetSession().Clone()) - request.SetRequestedScopes(originalRequest.GetRequestedScopes()) + + /* + There are two key points in the following spec section this addresses: + 1. If omitted the scope param should be treated as the same as the scope originally granted by the resource owner. + 2. The REQUESTED scope MUST NOT include any scope not originally granted. + + scope + OPTIONAL. The scope of the access request as described by Section 3.3. The requested scope MUST NOT + include any scope not originally granted by the resource owner, and if omitted is treated as equal to + the scope originally granted by the resource owner. + + See https://www.rfc-editor.org/rfc/rfc6749#section-6 + */ + + // Addresses point 1 of the text in RFC6749 Section 6. + if len(request.GetRequestedScopes()) == 0 { + request.SetRequestedScopes(originalRequest.GetGrantedScopes()) + } + request.SetRequestedAudience(originalRequest.GetRequestedAudience()) - for _, scope := range originalRequest.GetGrantedScopes() { - if !c.Config.GetScopeStrategy(ctx)(request.GetClient().GetScopes(), scope) { + strategy := c.Config.GetScopeStrategy(ctx) + originalScopes := originalRequest.GetGrantedScopes() + + for _, scope := range request.GetRequestedScopes() { + // Utilizing the fosite.ScopeStrategy from the configuration here could be a mistake in some scenarios. + // The client could under certain circumstances be able to escape their originally granted scopes with carefully + // crafted requests and/or a custom scope strategy has not been implemented with this specific scenario in mind. + // This should always be an exact comparison for these reasons. + if !originalScopes.Has(scope) { + if c.IgnoreRequestedScopeNotInOriginalGrant { + // Skips addressing point 2 of the text in RFC6749 Section 6 and instead just prevents the scope + // requested from being granted. + continue + } else { + // Addresses point 2 of the text in RFC6749 Section 6. + return errorsx.WithStack(fosite.ErrInvalidScope.WithHintf("The requested scope '%s' was not originally granted by the resource owner.", scope)) + } + } + + if !strategy(request.GetClient().GetScopes(), scope) { return errorsx.WithStack(fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope)) } + request.GrantScope(scope) } @@ -134,26 +174,36 @@ func (c *RefreshTokenGrantHandler) PopulateTokenEndpointResponse(ctx context.Con err = c.handleRefreshTokenEndpointStorageError(ctx, err) }() - ts, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, nil) + originalRequest, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, nil) if err != nil { return err - } else if err := c.TokenRevocationStorage.RevokeAccessToken(ctx, ts.GetID()); err != nil { + } else if err := c.TokenRevocationStorage.RevokeAccessToken(ctx, originalRequest.GetID()); err != nil { return err } - if err := c.TokenRevocationStorage.RevokeRefreshTokenMaybeGracePeriod(ctx, ts.GetID(), signature); err != nil { + if err := c.TokenRevocationStorage.RevokeRefreshTokenMaybeGracePeriod(ctx, originalRequest.GetID(), signature); err != nil { return err } storeReq := requester.Sanitize([]string{}) - storeReq.SetID(ts.GetID()) + storeReq.SetID(originalRequest.GetID()) if err = c.TokenRevocationStorage.CreateAccessTokenSession(ctx, accessSignature, storeReq); err != nil { return err } - if err = c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, storeReq); err != nil { - return err + if rtRequest, ok := requester.(fosite.RefreshTokenAccessRequester); ok { + rtStoreReq := rtRequest.SanitizeRestoreRefreshTokenOriginalRequester(originalRequest) + + rtStoreReq.SetSession(requester.GetSession().Clone()) + + if err = c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, rtStoreReq); err != nil { + return err + } + } else { + if err = c.TokenRevocationStorage.CreateRefreshTokenSession(ctx, refreshSignature, storeReq); err != nil { + return err + } } responder.SetAccessToken(accessToken) diff --git a/handler/oauth2/flow_refresh_test.go b/handler/oauth2/flow_refresh_test.go index 54df6cda..dbe7867a 100644 --- a/handler/oauth2/flow_refresh_test.go +++ b/handler/oauth2/flow_refresh_test.go @@ -11,14 +11,12 @@ import ( "time" "github.com/golang/mock/gomock" - - "github.com/ory/fosite/internal" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/fosite" + "github.com/ory/fosite/internal" "github.com/ory/fosite/storage" ) @@ -177,13 +175,104 @@ func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) { assert.NotEqual(t, sess, areq.Session) assert.NotEqual(t, time.Now().UTC().Add(-time.Hour).Round(time.Hour), areq.RequestedAt) assert.Equal(t, fosite.Arguments{"foo", "offline"}, areq.GrantedScope) - assert.Equal(t, fosite.Arguments{"foo", "bar", "offline"}, areq.RequestedScope) + assert.Equal(t, fosite.Arguments{"foo", "offline"}, areq.RequestedScope) assert.NotEqual(t, url.Values{"foo": []string{"bar"}}, areq.Form) assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.AccessToken)) assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.RefreshToken)) assert.EqualValues(t, areq.Form.Get("or_request_id"), areq.GetID(), "Requester ID should be replaced based on the refresh token session") }, }, + { + description: "should pass with scope in form", + setup: func(config *fosite.Config) { + areq.GrantTypes = fosite.Arguments{"refresh_token"} + areq.Client = &fosite.DefaultClient{ + ID: "foo", + GrantTypes: fosite.Arguments{"refresh_token"}, + Scopes: []string{"foo", "bar", "baz", "offline"}, + } + + token, sig, err := strategy.GenerateRefreshToken(nil, nil) + require.NoError(t, err) + + areq.Form.Add("refresh_token", token) + areq.Form.Add("scope", "foo bar baz offline") + err = store.CreateRefreshTokenSession(nil, sig, &fosite.Request{ + Client: areq.Client, + GrantedScope: fosite.Arguments{"foo", "bar", "baz", "offline"}, + RequestedScope: fosite.Arguments{"foo", "bar", "baz", "offline"}, + Session: sess, + Form: url.Values{"foo": []string{"bar"}}, + RequestedAt: time.Now().UTC().Add(-time.Hour).Round(time.Hour), + }) + require.NoError(t, err) + }, + expect: func(t *testing.T) { + assert.Equal(t, fosite.Arguments{"foo", "bar", "baz", "offline"}, areq.GrantedScope) + assert.Equal(t, fosite.Arguments{"foo", "bar", "baz", "offline"}, areq.RequestedScope) + }, + }, + { + description: "should pass with scope in form and should narrow scopes", + setup: func(config *fosite.Config) { + areq.GrantTypes = fosite.Arguments{"refresh_token"} + areq.Client = &fosite.DefaultClient{ + ID: "foo", + GrantTypes: fosite.Arguments{"refresh_token"}, + Scopes: []string{"foo", "bar", "baz", "offline"}, + } + + token, sig, err := strategy.GenerateRefreshToken(nil, nil) + require.NoError(t, err) + + areq.Form.Add("refresh_token", token) + areq.Form.Add("scope", "foo bar offline") + areq.SetRequestedScopes(fosite.Arguments{"foo", "bar", "offline"}) + + err = store.CreateRefreshTokenSession(nil, sig, &fosite.Request{ + Client: areq.Client, + GrantedScope: fosite.Arguments{"foo", "bar", "baz", "offline"}, + RequestedScope: fosite.Arguments{"foo", "bar", "baz", "offline"}, + Session: sess, + Form: url.Values{"foo": []string{"bar"}}, + RequestedAt: time.Now().UTC().Add(-time.Hour).Round(time.Hour), + }) + require.NoError(t, err) + }, + expect: func(t *testing.T) { + assert.Equal(t, fosite.Arguments{"foo", "bar", "offline"}, areq.GrantedScope) + assert.Equal(t, fosite.Arguments{"foo", "bar", "offline"}, areq.RequestedScope) + }, + }, + { + description: "should fail with broadened scopes even if the client can request it", + setup: func(config *fosite.Config) { + areq.GrantTypes = fosite.Arguments{"refresh_token"} + areq.Client = &fosite.DefaultClient{ + ID: "foo", + GrantTypes: fosite.Arguments{"refresh_token"}, + Scopes: []string{"foo", "bar", "baz", "offline"}, + } + + token, sig, err := strategy.GenerateRefreshToken(nil, nil) + require.NoError(t, err) + + areq.Form.Add("refresh_token", token) + areq.Form.Add("scope", "foo bar offline") + areq.SetRequestedScopes(fosite.Arguments{"foo", "bar", "offline"}) + + err = store.CreateRefreshTokenSession(nil, sig, &fosite.Request{ + Client: areq.Client, + GrantedScope: fosite.Arguments{"foo", "baz", "offline"}, + RequestedScope: fosite.Arguments{"foo", "baz", "offline"}, + Session: sess, + Form: url.Values{"foo": []string{"bar"}}, + RequestedAt: time.Now().UTC().Add(-time.Hour).Round(time.Hour), + }) + require.NoError(t, err) + }, + expectErr: fosite.ErrInvalidScope, + }, { description: "should pass with custom client lifespans", setup: func(config *fosite.Config) { @@ -216,7 +305,7 @@ func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) { assert.NotEqual(t, sess, areq.Session) assert.NotEqual(t, time.Now().UTC().Add(-time.Hour).Round(time.Hour), areq.RequestedAt) assert.Equal(t, fosite.Arguments{"foo", "offline"}, areq.GrantedScope) - assert.Equal(t, fosite.Arguments{"foo", "bar", "offline"}, areq.RequestedScope) + assert.Equal(t, fosite.Arguments{"foo", "offline"}, areq.RequestedScope) assert.NotEqual(t, url.Values{"foo": []string{"bar"}}, areq.Form) internal.RequireEqualTime(t, time.Now().Add(*internal.TestLifespans.RefreshTokenGrantAccessTokenLifespan).UTC(), areq.GetSession().GetExpiresAt(fosite.AccessToken), time.Minute) internal.RequireEqualTime(t, time.Now().Add(*internal.TestLifespans.RefreshTokenGrantRefreshTokenLifespan).UTC(), areq.GetSession().GetExpiresAt(fosite.RefreshToken), time.Minute) @@ -277,7 +366,7 @@ func TestRefreshFlow_HandleTokenEndpointRequest(t *testing.T) { assert.NotEqual(t, sess, areq.Session) assert.NotEqual(t, time.Now().UTC().Add(-time.Hour).Round(time.Hour), areq.RequestedAt) assert.Equal(t, fosite.Arguments{"foo"}, areq.GrantedScope) - assert.Equal(t, fosite.Arguments{"foo", "bar"}, areq.RequestedScope) + assert.Equal(t, fosite.Arguments{"foo"}, areq.RequestedScope) assert.NotEqual(t, url.Values{"foo": []string{"bar"}}, areq.Form) assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.AccessToken)) assert.Equal(t, time.Now().Add(time.Hour).UTC().Round(time.Second), areq.GetSession().GetExpiresAt(fosite.RefreshToken)) diff --git a/integration/helper_endpoints_test.go b/integration/helper_endpoints_test.go index 09e3ea2b..b2af3fcb 100644 --- a/integration/helper_endpoints_test.go +++ b/integration/helper_endpoints_test.go @@ -73,16 +73,22 @@ func authEndpointHandler(t *testing.T, oauth2 fosite.OAuth2Provider, session fos return } - if ar.GetRequestedScopes().Has("fosite") { - ar.GrantScope("fosite") - } + if ar.GetClient().GetID() == "grant-all-requested-scopes-client" { + for _, scope := range ar.GetRequestedScopes() { + ar.GrantScope(scope) + } + } else { + if ar.GetRequestedScopes().Has("fosite") { + ar.GrantScope("fosite") + } - if ar.GetRequestedScopes().Has("offline") { - ar.GrantScope("offline") - } + if ar.GetRequestedScopes().Has("offline") { + ar.GrantScope("offline") + } - if ar.GetRequestedScopes().Has("openid") { - ar.GrantScope("openid") + if ar.GetRequestedScopes().Has("openid") { + ar.GrantScope("openid") + } } for _, a := range ar.GetRequestedAudience() { @@ -130,7 +136,7 @@ func tokenEndpointHandler(t *testing.T, provider fosite.OAuth2Provider) func(rw accessRequest, err := provider.NewAccessRequest(ctx, req, &oauth2.JWTSession{}) if err != nil { - t.Logf("Access request failed because: %+v", err) + t.Logf("Access request failed because: %+v", fosite.ErrorToRFC6749Error(err).WithExposeDebug(true).GetDescription()) t.Logf("Request: %+v", accessRequest) provider.WriteAccessError(req.Context(), rw, accessRequest, err) return diff --git a/integration/refresh_token_grant_test.go b/integration/refresh_token_grant_test.go index e30b2bdf..1838967f 100644 --- a/integration/refresh_token_grant_test.go +++ b/integration/refresh_token_grant_test.go @@ -13,15 +13,15 @@ import ( "testing" "time" - "github.com/ory/fosite/internal/gen" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" "github.com/ory/fosite" "github.com/ory/fosite/compose" + hoauth2 "github.com/ory/fosite/handler/oauth2" "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/internal/gen" "github.com/ory/fosite/token/jwt" ) @@ -266,3 +266,294 @@ func TestRefreshTokenFlow(t *testing.T) { }) } } + +func TestRefreshTokenFlowScopeParameter(t *testing.T) { + type testCase struct { + name string + scopes fosite.Arguments + expected fosite.Arguments + err string + } + + type step struct { + OAuth2 *oauth2.Token + SessionAT, SessionRT fosite.Requester + } + + originalScopes := fosite.Arguments{"openid", "offline", "offline_access", "foo", "bar"} + + scenarios := []struct { + name string + ignore bool + checkTime bool + testCases []testCase + }{ + { + "ShouldPassRFC", + false, + true, + []testCase{ + { + "ShouldGrantOriginalScopesWhenOmitted", + nil, + originalScopes, + "", + }, + { + "ShouldNarrowScopesWhenIncluded", + fosite.Arguments{"openid", "offline_access", "foo"}, + fosite.Arguments{"openid", "offline_access", "foo"}, + "", + }, + { + "ShouldGrantOriginalScopesWhenOmittedAfterNarrowing", + nil, + originalScopes, + "", + }, + { + "ShouldGrantOriginalScopesExplicitlyRequested", + originalScopes, + originalScopes, + "", + }, + { + "ShouldErrorWhenBroadeningScopesAllowedByClientButNotOriginallyGranted", + fosite.Arguments{"openid", "offline", "offline_access", "foo", "bar", "baz"}, + nil, + "The requested scope is invalid, unknown, or malformed. The requested scope 'baz' was not originally granted by the resource owner.", + }, + }, + }, + { + "ShouldPassIgnoreFilter", + true, + false, + []testCase{ + { + "ShouldGrantOriginalScopesWhenOmitted", + nil, + originalScopes, + "", + }, + { + "ShouldNarrowScopesWhenIncluded", + fosite.Arguments{"openid", "offline_access", "foo"}, + fosite.Arguments{"openid", "offline_access", "foo"}, + "", + }, + { + "ShouldGrantOriginalScopesWhenOmittedAfterNarrowing", + nil, + originalScopes, + "", + }, + { + "ShouldGrantOriginalScopesExplicitlyRequested", + originalScopes, + originalScopes, + "", + }, + { + "ShouldErrorWhenBroadeningScopesAllowedByClientButNotOriginallyGranted", + fosite.Arguments{"openid", "offline", "offline_access", "foo", "bar", "baz"}, + fosite.Arguments{"openid", "offline", "offline_access", "foo", "bar"}, + "", + }, + }, + }, + } + + state := "1234567890" + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + ctx := context.Background() + + session := &defaultSession{ + DefaultSession: &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: "peter", + }, + Headers: &jwt.Headers{}, + Subject: "peter", + Username: "peteru", + }, + } + + fc := new(fosite.Config) + fc.GlobalSecret = []byte("some-secret-thats-random-some-secret-thats-random-") + fc.ScopeStrategy = fosite.ExactScopeStrategy + + s := compose.NewOAuth2HMACStrategy(fc) + + var f fosite.OAuth2Provider + + if scenario.ignore { + keyGetter := func(context.Context) (interface{}, error) { + return gen.MustRSAKey(), nil + } + + // OAuth2RefreshTokenGrantFactory creates an OAuth2 refresh grant handler and registers + // an access token, refresh token and authorize code validator.nmj + factoryRefresh := func(config fosite.Configurator, storage interface{}, strategy interface{}) interface{} { + return &hoauth2.RefreshTokenGrantHandler{ + AccessTokenStrategy: strategy.(hoauth2.AccessTokenStrategy), + RefreshTokenStrategy: strategy.(hoauth2.RefreshTokenStrategy), + TokenRevocationStorage: storage.(hoauth2.TokenRevocationStorage), + Config: config, + IgnoreRequestedScopeNotInOriginalGrant: true, + } + } + + f = compose.Compose( + fc, + fositeStore, + &compose.CommonStrategy{ + CoreStrategy: compose.NewOAuth2HMACStrategy(fc), + OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(keyGetter, fc), + Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter}, + }, + compose.OAuth2AuthorizeExplicitFactory, + compose.OAuth2AuthorizeImplicitFactory, + compose.OAuth2ClientCredentialsGrantFactory, + factoryRefresh, + compose.OAuth2ResourceOwnerPasswordCredentialsFactory, + compose.RFC7523AssertionGrantFactory, + + compose.OpenIDConnectExplicitFactory, + compose.OpenIDConnectImplicitFactory, + compose.OpenIDConnectHybridFactory, + compose.OpenIDConnectRefreshFactory, + + compose.OAuth2TokenIntrospectionFactory, + compose.OAuth2TokenRevocationFactory, + + compose.OAuth2PKCEFactory, + compose.PushedAuthorizeHandlerFactory, + ) + } else { + f = compose.ComposeAllEnabled(fc, fositeStore, gen.MustRSAKey()) + } + + ts := mockServer(t, f, session) + defer ts.Close() + + client := newOAuth2Client(ts) + client.Scopes = []string{"openid", "offline", "offline_access", "foo", "bar"} + client.ClientID = "grant-all-requested-scopes-client" + + testRefreshingClient := &fosite.DefaultClient{ + ID: "grant-all-requested-scopes-client", + Secret: []byte(`$2a$10$IxMdI6d.LIRZPpSfEwNoeu4rY3FhDREsxFJXikcgdRRAStxUlsuEO`), // = "foobar" + RedirectURIs: []string{ts.URL + "/callback"}, + ResponseTypes: []string{"code"}, + GrantTypes: []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}, + Scopes: []string{"openid", "offline_access", "offline", "foo", "bar", "baz"}, + Audience: []string{"https://www.ory.sh/api"}, + } + + fositeStore.Clients["grant-all-requested-scopes-client"] = testRefreshingClient + + entries := make([]step, len(scenario.testCases)+1) + + resp, err := http.Get(client.AuthCodeURL(state)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + entries[0].OAuth2, err = client.Exchange(ctx, resp.Request.URL.Query().Get("code"), oauth2.SetAuthURLParam("client_id", client.ClientID)) + + require.NoError(t, err) + require.NotEmpty(t, entries[0].OAuth2.AccessToken) + require.NotEmpty(t, entries[0].OAuth2.RefreshToken) + + assert.Equal(t, strings.Join(originalScopes, " "), entries[0].OAuth2.Extra("scope")) + + entries[0].SessionAT, err = fositeStore.GetAccessTokenSession(ctx, s.AccessTokenSignature(ctx, entries[0].OAuth2.AccessToken), nil) + require.NoError(t, err) + + entries[0].SessionRT, err = fositeStore.GetRefreshTokenSession(ctx, s.RefreshTokenSignature(ctx, entries[0].OAuth2.RefreshToken), nil) + require.NoError(t, err) + + assert.ElementsMatch(t, entries[0].SessionAT.GetRequestedScopes(), originalScopes) + assert.ElementsMatch(t, entries[0].SessionRT.GetRequestedScopes(), originalScopes) + assert.ElementsMatch(t, entries[0].SessionAT.GetGrantedScopes(), originalScopes) + assert.ElementsMatch(t, entries[0].SessionRT.GetGrantedScopes(), originalScopes) + assert.Equal(t, strings.Join(originalScopes, " "), entries[0].OAuth2.Extra("scope")) + + for i, tc := range scenario.testCases { + t.Run(tc.name, func(t *testing.T) { + if scenario.checkTime { + time.Sleep(time.Second) + } + + idx := i + 1 + + opts := []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("refresh_token", entries[i].OAuth2.RefreshToken), + oauth2.SetAuthURLParam("grant_type", "refresh_token"), + } + + if len(tc.scopes) != 0 { + opts = append(opts, oauth2.SetAuthURLParam("scope", strings.Join(tc.scopes, " ")), oauth2.SetAuthURLParam("client_id", client.ClientID)) + } + + entries[idx].OAuth2, err = client.Exchange(ctx, "", opts...) + if len(tc.err) != 0 { + require.Error(t, err) + require.Nil(t, entries[idx].OAuth2) + require.Contains(t, err.Error(), tc.err) + + return + } + + require.NoError(t, err) + require.NotEmpty(t, entries[idx].OAuth2.AccessToken) + require.NotEmpty(t, entries[idx].OAuth2.RefreshToken) + + entries[idx].SessionAT, err = fositeStore.GetAccessTokenSession(ctx, s.AccessTokenSignature(ctx, entries[idx].OAuth2.AccessToken), nil) + require.NoError(t, err) + + entries[idx].SessionRT, err = fositeStore.GetRefreshTokenSession(ctx, s.RefreshTokenSignature(ctx, entries[idx].OAuth2.RefreshToken), nil) + require.NoError(t, err) + + if len(tc.scopes) != 0 { + assert.ElementsMatch(t, entries[idx].SessionAT.GetRequestedScopes(), tc.scopes) + assert.Equal(t, strings.Join(tc.expected, " "), entries[idx].OAuth2.Extra("scope")) + } else { + assert.ElementsMatch(t, entries[idx].SessionAT.GetRequestedScopes(), originalScopes) + assert.Equal(t, strings.Join(originalScopes, " "), entries[idx].OAuth2.Extra("scope")) + } + assert.ElementsMatch(t, entries[idx].SessionAT.GetGrantedScopes(), tc.expected) + assert.ElementsMatch(t, entries[idx].SessionRT.GetRequestedScopes(), originalScopes) + assert.ElementsMatch(t, entries[idx].SessionRT.GetGrantedScopes(), originalScopes) + + var ( + j int + entry step + ) + + assert.Equal(t, entries[idx].SessionAT.GetID(), entries[idx].SessionRT.GetID()) + + for j, entry = range entries { + if j == idx { + break + } + + assert.Equal(t, entries[idx].SessionAT.GetID(), entry.SessionAT.GetID()) + assert.Equal(t, entries[idx].SessionAT.GetID(), entry.SessionRT.GetID()) + assert.Equal(t, entries[idx].SessionRT.GetID(), entry.SessionAT.GetID()) + assert.Equal(t, entries[idx].SessionRT.GetID(), entry.SessionRT.GetID()) + + if scenario.checkTime { + assert.Greater(t, entries[idx].SessionAT.GetSession().GetExpiresAt(fosite.AccessToken).Unix(), entry.SessionAT.GetSession().GetExpiresAt(fosite.AccessToken).Unix()) + assert.Greater(t, entries[idx].SessionRT.GetSession().GetExpiresAt(fosite.RefreshToken).Unix(), entry.SessionRT.GetSession().GetExpiresAt(fosite.RefreshToken).Unix()) + assert.Greater(t, entries[idx].SessionAT.GetRequestedAt().Unix(), entry.SessionAT.GetRequestedAt().Unix()) + assert.Greater(t, entries[idx].SessionRT.GetRequestedAt().Unix(), entry.SessionRT.GetRequestedAt().Unix()) + } + } + }) + } + }) + } +} diff --git a/oauth2.go b/oauth2.go index 0827b8ed..b1fded68 100644 --- a/oauth2.go +++ b/oauth2.go @@ -176,7 +176,7 @@ type IntrospectionResponder interface { // IsActive returns true if the introspected token is active and false otherwise. IsActive() bool - // AccessRequester returns nil when IsActive() is false and the original access request object otherwise. + // GetAccessRequester returns nil when IsActive() is false and the original access request object otherwise. GetAccessRequester() AccessRequester // GetTokenUse optionally returns the type of the token that was introspected. This could be "access_token", "refresh_token", @@ -217,7 +217,7 @@ type Requester interface { // AppendRequestedScope appends a scope to the request. AppendRequestedScope(scope string) - // GetGrantScopes returns all granted scopes. + // GetGrantedScopes returns all granted scopes. GetGrantedScopes() (grantedScopes Arguments) // GetGrantedAudience returns all granted audiences. @@ -245,9 +245,19 @@ type Requester interface { Sanitize(allowedParameters []string) Requester } +// RefreshTokenAccessRequester is an extended AccessRequester implementation that allows preserving +// the original Requester. +type RefreshTokenAccessRequester interface { + // SanitizeRestoreRefreshTokenOriginalRequester returns a sanitized copy of this Requester and mutates the relevant + // values from the provided Requester which is the original refresh token session Requester. + SanitizeRestoreRefreshTokenOriginalRequester(requester Requester) Requester + + AccessRequester +} + // AccessRequester is a token endpoint's request context. type AccessRequester interface { - // GetGrantType returns the requests grant type. + // GetGrantTypes returns the requests grant type. GetGrantTypes() (grantTypes Arguments) Requester @@ -305,7 +315,7 @@ type AccessResponder interface { // SetTokenType set's the responses mandatory token type SetTokenType(tokenType string) - // SetAccessToken returns the responses access token. + // GetAccessToken returns the responses access token. GetAccessToken() (token string) // GetTokenType returns the responses token type.