OAuth 2.0 and OpenID Connect
Rampart implements OAuth 2.0 (RFC 6749) and OpenID Connect Core 1.0 as a full-featured authorization server and identity provider. This page covers all supported grant types, PKCE, the OIDC layer, token formats, and discovery mechanisms.
Supported Grant Types
Authorization Code with PKCE
The Authorization Code flow with Proof Key for Code Exchange (RFC 7636) is the recommended flow for all client types — single-page applications, mobile apps, desktop apps, and server-side applications.
PKCE is mandatory for public clients and strongly recommended for confidential clients.
How PKCE Works
PKCE prevents authorization code interception attacks by binding the authorization request to the token exchange request using a cryptographic proof.
Step 1 — Generate the code verifier and challenge:
code_verifier = random_string(43..128 characters, [A-Z] [a-z] [0-9] "-" "." "_" "~")
code_challenge = BASE64URL(SHA256(code_verifier))
The code_verifier is a high-entropy random string generated by the client and kept secret. The code_challenge is a SHA-256 hash of the verifier, Base64URL-encoded.
Step 2 — Authorization request:
GET /oauth/authorize?
response_type=code&
client_id=my-app&
redirect_uri=https://app.example.com/callback&
scope=openid profile email&
state=random-csrf-token&
nonce=random-nonce&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Step 3 — User authenticates and consents:
Rampart displays the login page (themed per organization). After authentication, if consent is required, the consent screen is shown. On approval, Rampart generates a one-time authorization code.
Step 4 — Redirect with authorization code:
HTTP/1.1 302 Found
Location: https://app.example.com/callback?
code=SplxlOBeZQQYbYS6WxSbIA&
state=random-csrf-token
Step 5 — Token exchange:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://app.example.com/callback&
client_id=my-app&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Step 6 — Rampart validates and responds:
Rampart computes BASE64URL(SHA256(code_verifier)) and compares it with the stored code_challenge. If they match, tokens are issued:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "8xLOxBtZp8...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"scope": "openid profile email"
}
Complete PKCE Flow Diagram
Client Rampart User
| | |
| Generate code_verifier | |
| Derive code_challenge | |
| | |
| GET /oauth/authorize | |
| + code_challenge (S256) | |
|------------------------------>| |
| | Store code_challenge |
| | Show login page |
| |------------------------------->|
| | |
| | User enters credentials |
| |<-------------------------------|
| | |
| | Verify credentials |
| | (+ MFA if enabled) |
| | Show consent screen |
| |------------------------------->|
| | |
| | User grants consent |
| |<-------------------------------|
| | |
| 302 ?code=AUTH_CODE | Generate auth code |
|<------------------------------| Bind to code_challenge |
| | |
| POST /oauth/token | |
| + code + code_verifier | |
|------------------------------>| |
| | SHA256(code_verifier) |
| | == stored code_challenge? |
| | |
| { access_token, id_token, | |
| refresh_token } | |
|<------------------------------| |
Security notes:
- Only the
S256challenge method is supported. Theplainmethod is rejected due to security concerns. - Authorization codes are single-use and expire after 10 minutes.
- The
stateparameter must be validated by the client to prevent CSRF attacks. - The
nonceparameter must be verified in the ID token to prevent replay attacks.
Client Credentials
The Client Credentials flow (RFC 6749, Section 4.4) is for machine-to-machine communication where no user is involved. The client authenticates directly with its credentials to obtain an access token.
Request:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic BASE64(client_id:client_secret)
grant_type=client_credentials&
scope=api:read api:write
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api:read api:write"
}
Key characteristics:
- No refresh token is issued (the client can request a new access token at any time)
- No ID token is issued (there is no authenticated user)
- The
subclaim in the access token is the client ID, not a user ID - Scopes are limited to those pre-approved for the client during registration
- Client authentication can use HTTP Basic auth or
client_id/client_secretin the request body
Refresh Token
The Refresh Token flow (RFC 6749, Section 6) allows clients to obtain new access tokens after the original expires, without requiring the user to re-authenticate.
Request:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=8xLOxBtZp8...&
client_id=my-app
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "new-refresh-token...",
"scope": "openid profile email"
}
Rotation behavior:
- Every refresh token is single-use
- Exchanging a refresh token issues a new refresh token (rotation)
- The old refresh token is immediately invalidated
- If a revoked refresh token is reused (potential theft), the entire token family is revoked and the session is terminated
- See Authentication > Refresh Tokens for details on replay detection
Device Authorization (Device Code)
The Device Authorization Grant (RFC 8628) enables authentication on devices with limited input capabilities — smart TVs, CLI tools, IoT devices, and CI/CD agents.
Step 1 — Device requests authorization:
POST /oauth/device/code
Content-Type: application/x-www-form-urlencoded
client_id=cli-tool&
scope=openid profile
Response:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNh",
"user_code": "WDJB-MJHT",
"verification_uri": "https://auth.example.com/device",
"verification_uri_complete": "https://auth.example.com/device?user_code=WDJB-MJHT",
"expires_in": 1800,
"interval": 5
}
Step 2 — Display to user:
The device displays the user_code and verification_uri:
To sign in, visit: https://auth.example.com/device
Enter code: WDJB-MJHT
Step 3 — User authenticates on a separate device:
The user opens the verification URI in a browser, enters the user code, authenticates, and approves the device.
Step 4 — Device polls for tokens:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code&
device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNh&
client_id=cli-tool
The device polls at the specified interval. Rampart responds with:
authorization_pending— user has not yet completed authorizationslow_down— the device is polling too frequently (back off by 5 seconds)expired_token— the device code has expiredaccess_denied— the user denied authorization- A token response — authorization was successful
OpenID Connect Layer
OIDC Discovery
Rampart publishes an OpenID Connect Discovery document (OpenID Connect Discovery 1.0) at:
GET /.well-known/openid-configuration
This document allows clients and libraries to auto-configure themselves. Key fields:
| Field | Value |
|---|---|
issuer | Your Rampart instance URL |
authorization_endpoint | /oauth/authorize |
token_endpoint | /oauth/token |
userinfo_endpoint | /oauth/userinfo |
jwks_uri | /.well-known/jwks.json |
device_authorization_endpoint | /oauth/device/code |
revocation_endpoint | /oauth/revoke |
introspection_endpoint | /oauth/introspect |
end_session_endpoint | /oauth/logout |
response_types_supported | ["code"] |
grant_types_supported | ["authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:device_code"] |
subject_types_supported | ["public"] |
id_token_signing_alg_values_supported | ["RS256"] |
scopes_supported | ["openid", "profile", "email", "offline_access"] |
code_challenge_methods_supported | ["S256"] |
token_endpoint_auth_methods_supported | ["client_secret_basic", "client_secret_post", "none"] |
claims_supported | ["sub", "iss", "aud", "exp", "iat", "email", "email_verified", "name", "given_name", "family_name", "roles", "org"] |
ID Tokens
When the openid scope is requested, Rampart issues an ID Token alongside the access token. The ID Token is a JWT containing identity claims about the authenticated user.
Example ID Token payload:
{
"iss": "https://auth.example.com",
"sub": "550e8400-e29b-41d4-a716-446655440000",
"aud": "my-client-id",
"exp": 1741176000,
"iat": 1741172400,
"auth_time": 1741172400,
"nonce": "abc123",
"at_hash": "77QmUPtjPfzWtF2AnpK9RQ",
"email": "jane@example.com",
"email_verified": true,
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe"
}
ID Token-specific claims:
| Claim | Description |
|---|---|
nonce | Echoed from the authorization request; the client must verify this matches |
auth_time | Time when the user last authenticated (Unix timestamp) |
at_hash | Access token hash — the left half of the SHA-256 hash of the access token, Base64URL-encoded |
amr | Authentication methods reference (e.g., ["pwd", "mfa"]) |
acr | Authentication context class reference |
ID Tokens are for the client, not for resource servers. Use access tokens to authorize API requests.
UserInfo Endpoint
The UserInfo endpoint returns claims about the authenticated user:
GET /oauth/userinfo
Authorization: Bearer <access_token>
Response:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "jane@example.com",
"email_verified": true,
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe"
}
The claims returned depend on the scopes granted to the access token.
Scopes and Claims
| Scope | Claims Included |
|---|---|
openid | sub, iss, aud, exp, iat — required for OIDC flows |
profile | name, given_name, family_name, preferred_username, updated_at |
email | email, email_verified |
offline_access | Grants a refresh token (required for long-lived sessions) |
Custom claims: Rampart includes roles and org claims in access tokens by default. Additional custom claims can be configured per client through claim mappers in the admin console.
Token Formats
JWT Structure
All JWTs issued by Rampart follow the compact serialization format: header.payload.signature.
Header:
{
"alg": "RS256",
"typ": "JWT",
"kid": "rampart-key-1"
}
| Field | Description |
|---|---|
alg | Signing algorithm — always RS256 |
typ | Token type — always JWT |
kid | Key ID — identifies which key from the JWKS was used to sign |
Standard claims (access token payload):
| Claim | Type | Description |
|---|---|---|
iss | string | Issuer — the Rampart instance URL |
sub | string | Subject — the user ID (UUID) or client ID |
aud | string/array | Audience — the intended recipients |
exp | number | Expiration time (Unix timestamp) |
iat | number | Issued at (Unix timestamp) |
nbf | number | Not before (Unix timestamp) |
jti | string | JWT ID — unique identifier for the token |
Rampart-specific claims:
| Claim | Type | Description |
|---|---|---|
email | string | User's email address |
roles | string[] | Assigned roles within the organization |
org | string | Organization identifier |
scope | string | Space-separated list of granted scopes |
client_id | string | The OAuth client that requested the token |
JWKS (JSON Web Key Set)
Public keys for token verification are published at:
GET /.well-known/jwks.json
Example response:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "rampart-key-1",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM...",
"e": "AQAB"
}
]
}
Key rotation: Rampart supports graceful key rotation. When a new signing key is generated, both the old and new keys are published in the JWKS. Tokens signed with the old key remain valid until they expire. The kid header in each JWT identifies which key was used.
OAuth Clients
OAuth clients are registered through the admin console or the Admin API. Each client has:
| Property | Description |
|---|---|
client_id | Unique public identifier |
client_secret | Secret for confidential clients (hashed at rest) |
client_name | Human-readable name |
redirect_uris | Allowed callback URLs (exact match, no wildcards) |
grant_types | Permitted flows (authorization_code, client_credentials, refresh_token, device_code) |
scopes | Allowed scopes |
token_endpoint_auth_method | How the client authenticates at the token endpoint |
client_type | public (SPA/mobile) or confidential (server-side) |
Public clients (SPAs, mobile apps, CLI tools) do not have a client secret and must use PKCE. Confidential clients (server-side apps) authenticate with a client secret and should also use PKCE.
Token Revocation and Introspection
Revocation
POST /oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=<access_token_or_refresh_token>&
token_type_hint=refresh_token&
client_id=my-app
Rampart accepts revocation requests for both access tokens and refresh tokens. Revocation of an access token adds it to a deny list for the remaining validity period. Revocation of a refresh token invalidates the entire token family.
Introspection
POST /oauth/introspect
Content-Type: application/x-www-form-urlencoded
Authorization: Basic BASE64(client_id:client_secret)
token=<access_token>
Response:
{
"active": true,
"sub": "550e8400-e29b-41d4-a716-446655440000",
"client_id": "my-app",
"scope": "openid profile email",
"exp": 1741176000,
"iat": 1741172400,
"iss": "https://auth.example.com",
"token_type": "Bearer"
}
Introspection is available to confidential clients for server-side token validation when JWT verification is not feasible.
Security Best Practices
- Always use PKCE, even for confidential clients — it prevents authorization code injection
- Use short-lived access tokens — 1 hour or less limits the damage window
- Store refresh tokens securely — server-side storage or encrypted, HttpOnly cookies
- Validate the
stateparameter — prevents CSRF attacks on the authorization flow - Verify the
noncein ID tokens — prevents replay attacks - Use exact redirect URI matching — Rampart does not support wildcard redirect URIs
- Rotate signing keys periodically — publish new keys via JWKS before retiring old ones
- Use
offline_accessexplicitly — refresh tokens are only issued when this scope is requested in OIDC flows