Understanding implicit and hybrid integrations
All domains, codes, and tokens in these examples are fictional. The hash values were calculated from the example values shown.
Choosing a flow put the implicit grant among the flows to avoid: tokens placed in redirects leak, and nothing binds them to the client that asked. You will still meet OpenID Connect integrations built on the implicit and hybrid flows, often in applications written before browsers could comfortably run the code flow with PKCE. The implicit grant, in the OAuth lessons on history, legacy, and migration, explains the OAuth grant itself. OpenID Connect added protections to both flows, and to repair or replace these integrations you need to know what those protections cover and where they stop.
To keep the comparison direct, imagine the printer sending the sign-in from Following a complete sign-in in each of the older shapes, with the same attempt values.
The implicit flow
An implicit sign-in asks for its tokens directly from the authorization endpoint:
https://auth.photos.example/authorize
?response_type=id_token%20token
&client_id=photo-printer
&redirect_uri=https%3A%2F%2Fprinter.example%2Fsignin%2Fcallback
&scope=openid%20profile%20email
&state=demo-signin-3
&nonce=demo-nonce-3
There is no PKCE challenge, because there will be no code to exchange. The nonce, which a code-flow request may leave out, is required here. The response arrives in the fragment, with the ID token shortened for display:
https://printer.example/signin/callback
#access_token=demo-access-token-12
&token_type=Bearer
&expires_in=600
&id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6InBob3Rvcy1ycy0yMDI2LTA5IiwidHlwIjoiSldUIn0.eyJ...shortened
&state=demo-signin-3
&iss=https%3A%2F%2Fauth.photos.example
Compare that with the code flow. The printer never authenticated, so its client secret played no part. Both tokens have passed through the address bar and remain in your browser history, and only a script on the callback page can read them.
The ID token is now the only evidence that this response is genuine and belongs to this attempt, and the checks change to match. The printer must verify its signature, because the allowance that lets a code-flow relying party rely on its own TLS connection to the token endpoint does not cover a token that arrived through the browser. Its nonce must be demo-nonce-3. A token in a URL can be copied, and the nonce is what stops a token captured from another session being replayed into this one.
The access token needs a check of its own. Someone who could alter the fragment could swap in an access token for another account, and nothing in OAuth's implicit grant would notice. The ID token therefore carries an at_hash claim, a hash of the access token issued with it, which the printer compares with the token that arrived.
With response_type=id_token, no access token is issued at all, and so there is no at_hash. The claims that profile and email would have released through UserInfo are placed in the ID token instead, which turns that token into a description of you traveling in a URL.
The hybrid flow
The hybrid flow keeps the code exchange and adds tokens to the browser response. With response_type=code id_token, the response carries a code and an ID token, and combining it with response_mode=form_post delivers both straight to the server:
POST /signin/callback HTTP/1.1
Host: printer.example
Content-Type: application/x-www-form-urlencoded
code=demo-code-12
&id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6InBob3Rvcy1ycy0yMDI2LTA5IiwidHlwIjoiSldUIn0.eyJ...shortened
&state=demo-signin-3
&iss=https%3A%2F%2Fauth.photos.example
The body is split across lines for display. Decoded, the ID token's claims include one that the code flow's token did not:
{
"iss": "https://auth.photos.example",
"sub": "user-2048",
"aud": "photo-printer",
"iat": 1790845200,
"exp": 1790845500,
"auth_time": 1790845185,
"nonce": "demo-nonce-3",
"c_hash": "vF6ec9VXTnfCcMFxiT1V9Q"
}
Before it redeems the code, the printer now has a signed statement of who signed in and, in c_hash, a way to tell that the code beside it is the one the provider issued with that statement.
The nonce is required whenever the response type includes id_token. The first ID token is validated as in the implicit flow, signature included. Its iss must agree with the iss parameter beside it, and its c_hash must match the code. The printer then exchanges demo-code-12 at the token endpoint as before, with its client authentication and, if it sent a challenge, its PKCE verifier, and receives an access token and a second ID token.
The second ID token is validated in full as well. Its iss and sub must be identical to the first token's, and it may leave out c_hash, because it arrived over the printer's own connection. Current security guidance adds a rule for relying parties that count on the nonce to catch an injected code: check the nonce in this second token too, even though the first one passed, and use none of the tokens until that check succeeds.
Checking c_hash and at_hash
Both claims are calculated the same way, from the exact value that arrived in the response:
- Choose the hash function that matches the ID token's
alg: SHA-256 for RS256, ES256, PS256, or HS256, and SHA-384 or SHA-512 for the algorithms that end in 384 or 512. - Hash the ASCII bytes of the code or access token, as received after removing the URL or form encoding.
- Keep the left-most half of the hash, which is 16 bytes for SHA-256.
- Encode those bytes with Base64url, without padding, and compare the result with the claim exactly.
For the photo service's RS256 tokens, the calculation looks like this. The hashes are shown in hexadecimal and split at the halfway point, but the claim encodes the 16 bytes themselves, not their hexadecimal text:
ID token alg RS256, so SHA-256
code demo-code-12
SHA-256 bc5e9e73d5574e77c270c171893d55f5 392a9b5b318112b35490f46ea8fd57af
left-most half bc5e9e73d5574e77c270c171893d55f5
c_hash vF6ec9VXTnfCcMFxiT1V9Q
access token demo-access-token-12
SHA-256 b5711f37490166d89c0612ba84ad867b c81049cd0ebe141cce00bb2b783ff79f
left-most half b5711f37490166d89c0612ba84ad867b
at_hash tXEfN0kBZticBhK6hK2Gew
Which claims must be present depends on what the authorization endpoint returned beside the ID token:
response_type | c_hash | at_hash |
|---|---|---|
id_token | No code is returned | No access token is issued |
id_token token | No code is returned | Required |
code id_token | Required | No access token in this response |
code id_token token | Required | Required |
code token returns no ID token from the authorization endpoint, so there is nothing to compare until the token endpoint answers. In the code flow, an ID token from the token endpoint may also carry at_hash, and the relying party may check it in the same way.
A matching hash shows that the code or access token beside the ID token is the one the provider issued with it. With the signature and nonce checks, that defeats token substitution: an attacker cannot swap in a code or token from another session, because they cannot produce a signed ID token with this attempt's nonce and a matching hash. It says nothing about who else has seen the token, and a leaked access token still works at the photo API for anyone who holds it.
The common mistakes are small: hashing the encoded value instead of the decoded one, keeping the whole hash, using standard Base64 with +, /, and padding, or always using SHA-256 when the provider signs with ES384. Each produces a mismatch on a genuine response, and turning the check off in response removes the protection these flows depend on.
Moving to the code flow
Migrating older integrations, in the same OAuth lessons, covers moving any client away from the implicit grant. A relying party has a few more things to carry across:
- Change the request, keep the nonce. Send
response_type=codewith a fresh PKCE challenge, and keep sending and checking a nonce, which still ties the ID token to the attempt. Dropresponse_modeunless you deliberately chooseform_post. - Move profile claims to UserInfo. A relying party that used
response_type=id_tokenread your name and email from the ID token. With the code flow, those claims usually come from UserInfo, so the code that read them must call UserInfo and check itssub. - Replace silent renewal. Implicit applications often obtained new tokens by loading the authorization endpoint with
prompt=nonein a hidden frame, the technique Browser applications showed failing where third-party cookies are blocked. Use a refresh token protected as that lesson described, or a backend session. - Remove the old response types. Once the new flow is live, remove the implicit and hybrid response types from the client's registration so the provider refuses them. While they remain allowed, anyone can start a request that returns tokens to the old callback page, keeping any weakness there within reach.
A hybrid integration that uses code id_token with a form post is not insecure in itself, and current guidance names it as an acceptable choice. Moving it to code with PKCE still removes an ID token from the browser, a second validation path, and the hash checks, and adds PKCE's protection against injected codes. The result is the sign-in the printer has used throughout this series.