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

Access token response is not compliant with OAuth 2.0 (RFC 6749) #660

Open
1 of 2 tasks
isegura-eos-eng opened this issue Oct 4, 2024 · 6 comments
Open
1 of 2 tasks
Labels

Comments

@isegura-eos-eng
Copy link

isegura-eos-eng commented Oct 4, 2024

Preflight Checklist

  • I could not find a solution in the existing issues, docs, nor discussions
  • I have joined the ZITADEL chat

Describe your problem

In the RFC 6749 document describing the Access Token scope request parameter it indicates the following:

If the issued access token scope is different from the one requested by the client, the authorization server MUST include the "scope" response parameter to inform the client of the actual scope granted.

I've implemented both authorization_code and client_credentials in my program with the op.Storage interface. However, when I supply the scope parameter in the token endpoint the response does not include the required parameter.

I think I don't need to supply my code because it's easy to see that oidc.AccessTokenResponse does not include the scope parameter:

oidc/pkg/oidc/token.go

Lines 232 to 239 in 97d7b28

type AccessTokenResponse struct {
AccessToken string `json:"access_token,omitempty" schema:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty" schema:"token_type,omitempty"`
RefreshToken string `json:"refresh_token,omitempty" schema:"refresh_token,omitempty"`
ExpiresIn uint64 `json:"expires_in,omitempty" schema:"expires_in,omitempty"`
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
State string `json:"state,omitempty" schema:"state,omitempty"`
}

Describe your ideal solution

Be compliant with OAuth 2.0 in this case. Whenever the scope parameter is sent to the token endpoint, check whether the returned scope matches the request. Or return the scope always if that is easier, which complies also with the rule.

Version

3.30.0

Environment

Self-hosted

Additional Context

I'm not sure if I this filed this issue correctly. In the OpenID spec, in the token endpoint section indicates to follow "Section 3.2" of RFC 6749. Clearly, 3.3 is not mentioned. Should 3.3 be included?

Aside of this, I think is very helpful to obtain the actual scope given in the access token response. I may have missed if there is other way to achieve this, so please forgive me and educate me if this is the case.

Edit: oidc.AccessTokenResponse permalink to show all lines of the struct.

@muhlemmer muhlemmer added the auth label Oct 4, 2024
@muhlemmer
Copy link
Collaborator

@isegura-eos-eng Yes, you are right it should't take much to add it to the struct.

@livio-a this feels like a Deja Vu. Wasn't there already an issue, fix or discussion somewhere?

@muhlemmer
Copy link
Collaborator

Found it: zitadel/zitadel#6609

At the time someone volunteered for a fix, but went silent. @isegura-eos-eng do you want to send a PR for this?

@isegura-eos-eng
Copy link
Author

@muhlemmer I could try. I've looked at the code a little bit, already. There are four places where this response must be returned, assuming this is the only struct used for returning the Access Token response. I think this can be very easy or the opposite. Depending on whether the AuthRequest and TokenRequest (or other similar signature structs) return the actual scopes that are granted or just the ones requested by the client. Maybe someone that knows better the implementation could tell me.

If it's the first case, then is pretty straight-forward, as we can just get the actual scopes from there and into the response. As I explained in the issue, we don't really need to know if the scopes granted differ from the ones requested, although is not very elegant, this would comply with the spec (maybe enough for now). The elegant way to do it is compare whether the scopes are different form the ones requested, as it's written by the RFC.

If it's the second case, which I suspect it is. Then we need to get the information there. And looking at the code, I don't see a clear way of accessing this information. Does anyone have an idea how to make this change compatible with the API if this is the case?

@muhlemmer
Copy link
Collaborator

@isegura-eos-eng validation of scope is done early in the Authorize handler. The invalid scopes are silently dropped.

oidc/pkg/op/auth_request.go

Lines 268 to 286 in 5ae555e

// ValidateAuthReqScopes validates the passed scopes and deletes any unsupported scopes.
// An error is returned if scopes is empty.
func ValidateAuthReqScopes(client Client, scopes []string) ([]string, error) {
if len(scopes) == 0 {
return nil, oidc.ErrInvalidRequest().
WithDescription("The scope of your request is missing. Please ensure some scopes are requested. " +
"If you have any questions, you may contact the administrator of the application.")
}
scopes = slices.DeleteFunc(scopes, func(scope string) bool {
return !(scope == oidc.ScopeOpenID ||
scope == oidc.ScopeProfile ||
scope == oidc.ScopeEmail ||
scope == oidc.ScopePhone ||
scope == oidc.ScopeAddress ||
scope == oidc.ScopeOfflineAccess) &&
!client.IsScopeAllowed(scope)
})
return scopes, nil
}

The token endpoint has no access to the originally requested scope. For this fix I would just always return scope, which is not forbidden by the standard and fulfills the MUST requirement on different scopes.

@isegura-eos-eng
Copy link
Author

isegura-eos-eng commented Oct 9, 2024

Then it's pretty straight forward. I've never done a PR in this project, any tips @muhlemmer ?

And another question: To build the scopes I have to do: strings.Join(" ", scopes), nothing fancy. However, this uses the standard package strings. I noticed there is another package strings in this project fighting over the same name. Also, I see the only function that this package implements (and tests) is the same functionality that slices.Contains implements, but just for strings. Since the project uses Go +1.21, I would suggest getting rid of the code in favor of using the standard library. Maybe there is another reason to keep it there, is there? This would be outside the scope of the PR, do you usually work this small refactors in other issues?

@muhlemmer
Copy link
Collaborator

Then it's pretty straight forward. I've never done a PR in this project, any tips @muhlemmer ?

Contribution guidelines are here: https://github.com/zitadel/oidc/blob/main/CONTRIBUTING.md

And another question: To build the scopes I have to do: strings.Join(" ", scopes), nothing fancy.

You can use the oidc.SpaceDelimitedArray type in the struct and that should take care of proper marshalling the field in the JSON response.

I noticed there is another package strings in this project fighting over the same name. Also, I see the only function that this package implements (and tests) is the same functionality that slices.Contains implements, but just for strings. Since the project uses Go +1.21, I would suggest getting rid of the code in favor of using the standard library. Maybe there is another reason to keep it there, is there? This would be outside the scope of the PR, do you usually work this small refactors in other issues?

We are always welcoming other improvements! But please open a separate PR. It is good to change custom functions to newer standard functions, but take care we have a strict semver policy that does not allow us to remove exported functions under pkg. Unfortunately there are many functions which never should have been exported and should have been in internal. There is nothing we can do about that now. Every once in a while we do a major release where we can do such breaking changes, for that you can open a PR to the next branch.

Thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: 🐛 Bugs/Small Issues
Development

No branches or pull requests

2 participants