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

Scopes are not added to (client credentials) access token #1202

Closed
robertkhou opened this issue Jan 14, 2021 · 10 comments
Closed

Scopes are not added to (client credentials) access token #1202

robertkhou opened this issue Jan 14, 2021 · 10 comments

Comments

@robertkhou
Copy link

robertkhou commented Jan 14, 2021

Openiddict.AspNetCore (3.0.0-beta6.20527.75)

How to reproduce issue

  1. Add client application to openiddict server and add a few custom scopes.
  2. Fetch the (client credentials) access token, by calling /connect/token
curl --location --request POST 'https://localhost:5000/connect/token' \
--header 'Authorization: Basic myclientid:myclientsecret' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=myapp' \
--data-urlencode 'scope=account account.read account.write'
  1. Call a web api using the access token, and debug the claims, look for scopes (scopes are missing, no scopes are added to the access token by default?)
@kevinchalet
Copy link
Member

Hi,

image

As specified, the ability to post questions on GitHub is reserved to sponsors as a way to thank them. Please consider sponsoring the project or post your question on StackOverflow, where it will be visible by the community.

Thanks.

@robertkhou
Copy link
Author

Sponsored.

What do you mean by this...("Scopes are automatically added by OpenIddict in all access tokens." https://gitter.im/openiddict/openiddict-core?at=5ff487b1acd1e516f8d54f0b)

Under "/connect/token" Exchange web api method, I found the reason why no scopes (standard or custom scopes) are not added to access token, because there is no code that adds the scopes to the access token. Am I missing something here? I need to add all claims I need manually? (scopes, aud etc)

@kevinchalet
Copy link
Member

Sponsored.

Thanks for that! ❤️

What do you mean by this...("Scopes are automatically added by OpenIddict in all access tokens." https://gitter.im/openiddict/openiddict-core?at=5ff487b1acd1e516f8d54f0b)

All tokens are derived from a single ClaimsPrincipal instance, that represents the source of truth: OpenIddict will take care of adding the standard-compliant scope claim - defined in https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04#section-2.2.3 - to the access tokens it produces but you'll first need to tell it what scopes were actually granted by calling principal.SetScopes(...) (that adds oi_scp private claims under the hood): this can exactly match the requested scopes (e.g principal.SetScopes(request.GetScopes())), be just a portion of them or completely arbitrary scopes that were not even requested.

Hope that's clearer.

@kevinchalet
Copy link
Member

I opened openiddict/openiddict-documentation#28 to make sure I don't forget this point when I'll add the scopes/resources docs.

@robertkhou
Copy link
Author

robertkhou commented Jan 14, 2021

Sponsored.

Thanks for that! ❤️

What do you mean by this...("Scopes are automatically added by OpenIddict in all access tokens." https://gitter.im/openiddict/openiddict-core?at=5ff487b1acd1e516f8d54f0b)

All tokens are derived from a single ClaimsPrincipal instance, that represents the source of truth: OpenIddict will take care of adding the standard-compliant scope claim - defined in https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04#section-2.2.3 - to the access tokens it produces but you'll first need to tell it what scopes were actually granted by calling principal.SetScopes(...) (that adds oi_scp private claims under the hood): this can exactly match the requested scopes (e.g principal.SetScopes(request.GetScopes())), be just a portion of them or completely arbitrary scopes that were not even requested.

Hope that's clearer.

Thanks, everything works now!

by the way, principal.SetScopes(request.GetScopes()), this actually adds each scope as a new claim, is it suppose to do this? or is it suppose to add each scope via space separated value under a single oi_scp claim?

@kevinchalet
Copy link
Member

That's the expected behavior (and pretty much the reason why OpenIddict uses its own private scopes claims: we don't have to split a unique scope value each time we want to work with them)

@drsmile1001
Copy link

I have similar problem.
I use ClientCredentialsGrantType of samples/Aridka as my project base. And I found It don't add "scope" to claim.

I wondering why disscution above talking about principal.SetScopes(request.GetScopes()), but ClientCredentials seems don't have "user" like other "Grant" (ex: AuthorizationCodeGrantType).
samples/Aridka just simplily use identity.AddClaim to add "sub", "name" to AccessToken and IdentityToken.

So I change code block to below:

if (request.IsClientCredentialsGrantType())
{
    var application = await _applicationManager.FindByClientIdAsync(request.ClientId ?? "")
    ?? throw new InvalidOperationException("The application cannot be found.");

    var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType,
        Claims.Name, Claims.Role);

    identity.AddClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application) ?? "",
            Destinations.AccessToken, Destinations.IdentityToken);

    identity.AddClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application) ?? "",
        Destinations.AccessToken, Destinations.IdentityToken);
        
    //add below
    foreach (var scope in request.GetScopes())
    {
        identity.AddClaim(Claims.Scope, scope, Destinations.AccessToken, Destinations.IdentityToken);
    }
    //add above
    
    return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}

