RFC 9068: A JWT-Based OAuth2 Access Token Format Standard

Tokens / SHYCITYNikon

For anyone who has been paying attention, this blog post has been a long-time coming for multiple reasons. First, this is my first blog post in a couple of years — I’ve been heads down on a couple of projects for awhile now. This is literally the first time I’ve “come up for air” since the last time I posted a blog post — let’s make up for lost time. Second, the topic of this post is something that the original OAuth2 spec has been missing for a long time. Most of my identity-related blog posts in the past have used OAuth2 in one way or another. It is a versatile identity protocol that covers a wide variety of use cases. This blog post will explore what RFC9068 brings to the table for the OAuth2 spec/protocol.

RFC9068 defines an official OAuth2 Access Token format where the original OAuth2 spec simply described it as an opaque token that could have different formats or structures. I have described this in the past as “the OAuth2 Access Token structure was left to the creativity/needs of the IdP implementer.”

The original OAuth2 RFC described the OAuth2 Access Token as:

Access tokens are credentials used to access protected resources. An access token is a string representing an authorization issued to the client. The string is usually opaque to the client. Tokens represent specific scopes and durations of access, granted by the resource owner, and enforced by the resource server and authorization server.

Access tokens can have different formats, structures, and methods of
utilization (e.g., cryptographic properties) based on the resource
server security requirements. Access token attributes and the
methods used to access protected resources are beyond the scope of
this specification and are defined by companion specifications such
as [RFC6750].

It specifically states that the access token can have different formats and structures. It specifically assumes that the access token is usually opaque — not directly usable/observable to the OAuth2 resource or client.

In the meantime, most of the major IdP vendors that support OAuth2 (which is all of them) decided long ago to use JSON Web Token (JWT) defined in RFC7519 as the OAuth2 Access Token format. So, the introduction of an official standard that calls for JWT as the OAuth2 Access Token format isn’t really changing anything. It’s just formalizing what has been done informally for a long-time now.

What is more interesting is the specific claims that are now required in the JWT-based OAuth2 Access Token.

Standard Claims (JWT Header)

First, the JWT acting as an OAuth2 Access Token must be signed — so, don’t use “none” as the signing algorithm. Asymmetric Cryptography (read RSA signing algorithms, not HMAC) is recommended for use with digital signatures in this situation because it is easier for Resource Servers to validate the digital signature with public key cryptography. RS256 must be supported by the Authorization Server and Resource Server.

The following claims will be found in the header:

typ: REQUIRED — must contain the value “application/at+jwt” to identify compliance with RFC9068. This is defined in the JSON Web Signature (JWS) spec.

alg: REQUIRED — must contain the algorithm used for the digital signature (probably “RS256”). Also, defined by the JWS spec.

kid: REQUIRED — Key Identifier that corresponds to the public/private key pair used to generate the digital signature on this JWT. Also, defined by the JWS spec.

Standard Claims (JWT Payload)

From RFC9068, we have the following required claims:

iss: REQUIRED — as defined in Section 4.1.1 of [RFC7519].

exp: REQUIRED — as defined in Section 4.1.4 of [RFC7519].

aud REQUIRED — as defined in Section 4.1.3 of [RFC7519]. See Section 3 for indications on how an authorization server should determine the value of “aud” depending on the request.

sub: REQUIRED — as defined in Section 4.1.2 of [RFC7519]. In cases of access tokens obtained through grants where a resource owner is involved, such as the authorization code grant, the value of “sub” SHOULD correspond to the subject identifier of the resource owner. In cases of access tokens obtained through grants where no resource owner is involved, such as the client credentials grant, the value of “sub” SHOULD correspond to an identifier the authorization server uses to indicate the client application. See Section 5 for more details on this scenario. Also, see Section 6 for a discussion about how different choices in assigning “sub” values can impact privacy.

client_id: REQUIRED — as defined in Section 4.3 of [RFC8693].

iat: REQUIRED — as defined in Section 4.1.6 of [RFC7519]. This claim identifies the time at which the JWT access token was issued.

jti: REQUIRED — as defined in Section 4.1.7 of [RFC7519].

The Issuer(iss) claim is the Base URL of the Authorization Server that issued the token. One sees this in just about every implementation of JWT-based OAuth2 Access Tokens, probably, because it is required by the JWT spec.

The Expires(exp) claim is time in seconds since January 1st, 1970, when the token expires. Also, required by the JWT spec.

