"I presented at DevelopersIO 2026 with the title 'Who Needs JWT Access Tokens and ID Tokens, and For What Purpose' #devio2026"

"I presented at DevelopersIO 2026 with the title 'Who Needs JWT Access Tokens and ID Tokens, and For What Purpose' #devio2026"

This article is about a talk at DevelopersIO 2026 Fukuoka on October 16, 2026, covering who needs JWT access tokens and ID tokens and why, explaining the differences and how to distinguish them.
2026.10.19

This page has been translated by machine translation. View original

Hello. I'm Miyabe from the fannaly division, developing an identity product called fannaly auth.

This article is about "Who needs JWT access tokens and ID tokens, and for what purpose," a talk I gave at DevelopersIO 2026 Fukuoka on October 16, 2026. I'll explain the differences between the two types of tokens and how to distinguish them.

https://events.classmethod.jp/study-club/connpass-400607/

Presentation Materials

Session Overview

On October 16, 2026, at DevelopersIO 2026 Fukuoka, I spoke about the differences between JWT access tokens and ID tokens.

Originally, I felt that even among engineers, few people could explain the difference between ID tokens and access tokens. I also thought that when both are in the same JWT format, they look even more similar, making them even harder to distinguish.

Then, when thinking about a case where a client sends the wrong token, and whether it's always possible to correctly determine from the received JWT alone whether it's an ID token or an access token, I came to think "this is actually quite difficult" — and that's what motivated me to talk about this topic.

So I organized, based on RFC and OpenID Connect specifications, who needs each token and for what purpose, and how the receiving party can tell them apart.

Which one is which token's content?

Below are examples of headers and payloads when decoding a JWT-type access token and an ID token.

A

// Header
{ "alg": "RS256", "typ": "JWT" }
// Payload
{
  "iss": "https://as.example.com",
  "sub": "5ba552d67",
  "aud": "s6BhdRkqt3",
  "exp": 1639528912,
  "iat": 1618354090,
  "jti": "9f8e7d6c5b4a",
  "nonce": "n-0S6_WzA2Mj"
}

B

// Header
{ "alg": "RS256", "typ": "at+jwt" }
// Payload
{
  "iss": "https://as.example.com",
  "sub": "5ba552d67",
  "aud": "https://api.example.com",
  "client_id": "s6BhdRkqt3",
  "exp": 1639528912,
  "iat": 1618354090,
  "jti": "9f8e7d6c5b4a",
  "scope": "orders:read"
}

iss, sub, exp, and iat are present in both, making them almost indistinguishable at a glance.

Let's use these two as a basis for learning how to tell them apart.

The specification itself says they are "similar"

The fact that they are actually similar is even noted in RFC 9068, the specification for JWT access tokens, so my examples are not being tricky.

The JWT access token data layout described here is very similar to that of the id_token as defined by [OpenID.Core].

RFC 9068 Section 5

In the first place, the access token being a JWT is not itself a requirement of the specification.

OAuth 2.0's RFC 6749 does not define the format of access tokens (RFC 6749 Section 1.4).

The two most commonly used formats in practice are opaque tokens and JWTs.

Advantages of using JWT

Now let's think about the advantages of using JWT.

Consider the case where a client receives an access token from the authorization server and communicates with a resource server.

In the case of an opaque token

  1. Retrieve configuration information
  2. The client receives the access token from the authorization server
  3. The client attaches the access token to the Authorization header and accesses the resource server
  4. When the resource server receives the token, it queries the authorization server via introspection (RFC 7662) to verify whether it is valid, whose token it is, and what scope it has
  5. If correct, it returns the resource to the client

In the case of a JWT token

  1. Retrieve configuration information
  2. Retrieve the public key (JWKS)
  3. The client receives the access token from the authorization server
  4. The client attaches the access token to the Authorization header and accesses the resource server
  5. When the resource server receives the token, it verifies the signature
  6. Verify whether it is valid, whose token it is, and what scope it has
  7. If correct, it returns the resource to the client

As shown above, by using JWT, the resource server no longer needs to query the authorization server every time it receives a token.[1]

RFC 9068's Introduction also notes the background that many implementations issue tokens in a format that resource servers can directly parse and verify without going through the authorization server (RFC 9068 Section 1).

Conversely, in the case of opaque tokens, since they are random strings and neither the client nor the resource server can read their contents at all, mistaking them for ID tokens would not happen in the first place.

The aud may differ

The answer from before is that A is the ID token and B is the access token.

The most obvious difference is aud. The aud of an ID token is the client ID — this is defined by OpenID Connect Core (OpenID Connect Core 1.0 Section 2). The aud of an access token in RFC 9068 is the resource server's identifier (RFC 9068 Section 3).

However, I said "may differ" because there are cases where the client ID is also placed in the access token's aud.

In that case, aud alone cannot distinguish them.

Different aud means different recipients

aud is short for audience, meaning "who is this token addressed to."

An ID token is issued from the authorization server to the client, and it is the client that both receives and reads it — it is a token for authentication.

An access token is also received by the client, but it is the resource server that reads it; the client merely carries it — it is a token for authorization.

Using a letter as an analogy, they may be in the same-looking JWT envelope, but you should only open the one addressed to you.

The client just passes it without reading it