And it seem to solve no scope claim in access token.

@hypdeb
Copy link

hypdeb commented Nov 11, 2022

Hello @kevinchalet,

I faced the same issue as @drsmile1001 and solved it the same way. However, I simply do not understand why this works and it feels hacky. What I would expect is that:

identity.SetScopes(request.GetScopes());

and

foreach (var scope in request.GetScopes())
{
    identity.AddClaim(Claims.Scope, scope);
}

are doing the same thing, but evidently they don't. From the code, I can see that they use the type Claims.Private.Scope. Is that the difference ? Why is it using this Private type instead of the standard scope claim type ?

I sponsored and will gladly use one of my yearly support tickets to understand what's going on :)

Also, great work on this library. I enjoy working with it much more than I did with IdentityServer.

@kevinchalet
Copy link
Member

kevinchalet commented Nov 12, 2022

Hey @hypdeb,

I sponsored and will gladly use one of my yearly support tickets to understand what's going on :)

Great, thanks for that! 😃

the standard scope claim type ?

It's important to note that the standard scope claim actually only became a standard thing in October 2021 with the JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens specification. And as the name implies, it's only a standard claim for JWT access tokens, not for all types of JWT tokens (OpenIddict itself uses JWT for all its tokens, including authorization codes and refresh tokens).

To understand why I've opted for private/internal claims, a bit of history is needed:

  • ASOS and OpenIddict 1.x/2.x used ASP.NET Core Data Protection as the default token format and stored protocol-oriented stuff like code_challenge or the granted scopes in AuthenticationProperties rather than as claims in ClaimsPrincipal. This was a clean model, already used in Katana's OAuthAuthorizationMiddleware, from which ASOS was forked.

  • One of the main objectives of OpenIddict 3.x was to decouple the server and validation packages from ASP.NET Core to make them host-agnostic and introduce native compatibility with OWIN/Katana. For this reason, 3.x started using JWT as the default token format for all tokens and had to abandon AuthenticationProperties since it was an OWIN/ASP.NET Core-specific type (using it in the host-agnostic packages wasn't practical). Since then, ClaimsPrincipal has become the unique source of truth.

Of course, I could have used the generic scope name to represent the granted scopes, but decided against that for two reasons:

  • While OpenIddict 3.x uses JWT as the default token format and has companion packages for ASP.NET Core Data Protection, you're actually free to add support for different token formats, custom or standard. Yet, if OpenIddict massively used names coming from the JWT world in say, SAML-formatted refresh tokens, it would make things super weird. By using its own set of claims, OpenIddict avoids tying its tokens to a specific format (should I dare it's format-agnostic? 😄).

  • Using private claims helps make a clearer separation between "user claims" and "protocol claims".

are doing the same thing, but evidently they don't.

As you figured out, OpenIddict expects that you use the private scopes to represent the granted scopes. If you use JWT access tokens (the default format), it will end up returning a standard scope space-separated claim to be compatible with resource servers that accept RFC9068 tokens, but the private scope claims are the only way to tell it: "hey, I granted these scopes".

Also, it's worth noting that the standard scope claim defined in RFC9068 MUST be represented as a unique claim of space-separated values, which differs from the format projects like OpenIddict and IdSrv had adopted for their "public scope" claim (both projects used JSON arrays).

TL;DR: if you really wanted to use foreach instead of the extension, you'd need to use the private name (OpenIddict private scopes are represented as multiple claims instead of the much less space-separated format used for the public scope):

foreach (var scope in request.GetScopes())
{
    identity.AddClaim(Claims.Private.Scope, scope);
}

Not an easy topic, but I hope it made things (a bit) clearer 😄

@hypdeb
Copy link

hypdeb commented Nov 26, 2022

Thank you for the answer. I won't pretend I understood all of it, but I now know what I have to do with more confidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants