Authorization flow (Authorization Code + PKCE)

This page describes the full cycle — from the redirect to receiving and refreshing tokens. An OIDC library does most of these steps for you; what is written here is what you need to know to configure it and to handle errors.

Step 1 — Generate a PKCE pair

For every sign-in, generate a fresh random code_verifier (43–128 characters, [A-Za-z0-9-._~]) and compute code_challenge = BASE64URL(SHA256(code_verifier)). The verifier stays with you (in the session); the challenge goes in the authorize request.

PKCE is mandatory for every client, confidential ones included. Only the S256 method is supported — a plain challenge, or none at all, is refused with invalid_request.

Step 2 — Redirect to the authorize endpoint

GET {issuer}/oauth/authorize

Redirect the browser (not a server-side call, not an iframe) to GET {issuer}/oauth/authorize with these query parameters:

Field Required Description
response_type Yes Always code.
client_id Yes Your client identifier.
redirect_uri Recommended One of your registered addresses, matched exactly. May be omitted when the client has exactly one — sending it always is recommended.
scope Yes Space-separated list. openid is required; add profile email phone. See "User data".
state Recommended Random value protecting against CSRF. Returned to you unchanged — check that it matches.
nonce Recommended Random value returned in the id_token as the nonce claim. Protects against token replay.
code_challenge Yes The challenge computed in step 1.
code_challenge_method Yes Always S256.
prompt No none — never shows a form: if the customer is already signed in the code comes back at once, otherwise the error login_required. login — asks for the password again. select_account — shows the account chooser. create — opens the registration form directly (e.g. for a "Register with INEX" button).
max_age No In seconds: how old a sign-in may be. An older one is asked for the password again (with prompt=none — the error login_required). The actual time comes back in the id_token as auth_time.
login_hint No Email or phone used to prefill the sign-in field.
ui_locales No Language of the INEX ID pages: ka, en or ru.

Step 3 — Back on your redirect_uri

On success the customer returns to {redirect_uri}?code=…&state=…. Check state first, then exchange the code. The code is single-use and short-lived (a few minutes) — exchange it immediately.

On failure the same address receives ?error=…&error_description=…&state=…:

error When What to do
access_denied The customer pressed "back" / cancelled the sign-in. Return them to your sign-in page without showing an error.
login_required prompt=none and the browser has no active INEX ID session (or it is older than max_age). Start a normal (interactive) sign-in.
account_selection_required prompt=none and several accounts are signed in in the browser. Start a sign-in with prompt=select_account.
invalid_request A parameter is missing or wrong (e.g. PKCE, a method other than S256). Fix the request — this is an error on your side.
invalid_scope A requested scope does not exist. Use only the documented scopes.
When the client_id or the redirect_uri is wrong, INEX ID does not send the customer back to you, for security reasons, and shows the error on its own page.

Step 4 — Exchange the code for tokens

POST {issuer}/oauth/token

From your server, send POST {issuer}/oauth/token with Content-Type: application/x-www-form-urlencoded. A confidential client authenticates with its client_secret — either in the body (client_secret_post) or with an HTTP Basic header (client_secret_basic). A public client sends no secret.

Field Required Description
grant_type Yes authorization_code.
code Yes The code received on the callback.
redirect_uri Yes Exactly the one used in the authorize request.
client_id Yes Your client identifier.
client_secret Conditional Confidential clients only (unless you use the Basic header).
code_verifier Yes The verifier stored in step 1.

Response 200 OK

Field Type Description
token_type string Always Bearer.
expires_in integer Access token lifetime in seconds. Use this value — do not hard-code a lifetime.
access_token string Token for calling /userinfo.
refresh_token string For obtaining a new access token (see step 6). Keep it server-side only, encrypted.
id_token string (JWT) Signed JWT carrying the customer's identity. Returned when the scope contained openid.
Errors return HTTP 400/401 with JSON {"error": "…", "error_description": "…"}. Most common: invalid_grant (the code expired, was already used, or the verifier / redirect_uri does not match), invalid_client (wrong secret), unsupported_grant_type (only authorization_code and refresh_token are supported).

Step 5 — Verify the id_token

The id_token is a JWT signed with RS256. Before trusting it, always check:

  • The signature — with the public key from {issuer}/oauth/jwks, chosen by the header's kid. Cache the JWKS and download it again when an unknown kid appears (key rotation).
  • iss — equals the issuer exactly (e.g. https://id.inex.ge, no trailing /).
  • aud — equals your client_id.
  • exp — the token has not expired (an id_token is valid for 1 hour).
  • nonce — equals the one sent in the authorize request.

sub

sub is the customer's permanent identifier (a UUID). It never changes and is the same for every client. That is what you store and use to identify the customer — not the email or phone, which can change (and a phone number is not even unique).

Step 6 — Refresh the token

When the access token expires, send POST {issuer}/oauth/token with grant_type=refresh_token, refresh_token, client_id (+ client_secret for a confidential client). The response has the same shape: a new access token, a new refresh token and a new id_token (without nonce/auth_time).

Refresh tokens rotate: every refresh invalidates the old one and you must store the new one. Re-using an old one returns invalid_grant. If you make parallel requests for the same customer, guard the refresh with a lock so two processes never use the same refresh token at once.

Silent check (prompt=none)

To sign a customer in automatically when they are already signed in to INEX ID (e.g. arriving from another INEX service), send the normal authorize request with prompt=none. It returns at once with either a code or a login_required / account_selection_required error — the customer never sees a form.

Next Next: user data (scopes and claims)