RFC 9068 explicitly prohibits clients from inspecting the contents of access tokens.

The client MUST NOT inspect the content of the access token

RFC 9068 Section 6

The reason is also stated in the same Section: since the authorization server and resource server can change the token format at any time (for example, switching to opaque), client logic that reads the contents may break.

Also, access tokens may contain personal information.
Since anyone can decode a JWT without a key, reading it in the client and outputting it to logs or screens could lead to privacy issues.

The client simply places it in the Authorization: Bearer header and sends it to the resource server without looking at the contents.

It is the role of the receiving resource server to inspect and verify the contents.

How does the receiving party distinguish them?

If issued by the same authorization server, ID tokens and access tokens are in most cases signed with the same key published in the same JWKS. Therefore, they cannot be distinguished by signature verification alone.

If a resource server accepts a token simply because its signature verification passes, it will also accept ID tokens.

ID tokens are often placed on the frontend, and are frequently less protected than access tokens.

The reverse also occurs. There are cases where an authorization server receives an ID token via id_token_hint, but if an access token is placed there instead, signature verification will still pass. Moreover, under OpenID Connect Core, when used as id_token_hint, the authorization server need not be listed as an audience, so it cannot necessarily be rejected based on aud.

The Authorization Server need not be listed as an audience of the ID Token when it is used as an id_token_hint value.

OpenID Connect Core 1.0 Section 3.1.2.1

RFC 8725 calls this kind of mix-up a Cross-JWT Confusion (RFC 8725 Section 2.8).

Distinguishing by the typ header

This is why I also showed the headers in the first slide — the typ of an ID token was JWT, and the typ of an access token was at+jwt.

In RFC 9068, the first step in the resource server's validation procedure is to check typ.

The resource server MUST verify that the "typ" header value is "at+jwt" or "application/at+jwt" and reject tokens carrying any other value.

RFC 9068 Section 4

On the authorization server side as well, if a token with at+jwt or application/at+jwt arrives as id_token_hint, it can be rejected.

Whether the signature passed and what kind of token it is are separate matters.

Is typ always present?

However, it cannot be said that typ resolves everything.

RFC 9068 was published in October 2021, a relatively new specification that came 9 years after OAuth 2.0 (RFC 6749, October 2012).

Implementations that were issuing JWT access tokens before that each had their own proprietary formats, and many continue to maintain them for compatibility reasons.

Looking at the official documentation of major IDaaS services (as of October 2026):

  • Microsoft Entra ID: Access tokens for self-registered APIs are JWT, and typ is JWT.
  • Okta: Access tokens from custom authorization servers are JWT, with no typ in the header.
  • Auth0: Access tokens for APIs are JWT, and with the default profile typ is JWT. Only when the RFC 9068 profile is selected does it become at+jwt.
  • ID tokens: OpenID Connect Core does not define typ (historically JWT has been common).

In fact, there is a history where OpenID Connect adopted JWT for use as ID tokens before JWT was standardized as an RFC.

RFC 7519 Section 5.1 recommends specifying JWT when using the typ header, so it is entirely normal for both ID tokens and access tokens to have typ of JWT.

When typ cannot be relied upon, the validation rules must be made mutually exclusive.

If more than one kind of JWT can be issued by the same issuer, the validation rules for those JWTs MUST be written such that they are mutually exclusive, rejecting JWTs of the wrong kind.

RFC 8725 Section 3.12

For example, determine by whether aud is your own identifier, or whether there is a claim that only exists in one type of token.

What claims exist in only one type?

So which claims exist in only one type — that depends on the implementation.

For example, jti being included in an ID token is not a spec violation.
client_id may also appear to exist only in access tokens, but Entra ID's access tokens use azp instead of client_id.

There is no standard specification for distinguishing by claims. ID tokens have claims_supported published via OpenID Connect Discovery (OpenID Connect Discovery 1.0), but there is no equivalent specification for access tokens, so you would need to compare the published documentation of each service.

You might think "if it's an access token for authorization, it shouldn't contain authentication information" — but that's not necessarily true either.

RFC 9068 optionally allows authentication-related claims such as auth_time, acr, and amr to be included in access tokens (RFC 9068 Section 2.2.1).

In the end, you need to actually examine the tokens you receive and decide accordingly.

In Closing

The content I covered in the session was as follows:

  • Even though they look similar as JWTs, ID tokens are addressed to the client and access tokens are addressed to the resource server — they have different recipients
  • Clients pass the access token without reading it
  • The receiving party identifies them by typ, or if typ is absent, by aud or claims that exist in only one type
  • Read the documentation carefully and implement accordingly
  • Clients should send the correct token

Signature verification passing and a token being addressed to you are not the same thing.

When implementing a party that receives JWTs, take a moment to confirm what kind of token you intend to receive.

References

脚注
  1. In practice, the authorization server's configuration information (which contains the JWKS URL, etc.) and the public key (JWKS, RFC 7517 Section 5) are fetched and cached, and signature verification is done within the resource server. Configuration information is retrieved from /.well-known/openid-configuration (OpenID Connect Discovery 1.0 Section 4) or /.well-known/oauth-authorization-server (RFC 8414 Section 3). Since the cache is refreshed when it expires or when keys are rotated, queries to the authorization server do not disappear entirely. ↩︎

Share this article