The audience(aud) claim describes the OAuth2 Resource that is supposed to accept this Access Token. Also, required by the JWT spec. This could be the client_id of another OAuth2 client, it could be the URL of an API. It’s flexible. As noted above, it can be a single string or an array of strings. Again, required by the JWT spec.

The subject(sub) claim usually contains a reference to either a) the authenticated user or b) the client_id of the authenticated OAuth2 Client. Usually it is “userid”, but not always. This is a bit vague, but is meant to be flexible. You can certainly use a custom claim to convey the username. The description in the spec ties this claim to the resource owner. From a practical standpoint with use cases that I tend to deal with, the backend resource is probably some type of API that is used across a wide variety of applications in an enterprise context. The end user doesn’t necessarily “own” that API endpoint or the data being accessed. That’s okay; it’s an example of the flexibility the OAuth2 spec is bringing to the table. Again, required by the JWT spec.

The client_id claim is fairly self explanatory. This is the client_id of the OAuth2 Client that requested the token. It is NOT the client_id of the Resource described by the audience(aud) claim. This description makes the assumption that the OAuth2 Resource (API, etc) is registered with the OAuth2 Authorization Server (Provider) as an OAuth2 Client the same way the calling OAuth2 client is. Azure Active Directory will typically do things this way; Auth0 does things very similar to this, but not all IdPs necessarily do this. This claim is defined in RFC8693, the OAuth2 Token Exchange Spec (sometimes referred to as the OBO call).

The Issued At(iat) claim is the number of seconds since January 1st, 1970 that that the token was generated/issued. The difference between the exp field and this field can be used to determine if the token has expired or how much time is left before it does expire. As noted in other blog posts, use a clock skew value such as thirty or sixty seconds between the actual expiration time and when the system obtains a new OAuth2 Access Token. Also, defined by the JWT spec.

The JWT ID(jwt) field is a unique identifier for this token. This will become important when interacting with an OAuth2 Token Introspection endpoint. Also, defined by the JWT spec.

The following claims come from RFC9068, Section 2.2.1 when a resource owner (pretty much anytime an authenticated, human user is involved):

auth_time: OPTIONAL — as defined in Section 2 of [OpenID.Core]
acr: OPTIONAL — as defined in Section 2 of [OpenID.Core].
amr: OPTIONAL — as defined in Section 2 of [OpenID.Core].

The Authentication Time(auth_time) claim describes the number of seconds since January 1st, 1970 since the user was originally authenticated in the session context that this token was issued. With refresh tokens or other mechanisms, it is possible that many OAuth2 Access Tokens could be assigned to any given authenticated user session over the session lifetime.

The Authentication Context Class Reference is described in the OIDC Core spec as:

String specifying an Authentication Context Class Reference value that identifies the Authentication Context Class that the authentication performed satisfied. The value “0” indicates the End-User authentication did not meet the requirements of ISO/IEC 29115 [ISO29115] level 1. For historic reasons, the value “0” is used to indicate that there is no confidence that the same person is actually there. Authentications with level 0 SHOULD NOT be used to authorize access to any resource of any monetary value. (This corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] nist_auth_level 0.) An absolute URI or an RFC 6711 [RFC6711] registered name SHOULD be used as the acr value; registered names MUST NOT be used with a different meaning than that which is registered. Parties using this claim will need to agree upon the meanings of the values used, which may be context specific. The acr value is a case-sensitive string.

In the past ten+ years or so that I’ve been working with OIDC, OAuth2, and JWTs, I’ve never directly used this field. It might happen some day, but I’m not going to say anymore about it for now.

The Authentication Methods References (amr) claim describes the authentication method used to authenticate the user described in the subject(sub) claim. The OIDC Core spec describes this as:

Authentication Methods References. JSON array of strings that are identifiers for authentication methods used in the authentication. For instance, values might indicate that both password and OTP authentication methods were used. The amr value is an array of case-sensitive strings. Values used in the amr Claim SHOULD be from those registered in the IANA Authentication Method Reference Values registry [IANA.AMR] established by [RFC8176]; parties using this claim will need to agree upon the meanings of any unregistered values used, which may be context specific.

scopes: OPTIONAL (but, usually present) — The scopes claim is described in the original OAuth2 RFC. Scopes can be defined by the OIDC spec, custom scopes, or defined by IdP vendors. The scopes need to have meaning to the audience referenced in the aud claim.

