Claude Fable 5.1 & GPT-6 Astra packages are live

Oauth

Free

Implementing OAuth 2.1 and OIDC correctly — authorization code with PKCE, redirect URI exactness, state, and the flows that are now forbidden.

206 lines8.1 KB Mistral Security
targetModels
Mistral Medium 3.5Mistral Large 3Mistral Small 4Mistral FamilyFuture Mistral Models
name
oauth
category
Security
description
Implementing OAuth 2.1 and OIDC correctly — authorization code with PKCE, redirect URI exactness, state, and the flows that are now forbidden.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for Mistral: scripts/model-profiles.json -->

#How to apply this file

Each section opens with one imperative line; apply every rule in the section it introduces. Do not summarise or skip a section.


#Purpose

Rules for OAuth 2.0/2.1 and OpenID Connect as a client and as a provider.

The distinction that prevents most mistakes: OAuth is authorisation (delegated access). OIDC is authentication (who the user is). An access token says a client may call an API. It does not say who is logged in. If you need identity, use OIDC and validate the ID token.


#Use authorization code with PKCE. Nothing else.

[INST] Apply every rule in this section: Use authorization code with PKCE. Nothing else.. [/INST]

ini
GET /authorize
  ?response_type=code
  &client_id=abc123
  &redirect_uri=https://app.example.com/callback
  &scope=openid%20profile%20email
  &state=<csprng>
  &nonce=<csprng>
  &code_challenge=<BASE64URL(SHA256(verifier))>
  &code_challenge_method=S256

PKCE is required for every client type in OAuth 2.1, including confidential server-side clients. It binds the authorization code to the client that started the flow, so an intercepted code cannot be redeemed by anyone else.

js
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
// Store `verifier` server-side against the session; send only `challenge`.

Never use code_challenge_method=plain. Always S256.

#Flows that are removed or forbidden

[INST] Apply every rule in this section: Flows that are removed or forbidden. [/INST]

FlowStatus
Implicit (response_type=token)Removed in 2.1. Token in the URL fragment leaks via history, Referer and logs
Resource Owner Password CredentialsRemoved in 2.1. The client handles the password; defeats MFA and federation
Authorization code without PKCEForbidden — code interception
Client credentialsValid, but machine-to-machine only. Never for a user session

#Redirect URI

[INST] Apply every rule in this section: Redirect URI. [/INST]

The redirect URI is the most attacked parameter in OAuth.

  • Register the exact, full URI. Compare by exact string match.
  • Never allow wildcards, prefix matching, or open subpaths. https://app.example.com/cb must not match https://app.example.com/cb/../../evil or https://app.example.com.evil.tld/cb.
  • Never reflect a redirect target from a query parameter after the callback — that reintroduces an open redirect and leaks the code.
  • Require HTTPS. http://localhost may be permitted for development only, with the port ignored per the native-app guidance.

#state and nonce

[INST] Apply every rule in this section: state and nonce. [/INST]

Both are required and they do different jobs.

ParameterPurposeValidated
stateCSRF protection for the callbackCompare to the value stored in the session
nonceReplay protection for the ID tokenCompare to the nonce claim in the ID token

Generate both with a CSPRNG, bind them to the session, and make them single-use.

Never skip state because "the code is single-use". Without it, an attacker completes a flow with their own code in the victim's browser and links the victim's session to the attacker's account.


#Validating tokens

[INST] Apply every rule in this section: Validating tokens. [/INST]

An ID token is a JWT and must be validated as one — see Security/jwt:

  • Signature against the provider's JWKS, with an explicit algorithms allow-list
  • iss exactly equals the provider's issuer
  • aud contains your client_id
  • exp not passed, iat reasonable
  • nonce matches the value you sent
  • azp equals your client_id when present

An access token is opaque to the client. Never parse it, and never make authorisation decisions from its contents in the client. On the resource server, validate it by introspection or as a JWT with full claim validation, and check the scope and aud are for your API.

Never use the /userinfo response as proof of authentication on its own — it is fetched with an access token that may have been issued to a different client. That is the confused-deputy problem OIDC's ID token exists to solve.


js
// Redeem the code. The verifier proves this is the client that began the flow;
// without PKCE an intercepted `code` would be enough on its own.
const res = await fetch(`${ISSUER}/token`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    redirect_uri: REDIRECT_URI,          // must match the authorize request exactly
    client_id: CLIENT_ID,
    code_verifier: session.pkceVerifier,
  }),
});

const { id_token, access_token } = await res.json();
const claims = await verifyIdToken(id_token, { nonce: session.nonce });

#Tokens, scopes and storage

[INST] Apply every rule in this section: Tokens, scopes and storage. [/INST]

  • Request the narrowest scopes that work, and request them incrementally as features need them.
  • Access tokens short-lived (5–15 minutes); refresh tokens rotated on use, with reuse of a consumed refresh token revoking the whole family.
  • In a browser, keep tokens in an HttpOnly; Secure; SameSite cookie via a backend-for-frontend, not in localStorageSecurity/xss.
  • Store the client secret server-side only. A "confidential" client in a SPA or mobile app is a public client — the secret ships to every user.
  • Support revocation (/revoke) and honour it on logout. Clearing the client's copy is not revocation.

#Anti-patterns

[INST] Apply every rule in this section: Anti-patterns. [/INST]

Anti-patternWhy it failsFix
Implicit flowToken in the URL fragment; removed in 2.1Code + PKCE
Password grantClient sees the password; defeats MFACode + PKCE
Code flow without PKCEIntercepted code is redeemableAlways S256
Wildcard or prefix redirect matchingCode sent to an attacker originExact string match
Omitting stateSession fixation via the callbackCSPRNG, session-bound, single-use
Treating an access token as identityIt authorises; it does not authenticateValidate the ID token
Parsing an access token in the clientOpaque by contract; format may changeUse the ID token or /userinfo
/userinfo alone as login proofConfused deputy across clientsValidate the ID token's aud
Client secret in a SPA or mobile appShipped to every userPublic client + PKCE
Tokens in localStorageAny XSS becomes account takeoverHttpOnly cookie via BFF

#Checklist

  • Verify: Authorization code with PKCE S256 is the only user-facing flow
  • Verify: Implicit and password grants are disabled
  • Verify: Redirect URIs are registered in full and compared by exact string match
  • Verify: state and nonce are CSPRNG-generated, session-bound and single-use
  • Verify: ID token signature, iss, aud, exp and nonce are all validated
  • Verify: The resource server validates scope and aud for its own API
  • Verify: Access tokens are never parsed by the client
  • Verify: Scopes requested are the narrowest that work
  • Verify: Access tokens expire in 5–15 minutes; refresh tokens rotate on use
  • Verify: Refresh-token reuse revokes the family
  • Verify: No client secret exists in any browser or mobile bundle
  • Verify: Logout calls the revocation endpoint