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

Audience value changes when adding offline_access #415

Closed
andrewclymer opened this issue Dec 22, 2022 · 16 comments
Closed

Audience value changes when adding offline_access #415

andrewclymer opened this issue Dec 22, 2022 · 16 comments

Comments

@andrewclymer
Copy link

Which version of Duende IdentityServer are you using?
6.5.1
Which version of .NET are you using?
.NET 6
Describe the bug
The Audience in the JWT changes when adding offline_access scope

To Reproduce

scope.325908 is a shared scope
Two resources have the scope
urn:repro/325908 and urn:repro/325908-2

Make a request with a resource indicator.

resource : urn:repro/325908
scopes : openid profile scope.325908

Resource is not configured to Require Resource Indicator

Get back the expected JWT

{
"iss": "https://localhost:63024",
"nbf": 1671711275,
"iat": 1671711275,
"exp": 1671714875,
"aud": "urn:repro/325908",
"scope": "openid profile scope.325908",
"amr": [
"external"
],
"client_id": "demo-client",
"sub": "600808ab-8a13-41ae-b1a2-6ee4cd95aa2d",
"auth_time": 1671711266,
"idp": "okta",
"sid": "C27D158C4F665B6E50DD90F0AA043151",
"jti": "6F36E0729091B03F0EBBFEBC082BBA4E"
}

Repeat the request but adding offline_access scope,

resource : urn:repro/325908
scopes : openid profile scope.325908 offline_access

and now the audience contains both resources associated with the shared scope.

{
"iss": "https://localhost:63024",
"nbf": 1671711016,
"iat": 1671711016,
"exp": 1671714616,
"aud": [
"urn:repro/325908",
"urn:repro/325908-2"
],
"scope": "openid profile scope.325908 offline_access",
"amr": [
"external"
],
"client_id": "demo-client",
"sub": "600808ab-8a13-41ae-b1a2-6ee4cd95aa2d",
"auth_time": 1671710838,
"idp": "okta",
"sid": "8252ABEA0C36CF27541D873F8CA4CA63",
"jti": "75C60658C99E4D45B5AB622EE5DBF66F"
}

Expected behavior

Expected the audience to not change by adding offline_access

@josephdecock
Copy link
Member

Again, thanks for a bug report. But again, I'm unfortunately unable to reproduce this.

@GavinOsborn
Copy link

G'day @josephdecock, long time no speak 👋.
I have some skin in this game, so allow me to help expedite the investigation.

I was able to reproduce the behavior using demo.duendesoftware.com.
Client Id: interactive.confidential
Response Type: code
Response Mode: fragment
Resource: urn:resource1
Scope: openid profile shared.scope offline_access

Observe that the access token produced has an audience claim of ["urn:resource1", "urn:resource2"].
If you remove the offline_access scope and do this all over again you'll notice the audience claim is now simply "urn:resource1".

I have included har files that capture each flow in this zip file

I hope that helps

Gav

@andrewclymer
Copy link
Author

Hi Joseph

Any update on this issue, are you able to reproduce with Gavin's reproduce steps?

All the best

Andy

@josephdecock
Copy link
Member

Hi, good to hear from you all! Thanks for the really detailed repro steps. Using the HAR file I was able to see that your request to the authorize endpoint is including the resource indicator, but the subsequent request to the token endpoint is not. If you add the resource parameter to the token request as well, you should get the expected audience.

@andrewclymer
Copy link
Author

Hi Joseph

Thanks didn't spot that, so I guess you are hinting that this is a bug in the OIDC client library?

or you are expected to add code like

options.Events.OnAuthorizationCodeReceived = context =>
{
 context.TokenEndpointRequest.Resource = context.Protocol;
};

When exchanging an auth code for a token, I would have expected that the token you get back is the one described in the authorization request. That certainly is the case if you don't mention offline_access. Why is IDS behavior different when offline_access scope is supplied?

All the best

Andy

@josephdecock
Copy link
Member

Typically if you're using resources it make sense to include the resource in both the authorization and token request. That's the way RFC 8707 does it in their examples.

this is a bug in the OIDC client library?

Which library?

or you are expected to add code like
options.Events.OnAuthorizationCodeReceived = context =>
{
 context.TokenEndpointRequest.Resource = context.Protocol;
};

Yeah, that's what I would expect generally

When exchanging an auth code for a token, I would have expected that the token you get back is the one described in the authorization request.

The auth request is more "general" then that. It doesn't (at its most general) describe a single access token. Rather, it grants authorization for all the possible parameters that you might ask for when you actually ask for a token. It's actually possible to get multiple different tokens with different audiences, all based on this one initial grant.

Why is IDS behavior different when offline_access scope is supplied?

There's a special workflow for obtaining multiple tokens with different audiences where you use the refresh token to call the token endpoint and get those different tokens. I suspect that workflow is involved here, and I'm going to do a bit more investigation to make sure that this is working as designed.

@GavinOsborn
Copy link

GavinOsborn commented Jan 6, 2023

Hi, good to hear from you all! Thanks for the really detailed repro steps. Using the HAR file I was able to see that your request to the authorize endpoint is including the resource indicator, but the subsequent request to the token endpoint is not. If you add the resource parameter to the token request as well, you should get the expected audience.

So, I tested this behavior and can confirm that this does successfully correct the aud value but the scopes issued are now different.

Before: openid profile shared.scope offline_access
Now: shared.scope offline_access

However, I think this behavior makes sense. I asked for a token for the purpose of interacting with urn:resource1 and so the token is constrained accordingly 👍. The one unfortunate downside is that I'm now prohibited from accessing the /userinfo endpoint - I'm not sure how to now solve for that❔.

With all of that said though I still believe the originally reported behavior is very problematic.
The original ./authorize request described seeks authorization (and perhaps even explicit user consent?) to just one resource urn:resource1. It seems materially wrong that tokens produced from this flow would ever have greater privilege than for which authorization was originally granted - especially when refresh tokens are involved!

I think the RFC that you mentioned even says as much.

To the extent possible, when issuing access tokens, the authorization server should downscope the scope value associated with an access token to the value the respective resource is able to process and needs to know. (Here, "downscope" means give fewer permissions than originally authorized by the resource owner.)

Acknowledging that multiple resources can be requested yet urn:resource2 was never actually requested in the original authorization should the client application subsequently be issued a token for it? In the absence of a resource parameter in the token request would it not make more sense to limit the aud to those initially requested?

@andrewclymer
Copy link
Author

Hi Joe

Any news on your investigation?

All the best

Andy

@josephdecock
Copy link
Member

@GavinOsborn - we're thinking along the same lines, but also trying to make sure that any changes we consider aren't too disruptive
@andrewclymer - nothing else to report yet, but Brock and I are both looking into this.

@andrewclymer
Copy link
Author

Hi Joe
Thanks for the update

@josephdecock
Copy link
Member

Resource indicators are layered on top of scopes, which remain the mechanism for specifying the scope of access. While ApiResource definitions in your IdentityServer configuration include a collection of scopes, those scopes aren't owned by or isolated to the ApiResource. The ApiResource merely supports those scopes; it does not have exclusive use of them. You could have other ApiResources that also use those scopes, and there could even be an API that isn't represented as an ApiResource in the IdentityServer configuration system yet still is authorized using tokens from IdentityServer with those scopes. Furthermore, there's no relationship in the authorize request between scopes and resources. Both are flat lists, so there's no way to express an authorization request for scope x at resource y.

Instead, the request to the authorize endpoint is setting up the possible requests at the token endpoint. The token endpoint will accept one of the originally requested resources or no resource indicator at all.

If you request a resource, then IdentityServer will create a token with scopes filtered to what is supported by that resource, and with that resource as the audience. An implication of this filtering is that, when you request a resource at the token endpoint, you get a token that can't be used at the userinfo endpoint. The userinfo endpoint requires the profile scope and/or other identity resource scopes, which aren't supported by an ApiResource. Thus, the tokens produced by requesting a resource won't have the identity scopes needed at the userinfo endpoint.

The userinfo endpoint is a resource in the sense of being an OAuth Protected Resource, but there's no appropriate resource parameter to send when making requests to it. Thus, to make requests to the userinfo endpoint, you need to request an access token without passing any resource to the token endpoint. When you do that, you get the standard behavior of IdentityServer, which is to produce a token with all the originally requested scopes. This will include profile, etc, if they were originally requested at the authorize endpoint, thus producing a token that can be used at the userinfo endpoint.

An implication of the need to support userinfo calls as well as calls to ApiResources, is that almost everyone who is using resource indicators actually needs multiple tokens, even if there's only 1 ApiResource. The way that you obtain multiple tokens after a single authorize call is by using a refresh token. Your initial request to authorize includes the offline_access scope, all the scopes at all the resources you might need, and all the resources you want tokens for. Then the client can obtain tokens that are specific to those ApiResources by making requests to the token endpoint using the refresh grant, and specifying the different resource parameters.

As we were testing this, we did discover that we seem to have a bug when offline_access is not requested. We're in the process of fixing that bug, but we think that in general you will need to be requesting offline_access anyway.

The other default behavior that happens when you don't specify a resource at the token endpoint is that IdentityServer will determine the audiences of the token based on the requested scopes. Any ApiResource that supports a requested scope and that was either requested at the authorize endpoint or configured to not require the resource indicator for isolation (more on this in a moment) will be included as an audience in the token. This can be surprising, because audiences might be included that weren't explicitly requested in the authorize request.

The cause of this behavior is the RequireResourceIndicator flag on the ApiResource definition. Turing this flag on means that the ApiResource name will only ever appear as an audience when it is explicitly requested with the resource parameter. When the flag is off, the ApiResource will be added as an audience whenever any of its supported scopes are included in tokens. Resources with the flag on are referred to as isolated resources; they're isolated in the sense that tokens with the isolated resource as their audience won't have other resources as additional audiences.

The default behavior of IdentityServer is that RequireResourceIndicator is off. Resource indicators are a newer specification, and thus their use is opt-in. While IdentityServer does support configuring a mixture of isolated and non-isolated resources, in general our guidance is to try to avoid that mixture. If you opt-in to using resource indicators, the ideal is to use them everywhere and to isolate all of your ApiResources. Configuring your ApiResources in this way should give you the behavior that you're expecting.

@GavinOsborn
Copy link

Hey Joe, thank you for the excellent and thoughtful write-up.

There are a couple of lenses through which I'd like to reply, selfishly I'm going to focus my own immediate project-centric needs first :)

Resource Isolation
So, this is very promising. It fits our use-case very well whereby authorization may be granted for n ApiResources but tokens are only issued for one ApiResource at a time. In theory we can definitely work with this going forward.

I'm aware of an existing issue w/regards to ASP.NET OIDC middleware accepting multiple resource parameters. But I note the workaround listed in thread and hope🙏 that promised new middlware lands us in a better place all round.

So I believe we may now be able to solve for our use-case. Thank you.

@GavinOsborn
Copy link

Priviledge Escalation
Philosophically speaking though I can't really get behind the idea that the presence of the offline_access scope should ever elevate the level of priviledge granted to the client - it should only augment the client with ongoing yet revokable, access with the same level of priviledge as without offline_access.

Resource indicators are layered on top of scopes, which remain the mechanism for specifying the scope of access...

Your point here is well made and may or may not stand-up to scrutiny from a purist point of view - I don't mind admitting I'm unsure. But if we allow ourselves to assume (not unreasonably) that resource servers use the aud claim as one of several points of inbound validation before authorizing client operations... then the priviledge granted with a the offline_access scope is demonstrably higher than without.

Agree to disagree?

@brockallen
Copy link
Member

brockallen commented Jan 18, 2023

So, this is very promising. It fits our use-case very well whereby authorization may be granted for n ApiResources but tokens are only issued for one ApiResource at a time. In theory we can definitely work with this going forward.

Joe said all of this already, to be fair, so sorry for the redundancy:

One problem with resource indicators is that on the authorize request the list of scopes and resources is flat, and there's no way in that request to say that you want scope1 only for resource1, and scope2 for resource2. And in our configuration system, ApiResources don't "own" ApiScopes -- they only indicate the ones they support. This is needed because often you have scopes that needs to be used at different resources, and maybe not all of them require isolation.

So the scopes being requested is still the scopes a client is being granted. The resource isolation feature doesn't restrict the access that the client has. Resource isolation just ensures that an access token obtained can be resource-constrained so that the resource server can't abuse or misuse the access token presented to it. Or another use is if there's resource specific processing needed on the access token (e.g. encryption).

Were you wanting it to work differently?

Philosophically speaking though I can't really get behind the idea that the presence of the offline_access scope should ever elevate the level of priviledge granted to the client - it should only augment the client with ongoing yet revokable, access with the same level of priviledge as without offline_access

Correct, and I don't see that IdentityServer allows for escalation of access when using the refresh token. But if you don't request offline_access, then the token exchange only has one chance to obtain an access token, this it only has one chance to indicate the isolated resource. Thus, it's not practical if you need multiple access tokens for different audience constraints unless you obtain one.

Or am I misunderstanding?

@brockallen
Copy link
Member

brockallen commented Jan 18, 2023

Also, I suppose, the thing to keep in mind is that we're a framework, and our ApiResource concept supports two things: 1) allowing control of the "aud" claim in the access token based on the scopes requested (independent of resource indicators), and 2) allow resource isolation. And since these overlap, and since we also need to weave in the support for ~/userinfo, there might certainly be a tension from trying to use all of these together. In other words, it's hard to indicate with the resource param you do or do not want access to ~/userinfo and/or non-resource isolated resources.

I suspect, again what Joe already said, is that in your architecture if you really want resource isolation for everything, then it seems that you'd want to configure every ApiResource with RequireResourceIndicator=true.

The resource indicator spec is written from the OAuth-only mindset, and thus is under specified how this should work with OIDC (and ~/userinfo), and how the absence of the resource param should work on the token endpoint. This means that some things are left up to policy (or somesuch) in the token server for how things should work. We'd like to hope that, as a framework, you can configure things to make it work the way you want it to, so see how far setting RequireResourceIndicator gets you, and if there are other roadblocks please let us know.

@brockallen
Copy link
Member

We've merged the PR with the fix for this.

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