Response types and response modes
All domains, codes, and tokens in these examples are fictional.
Every authentication request in this series has said response_type=code, and every response has come back in the query string of the printer's callback. The authentication request noted what that meant: the ID token arrives later, in the direct token response, rather than in your browser's address bar.
Those were deliberate choices, not the only ones available. Older integrations and some provider documentation use other values, and a provider that supports every combination lists them in its metadata like this:
"response_types_supported": ["code", "id_token", "id_token token",
"code id_token", "code token", "code id_token token"],
"response_modes_supported": ["query", "fragment", "form_post"]
Two choices in one request
An authentication request makes two separate choices about the response. The response type, in response_type, says what the authorization endpoint returns: a code, an ID token, an access token, or a combination. That decides which flow the sign-in follows and which tokens pass through the browser. The response mode, in response_mode, says how the authorization endpoint delivers its answer to the redirect URI.
Each response type comes with a default mode. For code, it is the query string. For every response type that returns a token from the authorization endpoint, including an ID token, the default is the fragment, and the query string must never be used. A request includes response_mode only to choose something other than the default, which is why the printer's requests have never needed it.
Response type values combine with spaces, written as %20 in the request URL, and their order does not matter: code id_token and id_token code ask for the same thing. A provider compares them as a set of words, not as a string.
Response types and their flows
OpenID Connect uses six response types, and each selects one of three flows:
response_type | The authorization endpoint returns | Flow |
|---|---|---|
code | A code | Authorization code |
id_token | An ID token | Implicit |
id_token token | An ID token and an access token | Implicit |
code id_token | A code and an ID token | Hybrid |
code token | A code and an access token | Hybrid |
code id_token token | A code, an ID token, and an access token | Hybrid |
The implicit flow skips the token endpoint entirely. Everything arrives through the browser, so the provider never authenticates the client and issues no refresh token. The hybrid flow returns some values through the browser and then exchanges the code at the token endpoint as usual, which returns an access token and an ID token, just as in the code flow.
Plain token, the OAuth implicit grant, is missing from the table. It returns only an access token, and without an ID token there is no sign-in, so OpenID Connect does not use it. OAuth also registers none, which returns no credentials at all and is rarely seen. Understanding implicit and hybrid integrations follows the implicit and hybrid flows and the extra checks they need.
Three ways back
Whatever the response type, the answer reaches the redirect URI in one of three ways. To compare them, take the response from Following a complete sign-in and send it each way.
Query. The provider redirects to the redirect URI with the response in its query string, as every example so far has done. The browser requests that URL from the printer's server, which reads the response directly. A URL travels further than the request it starts, though: into browser history, server and proxy logs, and the Referer header of requests the callback page makes. That is acceptable for a code, which is short-lived, works once, and is useless without the printer's client authentication and PKCE verifier. It is not acceptable for a token, which is why tokens never use this mode.
Fragment. The response follows a # instead of a ?:
HTTP/1.1 302 Found
Location: https://printer.example/signin/callback#code=demo-code-12&state=demo-signin-3&iss=https%3A%2F%2Fauth.photos.example
Browsers do not send the fragment to the server. The printer's server receives a request for /signin/callback with nothing attached, and only JavaScript running on the callback page can read the response, then use it in the page or pass it to the server. The fragment was designed for applications that ran entirely in the browser. It still lands in browser history, every script on the page can read it, and a browser carries it along when a later redirect does not set a fragment of its own. Redirect URI validation showed how that last behavior lets an open redirector forward tokens to another site.
Form post. With response_mode=form_post, defined in the OAuth 2.0 Form Post Response Mode specification, the provider does not redirect. It answers with a small HTML page whose form is addressed to the redirect URI and submitted automatically:
HTTP/1.1 200 OK
Content-Type: text/html;charset=UTF-8
Cache-Control: no-store
<html>
<body onload="document.forms[0].submit()">
<form method="post" action="https://printer.example/signin/callback">
<input type="hidden" name="code" value="demo-code-12">
<input type="hidden" name="state" value="demo-signin-3">
<input type="hidden" name="iss" value="https://auth.photos.example">
</form>
</body>
</html>
Your browser then sends the response to the printer as an ordinary form submission:
POST /signin/callback HTTP/1.1
Host: printer.example
Content-Type: application/x-www-form-urlencoded
code=demo-code-12&state=demo-signin-3&iss=https%3A%2F%2Fauth.photos.example
The response now travels in a request body, so it stays out of the address bar, browser history, logs that record URLs, and Referer headers. The provider tells the browser not to store the page, because the response is meant to be used once. It may also add a visible button for browsers that do not run the script, and the printer must accept the POST however it was submitted.
Two things change at the printer. The POST starts on a page from the photo service's site, so cookies marked SameSite=Lax do not travel with it. As Request correlation and CSRF explained, the cookie that identifies the pending attempt must then be SameSite=None and Secure, with the same session-bound checks behind it. And many web frameworks require their own anti-forgery token on every POST, which the photo service's form cannot include. The sign-in callback is exempt from that requirement, because state, the nonce, and PKCE, all tied to this browser's pending attempt, already show that the response belongs to a sign-in started here. Every other POST keeps the framework's protection.
Signed versions of these modes also exist, such as query.jwt and form_post.jwt, in which the provider signs the whole response as a JWT. The JWT Secured Authorization Response Mode (JARM) lessons in Advanced OAuth, beginning with Protecting an authorization response, cover them.
Why code is the default
With the code flow and PKCE, nothing that passes through the browser works on its own. The code needs the printer's client authentication and the verifier from the pending attempt, both kept on the printer's backend, and it works once. The tokens travel over a direct connection from the token endpoint, so they never appear in a URL, a history entry, or a page's scripts. The provider gets the chance to authenticate the client, to refuse a code presented twice, and to bind tokens to a key the client holds. None of that is possible for a token handed over in the browser.
Current OAuth security guidance draws its line at access tokens. Clients should not use response types that return an access token from the authorization endpoint: OAuth's plain token, and id_token token, code token, and code id_token token. The guidance names code id_token as an acceptable alternative, because access tokens still come from the token endpoint. Even so, it puts a signed statement about you into the browser and adds checks the code flow does not need. For a new relying party, code with PKCE does everything the other response types did, with less to get wrong.
That leaves the mode. The query string is the default and needs nothing extra. A form post keeps even the code out of URLs, history, and Referer headers, and current guidance names it as one way to reduce those leaks, at the cost of the cookie and framework changes above. The fragment offers a server-side relying party nothing, because its server cannot read the response without a script on the callback page to forward it. For the printer, code with the default query mode remains a sound choice, and form_post is a reasonable step further where the provider supports it.