Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to request narrower scopes in OAuth2 Refresh Token Exchange #696

Open
3 of 6 tasks
silverspace opened this issue Aug 25, 2022 · 10 comments · May be fixed by #718
Open
3 of 6 tasks

Unable to request narrower scopes in OAuth2 Refresh Token Exchange #696

silverspace opened this issue Aug 25, 2022 · 10 comments · May be fixed by #718
Labels
bug Something is not working.

Comments

@silverspace
Copy link

silverspace commented Aug 25, 2022

Preflight checklist

Describe the bug

Hello!

I recently discovered that the Refresh Token Grant Handler does not currently honor the (optional) user requested scope parameter to request a subset of the originally granted scopes.

For reference, here is the code in question:

	for _, scope := range originalRequest.GetGrantedScopes() {
		if !c.Config.GetScopeStrategy(ctx)(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)
	}

The issues that I see with the current implementation are:

  1. The current implementation does not read the scope form param.
  2. The implementation currently compares the originally granted scopes against the current client scopes, but this endpoint should be completely agnostic of the client scopes. I think it should only depend on the originally granted scopes (and optionally the scope form param). The result is that the refresh token exchange will fail if the client scopes are themselves narrowed after a refresh token is created.

This is the implementation that I would propose, given my understanding of https://www.rfc-editor.org/rfc/rfc6749#section-6

diff --git a/handler/oauth2/flow_refresh.go b/handler/oauth2/flow_refresh.go
index 0e3be4d..642e774 100644
--- a/handler/oauth2/flow_refresh.go
+++ b/handler/oauth2/flow_refresh.go
@@ -96,11 +96,15 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex
        }
 
        request.SetSession(originalRequest.GetSession().Clone())
-       request.SetRequestedScopes(originalRequest.GetRequestedScopes())
+       if request.GetRequestForm().Has("scope") {
+               request.SetRequestedScopes(strings.Split(request.GetRequestForm().Get("scope"), " "))
+       } else {
+               request.SetRequestedScopes(originalRequest.GetGrantedScopes())
+       }
        request.SetRequestedAudience(originalRequest.GetRequestedAudience())
 
