OAuth 2.1 and OIDC skill for Claude Code
PKCE, state and nonce, checked every single time. Keeps Claude on the authorization code + PKCE path for every client type, with state, nonce and issuer checks tied to the browser session. It validates ID tokens by signature, iss, aud and nonce, identifies users by issuer plus subject rather than email, keeps tokens out of the browser with a BFF, and audits existing setups against the misconfigurations behind real account takeovers.
- name
oauth-oidc - size168 lines · ≈2,100 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Implements and reviews OAuth 2.1 and OpenID Connect integrations — authorization code flow with PKCE, state and nonce, exact redirect URI matching, ID token and access token validation, scopes, refresh token rotation, BFF pattern for SPAs, and the misconfigurations behind account takeover. Use when adding "Sign in with Google/Microsoft/Okta", building an OAuth client or resource server, choosing a flow for SPA/mobile/backend, debugging invalid_grant or redirect_uri errors, or auditing an OAuth setup. Try asking
- Add Sign in with Google to our Express app using OIDC and PKCE
- Our token exchange keeps returning invalid_grant — figure out why
- Review how our SPA handles OAuth tokens and tell me what is unsafe
Use it when
- Adding social login or enterprise SSO through an OIDC provider
- Building an API that accepts OAuth access tokens
- Choosing a flow for SPA, mobile, CLI, or machine-to-machine clients
- Debugging redirect_uri_mismatch, invalid_grant, or state errors
Skip it when
- The work is JWT signing and verification mechanics alone — use jwt-handling instead
- You are wiring a hosted auth SDK such as Clerk or Auth0 — use auth-providers instead
How to check the result
A tampered state, a replayed authorization code, and an ID token with the wrong audience are all rejected, and no tokens appear in localStorage or URLs.
The common mistake it prevents
Linking accounts by the email claim instead of issuer plus subject, letting anyone with an unverified matching email at some IdP take over the account.
Done well means: every client uses authorization code + PKCE, redirect URIs match exactly, state and nonce are bound to the user’s browser session and checked, tokens are validated fully (signature, iss, aud, exp, nonce), access tokens never authenticate a user and ID tokens never authorize an API call, and tokens live where script cannot read them. Follow RFC 9700 (OAuth 2.0 Security Best Current Practice) where 2.1 is still draft.
Hard rules
- Use a certified, maintained library for the protocol. Hand-rolling is for understanding, not production.
- Implicit flow and Resource Owner Password Credentials are removed in OAuth 2.1. Do not add them; migrate existing uses.
- PKCE with
S256for every client, confidential ones included. - Register exact redirect URIs. No wildcards, no prefix matching, no open redirects on the callback path.
- OAuth is authorization. Login (“who is this user?”) requires OIDC and a validated ID token, or a userinfo call made with a token issued to your client.
Choose the flow
| Client | Flow | Token storage |
|---|---|---|
| Server-rendered web app | Auth code + PKCE, confidential client (client secret or private_key_jwt) | Server-side session; tokens never reach the browser |
| SPA | BFF: a backend-for-frontend does auth code + PKCE and issues an HttpOnly session cookie | Server-side (preferred). Pure-browser token handling only if no backend is possible: memory storage, short TTL, refresh rotation |
| Native / mobile | Auth code + PKCE via system browser (ASWebAuthenticationSession, Custom Tabs), claimed HTTPS or private-use redirect | OS keychain / Keystore |
| Machine to machine | Client credentials (secret, private_key_jwt, or mTLS) | Process memory / secret store |
| TV / CLI without browser | Device authorization grant (RFC 8628) | OS keychain |
Embedded webviews for login are banned by most IdPs and enable credential phishing.
Authorization code + PKCE: what each step must do
- Generate per-attempt secrets and store them server-side (or in an encrypted, HttpOnly
cookie) tied to this browser:
code_verifier: 43–128 chars from a CSPRNG.code_challenge = BASE64URL(SHA256(code_verifier)),code_challenge_method=S256.state: random, unguessable — CSRF protection for the callback.nonce(OIDC): random — binds the ID token to this request.
- Redirect to the authorize endpoint with
response_type=code,client_id,redirect_uri,scope(includeopenidfor OIDC),state,nonce,code_challenge,code_challenge_method. - On callback: if the response includes
iss(RFC 9207), check it equals the expected issuer (defends against mix-up attacks with multiple IdPs). Comparestateto the stored value; reject on mismatch or missing. Rejecterrorresponses explicitly. - Exchange the code at the token endpoint with
code_verifierand the sameredirect_uri. Codes are single-use and short-lived (typically ≤ 60 s). - Validate the ID token (below), then create your own session. Discard the stored verifier, state, and nonce.
Generating PKCE values (Python standard library):
import base64, hashlib, secrets
verifier = secrets.token_urlsafe(64)[:96]
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
state, nonce = secrets.token_urlsafe(32), secrets.token_urlsafe(32)
Node with openid-client v6:
import * as client from "openid-client";
const config = await client.discovery(new URL("https://accounts.example.com"), CLIENT_ID, CLIENT_SECRET);
// login route
const code_verifier = client.randomPKCECodeVerifier();
const code_challenge = await client.calculatePKCECodeChallenge(code_verifier);
const state = client.randomState();
const nonce = client.randomNonce();
req.session.oauth = { code_verifier, state, nonce };
const url = client.buildAuthorizationUrl(config, {
redirect_uri: REDIRECT_URI, scope: "openid email profile",
code_challenge, code_challenge_method: "S256", state, nonce,
});
res.redirect(url.href);
// callback route
const { code_verifier, state, nonce } = req.session.oauth;
const tokens = await client.authorizationCodeGrant(config, new URL(req.originalUrl, BASE_URL), {
pkceCodeVerifier: code_verifier, expectedState: state, expectedNonce: nonce,
});
const claims = tokens.claims(); // validated ID token claims
Python: use Authlib (authlib.integrations.flask_client / starlette_client) or
django-allauth; they handle state, nonce, and JWKS when configured with the IdP’s
server_metadata_url.
ID token validation (OIDC Core §3.1.3.7)
- Signature with a key from the issuer’s JWKS (
jwks_urifrom discovery), selected bykid; algorithm from an allowlist (e.g.RS256,ES256,PS256). Never acceptnone. issequals the discovered issuer exactly.audcontains yourclient_id; if multiple audiences,azpequals yourclient_id.expin the future,iatreasonable (allow ≤ 60 s clock skew).nonceequals the value you stored.- Identify users by
iss+sub, never byemail. Email can change and, for some IdPs and tenants, is not verified (email_verifiedfalse or absent).
Resource server (API) access token validation
- JWT access tokens (RFC 9068): verify signature via JWKS,
iss,audequals this API’s identifier,exp, and requiredscope/scpor roles for the operation. - Opaque tokens: call the introspection endpoint (RFC 7662) and cache the result briefly.
- Reject ID tokens presented as access tokens (
typheaderat+jwthelps;audcheck catches most cases). - Check scopes per endpoint and still do object-level authorization; a
read:invoicesscope does not mean every invoice.
Refresh tokens
- Public clients: refresh token rotation (new refresh token on every use; reuse of an old one revokes the family) or sender-constrained tokens (DPoP, RFC 9449).
- Set absolute lifetimes; revoke on logout (
revocation_endpoint, RFC 7009) and on password change at the IdP where supported. - Request
offline_accessonly when you need refresh without the user present.
Scopes and consent
- Request the minimum; add incremental scopes when the feature is used.
- Map scopes to API permissions explicitly in one place.
- For multi-tenant IdPs (Microsoft Entra
common, Google Workspace), validate the tenant (tid,hd) when only certain organizations may sign in.hdis a hint; confirm via the ID token claim, not the request parameter.
Common misconfigurations (audit list)
- [ ] Missing or unchecked state -> login CSRF (attacker logs victim into attacker account)
- [ ] Redirect URI wildcard / prefix match / open redirect on callback -> code theft
- [ ] No PKCE or `plain` method -> code interception
- [ ] ID token signature/iss/aud/nonce not checked, or decoded without verification
- [ ] Users matched by email instead of iss+sub -> account takeover via unverified email
- [ ] Access token from another client accepted as login (token substitution)
- [ ] Tokens in localStorage or URLs; tokens logged
- [ ] Client secret shipped in SPA or mobile binary (it is public)
- [ ] Refresh tokens long-lived without rotation for public clients
- [ ] Multi-IdP setup without issuer check on callback (mix-up)
- [ ] Logout clears local session but not IdP session when single logout is required
Debugging common errors
| Error | Usual cause |
|---|---|
redirect_uri_mismatch | Scheme, host, port, path, or trailing slash differs from registration |
invalid_grant on token exchange | Code reused or expired, code_verifier wrong/missing, redirect_uri differs from authorize request, clock skew |
invalid_client | Wrong auth method (client_secret_basic vs client_secret_post), secret rotated |
ID token aud mismatch | Validating with the wrong client ID or an API audience |
| State mismatch after redirect | Session cookie lost: SameSite=Strict on the session cookie blocks it on the IdP redirect; use Lax |
Verification
- Callback with a tampered or missing
stateis rejected. - Replaying a used authorization code fails.
- An ID token with wrong
aud, expiredexp, or a modified payload is rejected (unit test with a locally signed token and a test JWKS). - Inspect the browser: no tokens in
localStorage, URLs, or readable cookies for BFF/server apps.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.