As part of creating our new Advanced OAuth training, I created a whole lecture on the evolution of access tokens and resource access.
It’s fascinating – since the original OAuth 2.0 spec does not have any information about the token format, content or semantics – everybody kind of made up something that works for them (including us).
As you can see on the timeline, JWTs were not even a thing when OAuth 2.0 was released, and to be honest, also the JWT spec is just a framework for how a JWT might look like, and is not always the best natural match for OAuth semantics.
Still JWTs won because simplicity, and if you look at various vendors’ JWT format, you can see different interpretations of how OAuth might map to it.
Fast forwarding to 2020, our good friend Vittorio made an effort of trying to formalize JWTs for OAuth and has been working on an official profile. There have been discussions going back and forth, we are not always in agreement, but nevertheless the document will be released soon-ish.
Let’s have a look at the proposed JWT format, and how IdentityServer conforms to it.
One of the best features of the spec IMO is the introduction of a type header. This allows resource server to make sure they actually deal with an access token. The value is at+jwt and we support this for a while now – I wrote about it here.
iss (issuer) and exp (expiration) claim
These are pretty much no brainers and have been supported in IdentityServer since ever.
Represents the client ID of the OAuth client. Present since day 1 in IdentityServer.
jti (JWT identifier) claim
A unique identifier for the token. Allows implementing replay detection. This is a per-client setting in IdentityServer, but we changed the default value to emit jti in v4.
If the scope request parameter is used, the access token should contain the granted scopes as a claim. Makes total sense, but there might be some variations with the actual format. The spec says it must be a space delimited list. IdentityServer has historically been using a string array for that, because it played nicer with the .NET claims infrastructure.
As of v4 you can switch from the array format to the string format by setting the EmitScopesAsSpaceDelimitedStringInJwt option – be aware that this will probably break existing consumers.
aud (audience) claim
This is a more complicated story – but to make it short: pure OAuth has no concept of an audience. The closest thing is the scope parameter, which is spectacularly under-defined and more abstract. IOW – everyone came up with their own interpretation of that.
In IdentityServer3 we emitted a static audience claim, and we changed that in IdentityServer4 to use the name of the request API resource(s). We were not happy with both approaches.
In the new v4 we give you more control – you can set a static audience; you can omit the audience altogether – or you can use the API resource name (and this will become even more interesting when you mix in resource indicators). I have a more detailed post in the making to discuss the various options and will post it soon.
The JWT profile spec makes aud mandatory and I don’t fully agree with this decision. Either way, IdentityServer conforms to it, if you want to.
iat (issued_at) claim
The profile favours iat over nbf. The practical intent is similar enough. The .NET library prefers nbf historically and will emit both now in v4.
sub (subject identifier) claim
This is the really big elephant in the room.
Since we designed IdentityServer to be an OpenID Connect and OAuth 2.0 system (strong emphasis on the ‘and’) – we decided to use the OIDC definition of the sub claim for both identity and access tokens. The main motivation was to make it very explicit to every token consumer: the sub claim represents the unique identifier of an end-user. This also meant that, when no sub claim is present, there is no user involved – which e.g. would apply to a pure machine to machine communication.
This is where the JWT profile differs. It gives the sub claim a dual semantic. If an end-user is involved, the sub claim uses the OIDC definition (aka the user ID). If no end-user is involved, the sub claim represents the client ID of the OAuth client.
I personally (and tbh none of our customers) don’t like that ambiguity and the best explanation I have is, that this unifies the sub claim to be the target of authorization rules in the consumer (either a user or a client). But this is only what I came up with, to make a little bit more sense out of that. I had situations where this was actually useful – but I prefer the freedom to decide how I want to model that. The profile does not motivate this besides: sub is mandatory (which btw is not true according to the JWT specification).
In IdentityServer we always supported emitting static claims per client, so you can easily emit a static sub claim, and e.g. make that the same value as client_id.
The profile allows for other (user-centric) claims, e.g. auth_time, amr or acr. Again, this has always been the case in IdentityServer, and makes total sense.
It’s a lot of work to write and discuss a new specification – so thanks Vittorio for taking on that task. The main goal of the profile is to give us a common language when talking about access token content and semantics – and to help with interop.
Given the type of changes we had to make to IdentityServer to conform with it (and I suppose we are one of the more agile OAuth implementations), I doubt if (or when) the major vendors will switch to this format. But still it is a useful start.
My final comment would be that I would prefer the profile to make the aud and sub claims optional, because I just don’t agree with the conclusions made here. But that’s just my personal opinion. We made sure that you can be compliant in IdentityServer, if you want to.