Authorize
This page helps you send a user to sign in at the authorization endpoint, /auth/authorize, and read what comes back.
Your app doesn’t call this endpoint itself: it sends the user’s browser there. The auth server signs the user in, asks for a one-time code or their consent when it needs to, and sends the browser back to your redirect URI with an authorization code, which your app redeems at the token endpoint.
Send an authorization request
Section titled “Send an authorization request”-
Send the browser to
/auth/authorizewith your request in the query string. Make a new randomstateand PKCE code verifier for each request, and keep both:GET /auth/authorize?client_id=my-app&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&response_type=code&scope=openid%20profile%20email&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=af0ifjsldkj&nonce=n-0S6_WzA2Mj HTTP/1.1Host: auth.example.com -
The user signs in. When the browser comes back, check that
stateis the one you sent, and thatissis the auth server’s issuer:HTTP/1.1 302 FoundLocation: https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj&iss=https%3A%2F%2Fauth.example.com -
Redeem the code at the token endpoint within 60 seconds, with the same
redirect_uriand your code verifier.
If the request is refused, the browser comes back with error and error_description in place of code, for example ?error=invalid_scope&error_description=...&state=af0ifjsldkj&iss=https%3A%2F%2Fauth.example.com. Some refusals never come back: see When an error reaches your app.
GET or POST
Section titled “GET or POST”Both work, as OpenID Connect requires. A GET carries the parameters in the query string. A POST carries them in an application/x-www-form-urlencoded body and is answered with a 303 See Other to a GET that carries a one-time request_handle in their place. The browser follows it on its own, and the sign-in continues from that GET.
The link works once, for five minutes, and only on its own: a request_handle sent twice, or beside any of the parameters below, is refused on a page at 400 Bad Request with “This sign-in link is no longer valid. It may have expired, it may have been used already, or it may have been changed. Go back to the application you were signing in to and start again.”
Parameters
Section titled “Parameters”| Parameter | Required | |
|---|---|---|
client_id |
Yes | The client’s identifier |
redirect_uri |
Yes | One of the client’s redirect URIs, matched exactly |
response_type |
Yes | code. The legacy implicit flow takes token, id_token or id_token token instead. |
scope |
Yes | One or more scopes, separated by single spaces, at most 2048 bytes. Include openid to get an ID token. |
code_challenge |
When PKCE is required | 43 to 128 characters of A-Z, a-z, 0-9, -, ., _ and ~. See PKCE. |
code_challenge_method |
With code_challenge |
S256, the only method supported |
state |
Recommended | Any value up to 2048 bytes. It comes back unchanged with the code or the error, so your app can check the answer is to its own request. |
nonce |
Recommended | Any value up to 2048 bytes. The ID token carries it in its nonce claim. |
response_mode |
No | query, fragment or form_post. See The response. |
prompt |
No | none, login or consent. See prompt. |
max_age |
No | A number of seconds. A user who signed in longer ago than that signs in again. |
acr_values |
No | The level of authentication to ask for. See ACR and AMR. |
id_token_hint |
No | An ID token naming the user your app expects. See id_token_hint. |
ui_locales |
No | The languages for the sign-in pages, as space-separated language tags in order of preference, such as pt-BR en |
PKCE is required for every public client, and for a confidential one unless an administrator made it optional.
acr_values and ui_locales are never refused for their value, though sent twice they’re refused like any repeated parameter. An acr_values with no level the auth server knows is ignored, and so is a malformed ui_locales; beyond ten language tags or 256 bytes, the rest is ignored. A parameter the auth server doesn’t read, such as login_hint, display or claims, is ignored too.
What the auth server checks
Section titled “What the auth server checks”The auth server checks the request before it shows the user anything. A check it can’t answer by sending the browser back to your app is answered on a page, with nothing sent to your app. Those come first, in this order:
| Check | Status | What the page says |
|---|---|---|
| The parameters can be read | 400 | “The authorization request could not be read: one of its parameters is not correctly encoded.” |
client_id, redirect_uri, response_type and response_mode are each sent once |
400 | “The name parameter was included more than once.” |
client_id names a client that exists and is enabled |
200 | “Invalid client_id parameter.” and why |
redirect_uri is absolute and registered on the client |
200 | “Invalid redirect_uri parameter.” and why. See Invalid redirect_uri. |
response_mode is query, fragment or form_post |
400 | “Invalid response_mode parameter. Supported values are: query, fragment, form_post.” |
response_mode=form_post names an http or https redirect URI |
400 | “response_mode=form_post posts the response to the redirect URI as a form, which no scheme other than http or https can receive.” and the modes to use instead: fragment for the implicit flow, query or fragment otherwise |
A missing client_id or redirect_uri is refused the same way. The page is shown in the language ui_locales asks for.
An unsupported response_mode gets no error response at all, whoever is signed in. Understanding the mode is what tells the auth server how to encode a response, so it answers 400 Bad Request with no error, error_description or state, as OpenID Connect Core 1.0 section 3.1.2.6 requires.
Every other check is answered with an error response to your redirect URI, carrying your state. The first that fails is the answer:
| Check | error |
|---|---|
Every other parameter is sent once. A repeated state is dropped, so that error comes back with no state. |
invalid_request |
There’s no request or request_uri: request objects aren’t supported |
request_not_supported, request_uri_not_supported |
response_type is there, separated by single spaces, and one the auth server supports |
invalid_request, unsupported_response_type |
The implicit flow is on for the client, when response_type asks for it |
unauthorized_client |
The authorization code flow is on for the client, when response_type asks for it |
unauthorized_client |
scope has openid, when response_type asks for an ID token |
invalid_request |
nonce is there, when the implicit flow asks for an ID token |
invalid_request |
state and nonce are at most 2048 bytes |
invalid_request |
code_challenge and code_challenge_method are there when PKCE is required, and right when they’re sent |
invalid_request |
The implicit flow doesn’t ask for response_mode=query, since tokens never travel in the query |
invalid_request |
max_age is digits only |
invalid_request |
scope is there, separated by single spaces, at most 2048 bytes, more than offline_access alone, and every scope in it exists |
invalid_scope |
| The client may ask for each administrative scope in it | invalid_scope |
prompt is separated by single spaces, has only none, login and consent, and none alone |
invalid_request, account_selection_required for select_account |
id_token_hint is an ID token this auth server signed |
invalid_request. See id_token_hint. |
Once the request passes, the user signs in, unless the browser’s session is enough, and goes through the one-time code and consent steps the request needs. See prompt, ACR and AMR and Sessions.
When an error reaches your app
Section titled “When an error reaches your app”The auth server never sends a browser to a client’s redirect URI with an error before the person at that browser has signed in, as RFC 9700 section 4.11.2 requires. Otherwise a link carrying one bad parameter would turn the auth server into a redirector, sending anyone who clicked it to an address of the client’s choosing. So an error from the second table above reaches your app in one of these ways, the first that applies:
| When | What happens |
|---|---|
| The client registered itself | No error is sent, ever. The user sees a page naming the address the client asked to send them to, and the request stops there. |
The request has prompt=none |
The error is sent at once, since a silent request must never show a page. |
The browser has a valid session, and the request doesn’t have prompt=login |
The error is sent at once |
Anything else, prompt=login included |
The user is asked for their password, and the error is sent once they’ve entered it, before any one-time code. That sign-in creates no session. |
The error, its description, the response mode, state and iss are the same whichever way it’s sent. See The app gets no error back for what each case looks like.
Rate limit
Section titled “Rate limit”With the rate limiter on, the GET and the POST share one budget, the authorize limit: 120 requests a minute from one IP address, counting every request, valid or not, silent or not. A request past it is answered 429 Too Many Requests with a Retry-After header and the “Too many attempts” page, before anything in it is checked, so no error reaches the redirect URI. That holds for a request with prompt=none too: a silent request in a hidden iframe gets a page your app can’t read, and sees no answer, as when the iframe times out. See rate limits and Too many attempts or 429.
Administrative scopes
Section titled “Administrative scopes”Only a client allowed to request the administrative scopes, the admin console’s own client or one an operator has allowed, may ask for authserver:manage, authserver:admin-read, authserver:manage-users, authserver:manage-clients, authserver:manage-settings or authserver:browser-sessions. Any other client asking for one is refused with invalid_scope, naming the first such scope it asked for, and gets no code and no token. The request is refused, never narrowed to the scopes that remain:
error=invalid_scopeerror_description=The client is not allowed to request the administrative scope 'authserver:manage'.It’s delivered as every other refusal above is, in the response mode the request asked for, the implicit flow’s fragment included. When the browser has a valid session, the refusal also leaves an administrative_scope_refused entry in the audit log. Without one, it leaves a log record but no audit entry, since anyone can reach this endpoint.
The allowance is read again just before the code or the implicit flow’s tokens are issued, so a sign-in under way when an operator switches a client’s allowance off ends with the same answer.
The response
Section titled “The response”A successful response carries code, state when your app sent one, and iss. An error response carries error, error_description, state when your app sent one, and iss. error_description is English, holds no character outside those RFC 6749 allows in it, and is at most 512 bytes.
iss is the auth server’s issuer, the issuer in the discovery document and the iss claim in every token. Compare it with the issuer you expect before you read anything else in the response. It tells an app that signs in with more than one auth server which one answered, so a response from one can’t pass for another’s (RFC 9207).
response_mode says how they travel:
response_mode |
How the browser comes back |
|---|---|
query, the default for response_type=code |
A 302 Found to the redirect URI with the parameters in its query string. The redirect URI’s own query is kept. |
fragment, the default for the implicit flow |
A 302 Found to the redirect URI with the parameters after #. The browser keeps them away from your server, so your page’s JavaScript reads them. |
form_post |
A page that posts the parameters to the redirect URI as a form, on its own, as OAuth 2.0 Form Post Response Mode defines. A form can’t target a custom scheme, so the redirect URI must be http or https. |
A redirect that answers the consent form, such as the access_denied a user who declines gets, is a 303 See Other in place of the 302 Found. The browser follows both with a GET, so your redirect URI sees the same request either way.
Whichever way it travels, a response, success or error, comes with Cache-Control: no-store and Pragma: no-cache, so neither the browser nor a proxy keeps a copy of it.
The implicit flow’s tokens and errors never travel in the query. See Implicit.
The code works once, for 60 seconds, and only with the redirect_uri it was issued for.