AuthGate supports three OAuth 2.0 grant types. This guide covers the Authorization Code Flow (RFC 6749 §4.1) with PKCE (RFC 7636), suitable for web apps that can perform browser redirects.
- Authorization Code Flow Guide
| Scenario | Recommended Flow |
|---|---|
| CLI tools, IoT devices, TV apps | Device Code Flow (RFC 8628) |
| Web apps with a server-side backend | Authorization Code Flow (confidential client) |
| Single-page apps (SPA), mobile apps | Authorization Code Flow + PKCE (public client) |
| Microservices / server-to-server (no user) | Client Credentials Grant (RFC 6749 §4.4) |
Navigate to Admin → OAuth Clients → Create New Client (or edit an existing one).
Required fields:
| Field | Description |
|---|---|
| Client Type | Confidential for server-side apps (has a secret). Public for SPAs/mobile (uses PKCE instead of a secret). |
| Grant Types | Check Authorization Code Flow (RFC 6749) |
| Redirect URIs | Comma-separated list of allowed callback URLs. Must be an exact match (no wildcards). |
Example redirect URIs:
https://app.example.com/callback
http://localhost:3000/callback
myapp://oauth/callback
Security note: For public clients, redirect URIs are the primary security boundary. Register only the exact URIs your app uses.
After creating a confidential client, copy the client_id and client_secret shown once on the confirmation page.
For public clients, only client_id is needed (no secret).
sequenceDiagram
participant App as Web/Mobile App
participant Browser as User's Browser
participant AuthGate as AuthGate Server
App->>Browser: 1. Redirect to /oauth/authorize?client_id=...
Browser->>AuthGate: GET /oauth/authorize
AuthGate->>Browser: Login page (if not logged in)
Browser->>AuthGate: User logs in
AuthGate->>Browser: Consent page (app name + requested scopes)
Browser->>AuthGate: User clicks "Allow Access"
AuthGate->>Browser: 302 redirect_uri?code=XXXXX&state=YYY
Browser->>App: code + state delivered to callback
App->>AuthGate: POST /oauth/token (code + secret or PKCE verifier)
AuthGate-->>App: access_token + refresh_token
Consent caching: If CONSENT_REMEMBER=true (default) and the user has already consented to the same scopes, the consent page is skipped on subsequent authorizations.
PKCE (Proof Key for Code Exchange, RFC 7636) protects public clients against authorization code interception. It replaces the client secret with a cryptographic proof.
# Generate random 32-byte verifier (must be 43–128 chars)
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | head -c 43)
# Compute S256 challenge
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
echo "verifier: $CODE_VERIFIER"
echo "challenge: $CODE_CHALLENGE"In Go:
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
)
// Generate verifier
b := make([]byte, 32)
rand.Read(b)
verifier := base64.RawURLEncoding.EncodeToString(b)
// Compute S256 challenge
sum := sha256.Sum256([]byte(verifier))
challenge := base64.RawURLEncoding.EncodeToString(sum[:])GET /oauth/authorize
The user's browser is redirected to this URL to begin the authorization flow.
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
client_id |
✅ | Your OAuth client ID (UUID) |
redirect_uri |
✅ | Must exactly match a registered Redirect URI |
response_type |
✅ | Must be code |
scope |
○ | Space-separated scopes. Include openid to receive an ID Token. Defaults to all client-allowed scopes. |
state |
Recommended | Random string to prevent CSRF. Returned unchanged in the redirect. |
nonce |
OIDC Recommended | Random string bound to the session; included verbatim in the id_token to prevent replay attacks. |
code_challenge |
Public clients ✅ | Base64url(SHA256(code_verifier)) |
code_challenge_method |
Public clients ✅ | Must be S256 |
Example (confidential client):
GET /oauth/authorize?
client_id=550e8400-e29b-41d4-a716-446655440000
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&response_type=code
&scope=email%20profile
&state=abc123xyz
Example (public client with PKCE):
GET /oauth/authorize?
client_id=550e8400-e29b-41d4-a716-446655440000
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&response_type=code
&scope=email
&state=abc123xyz
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Success redirect (to your app):
https://app.example.com/callback?code=a1b2c3d4...&state=abc123xyz
Error redirect:
https://app.example.com/callback?error=access_denied&state=abc123xyz
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
Exchange the authorization code for access and refresh tokens. The code is single-use and expires in AUTH_CODE_EXPIRATION (default: 10 minutes).
Request Parameters:
| Parameter | Required | Description |
|---|---|---|
grant_type |
✅ | authorization_code |
code |
✅ | Authorization code received in the redirect |
redirect_uri |
✅ | Must exactly match the redirect_uri used in step 1 |
client_id |
✅ | Your OAuth client ID |
client_secret |
Confidential ✅ | Your client secret |
code_verifier |
Public ✅ | The original random string used to generate code_challenge |
Example (confidential client):
curl -X POST https://auth.example.com/oauth/token \
-d grant_type=authorization_code \
-d code=a1b2c3d4... \
-d redirect_uri=https://app.example.com/callback \
-d client_id=550e8400-... \
-d client_secret=your-client-secretExample (public client with PKCE):
curl -X POST https://auth.example.com/oauth/token \
-d grant_type=authorization_code \
-d code=a1b2c3d4... \
-d redirect_uri=https://app.example.com/callback \
-d client_id=550e8400-... \
-d code_verifier=your-original-verifierSuccess Response (200 OK) — without openid scope:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "email profile"
}Success Response (200 OK) — with openid scope (OIDC):
When openid is included in scope, the response also contains an id_token:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}The id_token is a signed HS256 JWT (OIDC Core 1.0 §2). Its claims depend on the granted scopes:
| Claim | Always | profile scope |
email scope |
|---|---|---|---|
iss |
✅ | ||
sub |
✅ | ||
aud |
✅ | ||
exp, iat |
✅ | ||
auth_time |
✅ | ||
jti |
✅ | ||
nonce |
✅ (if provided) | ||
at_hash |
✅ | ||
name |
✅ | ||
preferred_username |
✅ | ||
picture |
✅ (if set) | ||
updated_at |
✅ | ||
email |
✅ | ||
email_verified |
✅ |
Example authorization request with OIDC:
GET /oauth/authorize?
client_id=550e8400-e29b-41d4-a716-446655440000
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&response_type=code
&scope=openid%20profile%20email
&state=abc123xyz
&nonce=random-nonce-value
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Note: The
id_tokenis not stored in the database and is not revocable. Use theaccess_tokento call/oauth/userinfoif you need up-to-date user claims after the initial login.
Refreshing tokens works identically to the device flow — use grant_type=refresh_token:
curl -X POST https://auth.example.com/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=eyJ... \
-d client_id=550e8400-...Two CLI examples demonstrate Authorization Code Flow. Choose the one that fits your use case.
github.com/go-authgate/oauth-cli always uses the browser, suitable for desktop apps and scripts that run on a machine with a display.
git clone https://github.com/go-authgate/oauth-cli
cd oauth-cli
cp .env.example .env # Fill in CLIENT_ID (and CLIENT_SECRET for confidential clients)
go run .What the example demonstrates:
- Generates a cryptographically random
state(CSRF protection) and PKCEcode_verifier/code_challengeon every run - Opens the authorization URL in your default browser
- Starts a local HTTP server on
localhost:8888/callbackto receive the redirect - Validates the returned
stateand exchanges thecodefor tokens - Saves tokens to
.authgate-tokens.json(supports multiple client IDs in one file) - On subsequent runs, reuses valid tokens or refreshes them silently
Supported client modes:
| Mode | CLIENT_SECRET |
PKCE |
|---|---|---|
| Public client (SPA, mobile, CLI) | Leave empty | Always used |
| Confidential client (server-side) | Set the secret | Also used (defence-in-depth) |
See go-authgate/oauth-cli for full configuration options.
github.com/go-authgate/cli automatically selects the flow based on the environment: browser flow on a local machine, Device Code Flow over SSH or in headless environments. This is the recommended pattern for CLIs that must run in both contexts.
Users can view and revoke the applications they have authorized at /account/authorizations.
GET /account/authorizations
Displays a list of all applications the logged-in user has granted access to, with the authorized scopes and grant date.
Click Revoke Access next to any application in the list. This immediately:
- Invalidates the consent record (user will see the consent page again next time)
- Revokes all active access and refresh tokens issued to that application for this user
GET /admin/clients/:client_id/authorizations
Accessible from Admin → OAuth Clients → [Client Name] → 👥 Authorized Users.
Shows every user who has granted consent to this application, with their username, email, approved scopes, and grant timestamp.
POST /admin/clients/:client_id/revoke-all
Available in the Danger Zone section of the client detail page. This action:
- Revokes every active access and refresh token for this client
- Clears all user consent records (every user will see the consent page again)
Use this when rotating client secrets, responding to security incidents, or forcing all users to re-acknowledge updated permissions.
Audit log: This action is logged at
CRITICALseverity under event typeCLIENT_TOKENS_REVOKED_ALL.
| Variable | Default | Description |
|---|---|---|
AUTH_CODE_EXPIRATION |
10m |
How long an authorization code is valid. RFC 6749 recommends ≤ 10 minutes. |
PKCE_REQUIRED |
false |
When true, all clients (including confidential) must use PKCE. |
CONSENT_REMEMBER |
true |
Skip the consent page if the user has already approved the same scopes. Set to false to always show the consent page. |
Errors are returned as redirects to your redirect_uri with the following query parameters:
error |
Cause |
|---|---|
unauthorized_client |
Unknown client_id, inactive client, or Auth Code Flow not enabled |
unsupported_response_type |
response_type is not code |
invalid_scope |
Requested scope is not within the client's allowed scopes |
invalid_request |
Missing required parameter, invalid redirect_uri, or PKCE required but not provided |
access_denied |
User clicked Deny on the consent page |
Token endpoint errors are returned as JSON (HTTP 400):
error |
Cause |
|---|---|
invalid_grant |
Code expired, already used, wrong redirect_uri, or invalid PKCE code_verifier |
unauthorized_client |
Wrong or missing client_secret |
unsupported_grant_type |
grant_type is not supported |