Sorting OIDC / OAuth 2.0 Terms into Three Axes — Authorization Code / Implicit / Confidential / Public / PKCE

Many OIDC and OAuth 2.0 terms look like opposites but actually belong to different axes. Confusion like "Is the opposite of Implicit Confidential?" comes from this. I also could not sort it out in my head for a while when I was building my own OIDC provider.
If you learn them split into three axes, most of it goes away.
| Axis | Opposing terms | What it decides |
|---|---|---|
| 1. Flow | Authorization Code ↔ Implicit ↔ Hybrid | Where you receive the tokens |
| 2. Client type | Confidential ↔ Public | Whether the credentials can be kept secret |
| 3. Client authentication method | client_secret_basic / private_key_jwt / tls_client_auth / none | How the client proves its identity at the token endpoint |
PKCE is orthogonal to all three of these axes. It is not an axis but a protection mechanism that works across the axes.
So the opposite of Implicit is Authorization Code, and the opposite of Confidential is Public. These two are on different axes, so they do not directly oppose each other.
Axis 1: Flow — where you receive tokens
It is decided by response_type.
| response_type | How tokens are received | Current status |
|---|---|---|
code |
Only a voucher (the code) is received by redirect, and it is exchanged at the token endpoint over the back channel | The only choice, together with PKCE |
id_token / id_token token (Implicit) |
The tokens are returned directly on the front channel (URL fragment) | Deprecated |
code id_token etc. (Hybrid) |
Both | Heading toward deprecation |
[Authorization Code Flow]
Browser ──① authorization request──▶ Authorization server
◀─② code (redirect)──────────
App ─────③ code + PKCE verifier ──▶ token endpoint ← back channel
◀────④ id_token / access_token ──
The code is a voucher, and it contains no information itself. You get the contents for the first time at ④. What matters is that the tokens do not pass through the front channel (the browser).
Why Implicit was retired
Because the tokens are carried in the URL fragment, the browser history, the Referer header, the access logs of reverse proxies and CDNs, and extensions and other scripts on the same page all become leak paths.
In addition, a token returned on the front channel cannot be cryptographically bound to the party it was issued to, so it is hard to prevent the attack in which an attacker injects an intercepted token into their own session (token injection).
RFC 9700 (OAuth 2.0 Security Best Current Practice, 2025-01) deprecated it, and it was removed in OAuth 2.1.
Strictly speaking, what RFC 9700 rejects by name is response_type=token (the one that puts the access_token on the front channel). The configuration of response_type=id_token only + response_mode=form_post is less dangerous because the id_token is not a bearer credential for APIs, and you still see it in old configurations. Even so, there is no reason to choose it for something new.
Axis 2: Client type — Confidential / Public
The criterion is in RFC 6749 §2.1. Whether the client can maintain the confidentiality of its credentials, and nothing else.
| Examples | client_secret | |
|---|---|---|
| Confidential | Server-side web apps, batch jobs, server-to-server integration | Can have one. It can be kept inside the server |
| Public | SPAs, mobile apps, desktop apps, CLIs | Cannot have one. Even if it is embedded in the distributed files, it can be read by disassembly or DevTools |
It is not that "a public client is dangerous because it has no secret". A public client is one that is designed after correctly accepting the fact that it cannot have one. What is dangerous is the configuration where "it is a public client but a secret is embedded and it is assumed to be confidential".
Axis 3: Client authentication method
This is how a confidential client proves itself at the token endpoint, and it is registered as token_endpoint_auth_method.
| Method | Description |
|---|---|
client_secret_basic |
Sends the secret in the Authorization header. The most common |
client_secret_post |
Sends the secret in the request body |
private_key_jwt |
Sends a JWT (client assertion) that the client signed with its private key. The secret does not travel over the network |
tls_client_auth |
Authenticates with an mTLS client certificate |
none |
Public client. No client authentication |
private_key_jwt and tls_client_auth are stronger than client_secret_* in that they do not send a shared secret over the network. They are required for high-security requirements such as finance.
PKCE changed the coordinate system
In the past, people thought in this chain.
An SPA is a public client → it has no client_secret → it cannot do client authentication at the token endpoint → it cannot exchange the code → so it has no choice but to use Implicit
This wrong link, "Public, so Implicit", is the cause of the illusion that Implicit and Confidential are opposing concepts.
PKCE (RFC 7636) overturned this premise.
PKCE generates a single-use code_verifier for each request and sends only its hash (code_challenge) in the authorization request. Even if someone intercepts the code, they cannot exchange it for tokens unless they know the original code_verifier.
No secret sharing and no key management are involved (it is single-use every time). Therefore even a public client with no client_secret can use the Authorization Code Flow.
As a result, the current mapping is this.
| Authorization Code + PKCE | Implicit | |
|---|---|---|
| Confidential client | Recommended | Do not use |
| Public client | Recommended | Do not use |
"Public, so Implicit" no longer holds.
Also, PKCE is not a measure only for public clients. RFC 9700 recommends it for all clients, and OAuth 2.1 makes it mandatory. Even for a confidential client, it is worth closing the path for intercepting the authorization response.
Bonus: "JWT or authorization code" is not a choice either
This is a common confusion of the same kind.
The id_token is always a JWT, and OIDC Core defines it as "The ID Token is represented as a JSON Web Token (JWT)". There are no exceptions. The authorization code is the means of receiving that JWT safely. So it is not a choice between the two, and you use both. The JWT is returned as the result of exchanging the code.
On the other hand, the format of the access_token is free in the specification, and here there really is a branch.
| How it is verified | Characteristics | |
|---|---|---|
| JWT (RFC 9068) | Verify the signature locally | Fast. No round trip to the authorization server is needed. Revocation does not work (valid until expiry) |
| opaque (just a random string) | Query with Token Introspection (RFC 7662) | Can be revoked immediately. A round trip happens every time |
The debate over "should the access token be a JWT" is about this one, and there is no room for choice with the id_token.
By the way, the response of the UserInfo endpoint is normally just JSON (it can also be a JWT if you want to sign or encrypt it). It is not the case that "everything in OIDC is a JWT" either.
References
We look forward to discussing your development needs.