Skip to content

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.

  1. Send the browser to /auth/authorize with your request in the query string. Make a new random state and 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.1
    Host: auth.example.com
  2. The user signs in. When the browser comes back, check that state is the one you sent:

    HTTP/1.1 302 Found
    Location: https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=af0ifjsldkj
  3. Redeem the code at the token endpoint within 60 seconds, with the same redirect_uri and 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. Some refusals never come back: see When an error reaches your app.

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.”

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.

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, is enabled and has the authorization code flow on (for the implicit flow, it’s checked later) 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.”

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
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.

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 and state are the same whichever way it’s sent. See The app gets no error back for what each case looks like.

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_scope
error_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.

A successful response carries code, and state when your app sent one. An error response carries error, error_description, and state when your app sent one. error_description is English, holds no character outside those RFC 6749 allows in it, and is at most 512 bytes.

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

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.