Authorization flow
Authorization code with PKCE, from redirect to tokens.
The round trip
- Generate a PKCE verifier and challenge; keep the verifier server-side.
- Redirect the user to the authorization endpoint.
- They sign in and approve on the consent screen.
- They come back to your redirect URI with a
code. - You exchange the code — plus the verifier — for tokens.
Step 0 — PKCE
PKCE is required. Create a random code_verifier, hash it with SHA-256, and
base64url-encode the digest to get the code_challenge.
# bash
verifier=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)
challenge=$(printf '%s' "$verifier" \
| openssl dgst -binary -sha256 \
| openssl base64 | tr '+/' '-_' | tr -d '=')
Store the verifier in the user's server-side session, keyed alongside the
state. It must never travel in the URL or sit in browser storage — that would
defeat the point.
Step 1 — Send the user to authorize
GET https://account.gocosys.com/oauth-authorize.php
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyourapp.example%2Fcallback
&scope=openid%20profile%20email
&state=RANDOM_OPAQUE_VALUE
&nonce=RANDOM_VALUE
&code_challenge=THE_CHALLENGE
&code_challenge_method=S256
| Parameter | Required | Notes |
|---|---|---|
response_type | yes | Always code. |
client_id | yes | From registration. |
redirect_uri | yes | Must match a registered URI exactly. |
scope | recommended | Space-separated. Defaults to the client's registered scopes if omitted. |
state | yes, in practice | Opaque value echoed back. Your CSRF defence. |
nonce | recommended | Echoed into the ID token, binding it to this request. |
code_challenge | yes | base64url(SHA256(verifier)). |
code_challenge_method | yes | Must be S256. |
Step 2 — The user approves
If they cancel, they return to your redirect URI with an error instead of a code:
https://yourapp.example/callback?error=access_denied&state=…
Step 3 — Handle the callback
https://yourapp.example/callback?code=THE_CODE&state=YOUR_STATE
Compare state against what you stored, and reject the request if it
differs. Without this, an attacker can feed you a code from their own session and
have your application link their identity to your user.
Step 4 — Exchange the code
Within 60 seconds. Codes are single use — a replay is rejected. The endpoint accepts form-encoded (below) or JSON.
curl -X POST https://account.gocosys.com/oauth/token \
-d grant_type=authorization_code \
-d code=THE_CODE \
-d redirect_uri=https://yourapp.example/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code_verifier=THE_ORIGINAL_VERIFIER
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "…",
"id_token": "…",
"scope": "openid profile email"
}
The redirect_uri must be byte-identical to step 1, and the
code_verifier must match the challenge. Either mismatch fails with
OAUTH_INVALID_GRANT.
Step 5 — Identify the user
curl https://account.gocosys.com/oauth/userinfo \
-H "Authorization: Bearer ACCESS_TOKEN"
Create or look up your local user by the sub claim. See
Scopes & claims.
Common mistakes
- Reusing a code. Single use; a retry needs a fresh authorization.
- Taking too long. Codes expire in 60 seconds — do the exchange in the callback handler, not a background job.
- A differently-encoded
redirect_uri. It must match the registered value character for character. - Losing the verifier. If your session is not shared across instances, the exchange fails behind a load balancer.
- Keying users on email. Emails change. Use
sub.