-       for _, scope := range originalRequest.GetGrantedScopes() {
-               if !c.Config.GetScopeStrategy(ctx)(request.GetClient().GetScopes(), scope) {
+       for _, scope := range request.GetRequestedScopes() {
+               if !c.Config.GetScopeStrategy(ctx)(originalRequest.GetGrantedScopes(), scope) {
                        return errorsx.WithStack(fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope))
                }
                request.GrantScope(scope)

Some pertinent notes from my reading of https://www.rfc-editor.org/rfc/rfc6749#section-6:

scope
         OPTIONAL.  The scope of the access request as described by
         [Section 3.3](https://www.rfc-editor.org/rfc/rfc6749#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.
  • This optional scope parameter should allow the client to request a narrower set of scopes than were originally granted by the resource owner.
  • The refresh token grant handler should be completely agnostic of the client scopes. It should only depend on the originally granted scopes, and optionally the scope form param.
  • I would expect this narrower set of scopes to apply only to this specific access token, and not to future refresh token requests. (This is referenced later in the RFC with "If a new refresh token is issued, the refresh token scope MUST be identical to that of the refresh token included by the client in the request.")

Reproducing the bug

The current unit tests reproduce this issue. For example:

=== RUN   TestRefreshFlow_HandleTokenEndpointRequest/strategy=hmac/case=should_pass
    flow_refresh_test.go:194: 
        	Error Trace:	flow_refresh_test.go:194
        	            				flow_refresh_test.go:356
        	Error:      	Not equal: 
        	            	expected: fosite.Arguments{"foo", "bar", "offline"}
        	            	actual  : fosite.Arguments{"foo", "offline"}
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1,4 +1,3 @@
        	            	-(fosite.Arguments) (len=3) {
        	            	+(fosite.Arguments) (len=2) {
        	            	  (string) (len=3) "foo",
        	            	- (string) (len=3) "bar",
        	            	  (string) (len=7) "offline"
        	Test:       	TestRefreshFlow_HandleTokenEndpointRequest/strategy=hmac/case=should_pass
        --- FAIL: TestRefreshFlow_HandleTokenEndpointRequest/strategy=hmac/case=should_pass (0.00s)


Expected :fosite.Arguments{"foo", "bar", "offline"}
Actual   :fosite.Arguments{"foo", "offline"}

Relevant log output

No response

Relevant configuration

No response

Version

master

On which operating system are you observing this issue?

No response

In which environment are you deploying?

No response

Additional Context

No response

@silverspace silverspace added the bug Something is not working. label Aug 25, 2022
@james-d-elliott
Copy link
Contributor

Your reading of the RFC seems correct to me. I can't speak officially on behalf of ory but generally this is something that provided there is a linked RFC, and you can produce a test that fails before your fix and passes after it will be accepted if you're interested in producing a PR. If not I'm happy to do so and credit you.

@silverspace
Copy link
Author

Sure, I'd be happy to throw together a PR with this fix. Thanks!

silverspace added a commit to silverspace/fosite that referenced this issue Aug 31, 2022
Fixing the OAuth2 token refresh handler to:
 - Read and use the optional 'scope' form parameter, if present.
 - Otherwise default to requesting the originally granted scopes.

This endpoint should be completely agnostic of:
 - The originally **requested** scopes
 - The **client scopes** (both current and past client scopes)

Fixes ory#696
silverspace added a commit to silverspace/fosite that referenced this issue Aug 31, 2022
Fixing the OAuth2 token refresh handler to:
 - Read and use the optional 'scope' form parameter, if present.
 - Otherwise default to requesting the originally granted scopes.

This endpoint should be completely agnostic of:
 - The originally **requested** scopes
 - The **client scopes** (both current and past client scopes)

Fixes ory#696
@aeneasr
Copy link
Member

aeneasr commented Nov 1, 2022

As discussed in the PR, the scope parameter should be respected in the refresh flow as well as the OAuth2 client scope :)

@silverspace
Copy link
Author

Just to clarify the above: Fosite does currently have a bug where it does not respect the scope form field in the refresh flow.

@james-d-elliott
Copy link
Contributor

james-d-elliott commented Nov 2, 2022

I've managed to wrangle together some actual proof the issue exists. The tests are very primitive but demonstrate the scope field is being ignored. I used the higher level functions to ensure one of the auxiliary handlers was not handling this case specifically as I'm not 100% certain of the interaction of some of the handlers (many are utilized in loops).

https://gist.github.com/james-d-elliott/1475671e805c9b4e2167c6c1b61f05f2

@james-d-elliott
Copy link
Contributor

james-d-elliott commented Nov 2, 2022

The following patch resolves said issue but breaks TestRefreshFlow_HandleTokenEndpointRequest (I'll look into why but it's probably due to my use of checking the requested scopes instead of granted scopes):

Index: handler/oauth2/flow_refresh.go
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/handler/oauth2/flow_refresh.go b/handler/oauth2/flow_refresh.go
--- a/handler/oauth2/flow_refresh.go	(revision 2d32d9ebe7d97181c9193292f7004133ae097f40)
+++ b/handler/oauth2/flow_refresh.go	(date 1667363251433)
@@ -96,13 +96,25 @@
 	}
 
 	request.SetSession(originalRequest.GetSession().Clone())
-	request.SetRequestedScopes(originalRequest.GetRequestedScopes())
+
+	switch scope := request.GetRequestForm().Get("scope"); scope {
+	case "":
+		request.SetRequestedScopes(originalRequest.GetGrantedScopes())
+	default:
+		request.SetRequestedScopes(fosite.RemoveEmpty(strings.Split(scope, " ")))
+	}
+
 	request.SetRequestedAudience(originalRequest.GetRequestedAudience())
 
-	for _, scope := range originalRequest.GetGrantedScopes() {
+	for _, scope := range request.GetRequestedScopes() {
+		if !originalRequest.GetGrantedScopes().Has(scope) {
+			return errorsx.WithStack(fosite.ErrInvalidScope.WithHintf("The requested scope '%s' is not allowed as it was not originally granted during the access request.", scope))
+		}
+
 		if !c.Config.GetScopeStrategy(ctx)(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)
 	}

@james-d-elliott james-d-elliott linked a pull request Nov 2, 2022 that will close this issue
6 tasks
@james-d-elliott
Copy link
Contributor

james-d-elliott commented Nov 4, 2022

So I think the original specific issue is solved by the changes. There is one particular point regarding the spec and what the text if omitted is treated as equal to the scope originally granted by the resource owner exactly means. The particular question is what originally granted means. Does it mean the first access response that granted the refresh token? Or does it mean the scope granted of the (potentially) narrower token?

@aeneasr
Copy link
Member

aeneasr commented Nov 4, 2022

The particular question is what originally granted means. Does it mean the first access response that granted the refresh token? Or does it mean the scope granted of the (potentially) narrower token?

The resource owner is not involved during token refresh and is not granting scopes there. He/she only grants scopes on the consent UI screen. Therefore, this has to imply the original request!

1 similar comment
@aeneasr
Copy link
Member

aeneasr commented Nov 4, 2022

The particular question is what originally granted means. Does it mean the first access response that granted the refresh token? Or does it mean the scope granted of the (potentially) narrower token?

The resource owner is not involved during token refresh and is not granting scopes there. He/she only grants scopes on the consent UI screen. Therefore, this has to imply the original request!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something is not working.
Projects
None yet
3 participants