OAuth 2.0 and OpenID Connect Security: A Practical Beginner's Guide
OAuth 2.0 and OpenID Connect (OIDC) let an application use an identity provider to sign users in and let applications access APIs without collecting a user’s password. They solve related but different problems: OAuth delegates authorization to an API, while OIDC adds a standard way for a client to authenticate a user. Knowing that distinction—and validating every redirect and token at the right boundary—is essential to a secure implementation.
This guide explains the protocol roles, the authorization-code flow with Proof Key for Code Exchange (PKCE), common alternatives, and practical controls for web, mobile, and service applications.
What Are OAuth 2.0 and OpenID Connect?
OAuth 2.0 is an authorization framework. A user can authorize a client application to request limited access to a resource, without giving that application the user’s password. The authorization server issues an access token, and the resource server uses that token to decide whether a request is allowed.
OpenID Connect is an identity layer built on OAuth 2.0. It uses the openid scope and returns an ID token that tells the client about an authentication event. An ID token is intended for the client; an access token is intended for an API. They are not interchangeable, even if both happen to be JSON Web Tokens (JWTs).
In short, authentication answers “who signed in?” and authorization answers “what may this client do?” OIDC handles the former, while OAuth access tokens support the latter.
The Problem They Solve
Without delegated authorization, an application might ask users for credentials belonging to another service, store those credentials, and use them to retrieve data. That increases the damage from a breach and makes it hard for a user to limit or revoke access.
OAuth replaces password sharing with a scoped grant: the user authenticates with an authorization server, which issues a token for a specific client and resource. OIDC standardizes how the client receives and verifies information about that sign-in.
These protocols do not make an application secure by themselves. A permissive scope, weak redirect handling, an exposed token, or incorrect token validation can still give an attacker access. The OAuth 2.0 Security Best Current Practice (RFC 9700) describes current threat mitigations and discourages older flows that expose tokens or credentials to clients.
How OAuth and OIDC Work: Architecture
The key participants are:
| Participant | Responsibility |
|---|---|
| Resource owner | The user whose protected data or account is involved |
| Client | The web, mobile, or service application requesting authorization |
| Authorization server | Authenticates the user, obtains consent when needed, and issues tokens |
| Resource server | Hosts an API and validates access tokens before serving protected data |
A typical interactive OIDC sign-in with OAuth authorization follows this sequence:
- The client creates a short-lived, single-use
statevalue and a PKCE verifier. For OIDC, it also creates anonce. It keeps these values associated with the sign-in transaction. - The client redirects the user’s browser to the authorization server with
response_type=code, a registeredredirect_uri, the requested scopes,state, a PKCE challenge, and—when using OIDC—nonce. - The authorization server authenticates the user and obtains consent or applies the user’s existing permissions.
- The authorization server redirects back with a short-lived authorization code. The client checks that the returned
statematches the transaction it started and that the callback is the expected one. - The client sends the code and PKCE verifier to the token endpoint. A confidential server-side client also authenticates itself there; a browser or native public client must not contain a client secret.
- The client validates the ID token if it requested OIDC. It sends the access token to the intended API, which validates that token independently before authorizing the request.
PKCE binds the authorization code to the client instance that began the flow: the client sends a hash of a secret verifier in the authorization request, then proves possession of the verifier at the token endpoint. An intercepted code alone is not enough to redeem it. Use the S256 challenge method and follow the PKCE specification (RFC 7636).
The tokens have different audiences and purposes:
| Token | Intended recipient | Safe handling rule |
|---|---|---|
| Access token | The resource server named by the token | Send only to that API; the API validates its signature or introspects it and checks its issuer, audience, expiry, and authorization |
| ID token | The OIDC client identified by its audience | Validate the signature and OIDC claims, including issuer, audience, expiry, and the request’s nonce; do not use it as an API access token |
| Refresh token | The authorization server’s token endpoint | Treat as a high-value credential; protect it, rotate or sender-constrain it when supported, and revoke it when a session ends or reuse is detected |
An access token may be an opaque value or a JWT. Clients should use the provider’s protocol library rather than assume a token format or decode a JWT and trust its claims without verification.
Components and OAuth Flow Variants
The appropriate flow depends on whether a user is present and whether the client can protect a credential:
| Flow | Typical use | Security guidance |
|---|---|---|
| Authorization Code with PKCE | Browser, native, and server-side applications acting for a user | Use PKCE; require exact registered redirect URIs; a confidential client also authenticates at the token endpoint |
| Client Credentials | Backend service calling an API without a user | Use only for service identity, protect client credentials, and limit scopes and token audience |
| Device Authorization | A device with limited input, such as a TV | Display the verification URI and user code securely, enforce expiry and polling limits, and bind approval to the requesting device |
| Implicit and Resource Owner Password Credentials | Legacy integrations | Avoid for new systems; migrate to Authorization Code with PKCE or an appropriate service flow |
An OIDC provider may publish a discovery document listing its authorization, token, and key endpoints. Clients can use the provider’s documented metadata and JWKS endpoint to find signing keys, but must still check that the issuer matches the configured provider. The OpenID Connect Core specification defines ID token validation and the OIDC claims.
Real-World Use Cases
- Sign in with an identity provider: A web application uses OIDC to authenticate a user and establishes its own application session after validating the ID token.
- A mobile app accessing a user’s account: Authorization Code with PKCE lets the app obtain a scoped API token without embedding a client secret.
- A service calling another service: Client Credentials authorizes a workload, not an end user. Restrict the token to the intended API and required permissions.
- A connected device: Device Authorization lets a user approve a sign-in from a second device, while short code lifetimes and polling limits reduce abuse.
- Enterprise single sign-on: An application delegates sign-in to an enterprise identity provider, then maps verified identity and groups to its own authorization policy.
Practical Security Guide
Generate per-request transaction values
Use a cryptographically secure random source for state, nonce, and the PKCE verifier. Do not reuse them across logins. Keep them bound to the initiating browser session, compare returned values exactly, and discard them after the transaction completes or expires.
This Node.js example creates PKCE values and a separate state and nonce. Use a maintained OAuth/OIDC client library to build and process the full protocol request in production.
import { createHash, randomBytes } from 'node:crypto';
function base64url(buffer) {
return buffer
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
const codeVerifier = base64url(randomBytes(32));
const codeChallenge = base64url(
createHash('sha256').update(codeVerifier).digest(),
);
const state = base64url(randomBytes(32));
const nonce = base64url(randomBytes(32));
The authorization request then includes response_type=code, the registered client_id and redirect_uri, the required scopes, state, code_challenge, and code_challenge_method=S256. Include nonce when requesting OIDC. Never place tokens or secrets in a URL.
Validate at the correct boundary
- At the callback: Match
stateto the outstanding sign-in, reject unexpected or reused authorization codes, and use the same registered redirect URI in the token exchange. - For an ID token: Use a vetted OIDC library to verify the signature against trusted issuer keys and validate
iss,aud,exp, and the originalnonce. Apply the specification’s additional audience checks when they are relevant. - For an access token: The resource server—not merely the frontend—checks the token’s issuer, audience, expiry, signature or introspection result, and required permissions. Pin accepted algorithms in trusted configuration rather than accepting an algorithm selected by an untrusted token.
- For scopes: Request only the permissions needed. The resource server must still enforce authorization for each operation; a scope is not a substitute for application-level access control.
The JWT Best Current Practices (RFC 8725) covers algorithm confusion, validation, and other JWT implementation risks. Opaque access tokens should be validated using the authorization server’s documented mechanism, such as introspection, rather than treated as JWTs.
Protect credentials and sessions
Keep confidential-client secrets and signing keys on trusted servers or in a secret manager; never ship them in a browser bundle or mobile application. Avoid logging authorization codes, access tokens, refresh tokens, or full callback URLs. Use TLS for every protocol endpoint and configure exact redirect URIs rather than wildcard patterns.
Keep access tokens short-lived where the provider and use case allow. Protect refresh tokens as credentials: use rotation or sender-constraining features when available, detect reuse, and revoke tokens when users sign out or access is withdrawn. Browser applications should carefully choose between a backend session and direct token handling, with explicit defenses against cross-site scripting and request forgery.
Common Misconceptions
- “OAuth is a login protocol.” OAuth delegates access to resources. Use OIDC when the client needs a standard user-authentication result.
- “An ID token is an API token.” Its audience is the OIDC client, not the API. Send an access token to the resource server and have that server validate it.
- “Every access token is a JWT.” Tokens can be opaque. The API must follow its provider’s validation contract instead of guessing from the token’s appearance.
- “A valid signature is enough.” A signed token can still be intended for another issuer, client, or API, be expired, or lack the permission required for an operation.
- “PKCE makes every flow safe.” PKCE protects code redemption; it does not repair open redirects, missing
statechecks, excessive scopes, unsafe token storage, or incorrect token validation. - “Scopes enforce authorization automatically.” Scopes describe delegated permissions. The resource server must enforce them alongside its own resource-level policies.
Related Articles
- For an implementation walkthrough covering provider setup and the authorization-code flow, see OAuth 2.0 and OpenID Connect implementation.
- For validating signed access tokens at an API, read JWT authentication for APIs.
- To compare OAuth and OIDC with sessions, passkeys, and other login approaches, see authentication methods for web applications.
- For API threat modeling and layered endpoint defenses, continue with API security best practices.
- For publishing a vulnerability-reporting contact, see the Security.txt setup guide.