groups, roles, entitlements claims: OPTIONAL — These are defined in the SCIM (System for Cross-domain Identity Management: Core Schem) RFC. This is a more obscure reference than some of the other claims mentioned in this post, but SCIM does provide a rich, standardized way of describing these concepts — so, let’s go with it. Each of these claims could be an array of values.

As an example, groups defined in this way may look like (from the RFC7643 spec):

     "groups": [
{
"value": "e9e30dba-f08f-4109-8486-d5c6a331660a",
"$ref":
"https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a",
"display": "Tour Guides"
},
{
"value": "fc348aa8-3835-40eb-a20b-c726e15c55b5",
"$ref":
"https://example.com/v2/Groups/fc348aa8-3835-40eb-a20b-c726e15c55b5",
"display": "Employees"
},
{
"value": "71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
"$ref":
"https://example.com/v2/Groups/71ddacd2-a8e7-49b8-a5db-ae50d0a5bfd7",
"display": "US Employees"
}
]

While entering that example above, I also see that medium.com has added a code section option finally. Good for them. Makes code (and JSON) samples much more readable.

Requesting a JWT-Based OAuth2 Access Token

This is more-or-less the same as what one would find while interacting with the OAuth2 Token Endpoint for any other use case. RFC9068 does provide for an additional parameter to the OAuth2 Authorization Endpoint called “resource”, which should match the audience(aud) claim in any JWT-based OAuth2 Access Token that is issued in response to a Token Endpoint request with the matching Authorization Code.

From RFC9068, “The authorization server MUST NOT issue a JWT access token if the authorization granted by the token would be ambiguous.” Section 5 has details of what might lead to ambiguity.

If the request doesn’t include a resource parameter, then a reasonable default must be defined for the Audience (aud) claim by the Authorization Server. In many cases, this will probably match the client_id of the calling OAuth2 Client similar the OIDC ID Token. If a scope parameter is present in the OAuth2 request to the Authorization Endpoint, it may be possible to infer a valid audience paramter for the OAuth2 Access Token from that. If this type of inference is not possible or the combination of scopes and audience/resource does not make sense, then an invalid_scopes error should be returned to the OAuth2 Client.

Validating JWT-Based OAuth2 Access Tokens

From RFC9068, we have the following paraphrased information:

Resource Servers receiving a JWT access token MUST validate it in the following manner.

  • Verify that the “typ” header value is “at+jwt” or “application/at+jwt”. Reject the token if not.
  • If JWE is used, decrypt the token first. If token is supposed to be encrypted and it is not, reject it. If the token fails decryption with the expected key, reject it.
  • The issuer identifier for the authorization server MUST exactly match the “iss” claim value.
  • The aud” claim MUST contain a value that the Resource Server is expecting. If not, the request must be rejected.
  • The resource server MUST validate the signature of all incoming JWT access tokens according to [RFC7515] using the algorithm specified in the JWT “alg” Header Parameter. If this validation fails, the request must be rejected.
  • The current time MUST be before the time represented by the “exp” claim. A small clock skew of thirty or sixty seconds is recommended.

Some additional notes.

Authorization Servers should use RFC8414 to publish metadata about how to validate OAuth2 Access Tokens. In particular, this will provide a JWKS endpoint to obtain signing certificate information and the correct value of the issuer (iss) Claim in the JWT-based OAuth2 Access Token. Alternatively, the OIDC Discovery Endpoint could also be used. Information published across the two endpoints should be consistent.

Different keys can be used for signing OIDC ID Tokens and OAuth2 JWT-Based Access Tokens. There isn’t a specific mechanism for conveying the different signing keys; however, some IdPs will publish different JWKs endpoints for the two use cases.

When an OAuth2 Resource Server fails to validate an OAuth2 Access Token, the response must include the error code “invalid_token”. I’ve seen exceptions to this.

The Resource Server should use claims such as audience and scopes to determine if the request this token is attached to should have access granted. The details of these authorization decisions is highly context specific and beyond the scope of RFC9068.

Conclusion

RFC9068 has been a long-time coming. Luckily, it is formalizing what most IdP vendors are already doing. Having critical pieces of functionality that are not defined by a spec is never a good situation. As the IAM industry moves forward, this will become more-and-more the default standard that all IdPs use for OAuth2 Access Token formats.

Leave a Reply