Skip to main content

How to Implement OAuth Authentication

OAuth 2.0 flows for web applications -- authorization code grant, PKCE, token refresh, security considerations, and the patterns that prevent common auth vulnerabilities.

Category Guide
Read Time 9 min read
Updated August 2026
Steps 5 steps

Who This Guide Is For

This guide is for developers implementing OAuth 2.0 in a web application, either as a client (your app authenticates users via a third-party provider like Google, GitHub, or a custom OAuth server) or as a provider (your app issues tokens to third-party clients). It is part of our series of technical guides on web development. You understand HTTP, session management, and basic authentication concepts. You want to implement OAuth correctly rather than copy-pasting a tutorial and hoping the security details are right.

Before You Start

You should have a working web application with existing user authentication (or be building one) and a clear understanding of what OAuth is solving for you. OAuth is an authorisation framework, not an authentication protocol, though it is commonly used for authentication via extensions like OpenID Connect. Understand the difference: OAuth grants access to resources, OpenID Connect verifies identity. If you only need to verify that a user is who they claim to be, OpenID Connect on top of OAuth is what you want.

You should also have registered your application with the OAuth provider (for client implementations) or have the infrastructure to issue and validate tokens (for provider implementations). This guide focuses on the authorization code flow, which is the recommended flow for server-side web applications.

Step 1: Understand the Authorization Code Flow

The authorization code flow is the standard OAuth 2.0 flow for server-side applications. It involves four parties: the user, your application (the client), the authorization server, and the resource server. The flow has distinct phases, and understanding each phase is essential for a secure implementation.

Phase one: authorization request. Your application redirects the user’s browser to the authorization server’s authorization endpoint. The redirect URL includes your client ID (which identifies your application), the requested scopes (which define what access you are requesting), a redirect URI (where the authorization server will send the user back), and a state parameter (a random, unguessable string that prevents CSRF attacks).

The state parameter deserves emphasis. It is not optional. Generate a cryptographically random string, store it in the user’s session, and include it in the authorization request. When the authorization server redirects back to your application, verify that the state parameter in the response matches the one you stored. If it does not match, reject the response. It may be a CSRF attack.

Phase two: user authorization. The authorization server presents a consent screen to the user, showing what your application is requesting access to. The user approves or denies the request. This happens entirely on the authorization server; your application is not involved.

Phase three: authorization code. If the user approves, the authorization server redirects the user back to your application’s redirect URI with an authorization code and the state parameter. The authorization code is a short-lived, single-use token that your application exchanges for an access token.

Phase four: token exchange. Your application makes a server-to-server POST request to the authorization server’s token endpoint, sending the authorization code, your client ID, your client secret, and the redirect URI. The authorization server validates these, and if everything checks out, returns an access token (and optionally a refresh token and an ID token if using OpenID Connect).

This flow is secure because the access token is never exposed to the user’s browser. The browser only sees the authorization code, which is useless without the client secret. The client secret stays on your server.

Step 2: Implement PKCE for Additional Security

Proof Key for Code Exchange (PKCE, pronounced “pixie”) is an extension to the authorization code flow that protects against authorization code interception attacks. Originally designed for mobile and single-page applications that cannot safely store a client secret, PKCE is now recommended for all OAuth clients, including server-side applications.

How PKCE works: before starting the authorization flow, your application generates a random string called the code verifier. It then creates a code challenge by computing the SHA-256 hash of the code verifier and base64url-encoding the result. The code challenge is included in the authorization request. When exchanging the authorization code for tokens, your application sends the original code verifier. The authorization server hashes the verifier and compares it to the stored challenge. If they match, the exchange proceeds.

Why PKCE matters even for server-side apps: if an attacker intercepts the authorization code (through a compromised redirect URI, a malicious browser extension, or a man-in-the-middle on the redirect), they cannot exchange it for tokens without the code verifier, which never left your server. PKCE provides defence in depth even when the client secret is secure.

Implementation: generate a code verifier of at least 43 characters using a cryptographically secure random generator. Compute the SHA-256 hash and base64url-encode it (standard base64 with + replaced by -, / replaced by _, and padding removed). Store the verifier in the user’s session alongside the state parameter. Include both the code challenge and the challenge method (S256) in the authorization request.

Step 3: Handle Token Management

Access tokens expire. Refresh tokens enable your application to obtain new access tokens without requiring the user to re-authorise. Proper token management is the difference between a smooth user experience and one that constantly redirects users to log in again.

Store tokens securely. Access tokens and refresh tokens are credentials. Treat them with the same care as passwords. On the server side, store them encrypted in the database, associated with the user’s account. Never store tokens in cookies, local storage, or anywhere accessible to client-side JavaScript in a server-rendered application.

Implement automatic token refresh. When your application makes an API request and receives a 401 (Unauthorized) response, or when you detect that the access token has expired (by checking the expiry timestamp returned with the token), use the refresh token to obtain a new access token. This should be transparent to the user. They should not see a login screen because a token expired.

Handle refresh token rotation. Many OAuth providers issue a new refresh token each time you use the current one, invalidating the old refresh token. This limits the damage if a refresh token is compromised. The attacker can use it once, but the legitimate application’s next refresh attempt will fail, alerting you to the compromise. If the provider rotates refresh tokens, always store the new refresh token from the response.

Handle refresh failure gracefully. If the refresh token is expired, revoked, or invalid, the user must re-authorise. Redirect them to the authorization flow and preserve the URL they were trying to access so you can redirect them back after re-authorisation.

Token scoping limits the damage from a compromised token. Request only the scopes your application needs, not every scope available. If your application only reads user profile data, do not request write scopes. If you need different levels of access for different features, use multiple tokens with different scopes rather than one token with broad access.

Step 4: Secure the Implementation

OAuth implementations have a well-documented set of security considerations, and most OAuth vulnerabilities come from ignoring them rather than from flaws in the protocol itself.

Validate the redirect URI exactly. When registering your application with the OAuth provider, specify the exact redirect URI, including the scheme, host, port, and path. Do not use wildcard redirect URIs. The redirect URI is the target for the authorization code, and an open redirect vulnerability here allows an attacker to steal authorization codes.

Validate the state parameter on every callback. This prevents CSRF attacks where an attacker tricks a user into completing an OAuth flow that links the attacker’s account to the victim’s session. The state parameter must be unpredictable, stored server-side, and verified on the callback.

Use HTTPS for everything. The authorization code, access tokens, and redirect URIs must all use HTTPS. Running OAuth without HTTPS exposes every token and authorisation code to interception.

Validate ID tokens (if using OpenID Connect). Verify the token’s signature using the provider’s public keys, check the issuer claim matches the expected provider, check the audience claim matches your client ID, and check the expiry. Do not trust an ID token without validating it.

Revoke tokens when sessions end. When a user logs out of your application, revoke the access token and refresh token by calling the provider’s revocation endpoint (if one exists). This prevents the tokens from being used after the user expects to be logged out.

Step 5: Handle Edge Cases and Failure Modes

Production OAuth integrations encounter failures that tutorials never mention. Handling them determines whether your authentication system is frustrating or seamless.

Provider downtime. If the OAuth provider is unavailable, your users cannot log in. For applications where this is unacceptable, implement a fallback authentication method (local username/password) alongside OAuth. At minimum, detect provider downtime quickly and display a meaningful error rather than a generic server error.

Account linking. When a user authenticates via OAuth, they may already have an account in your system (created via a different OAuth provider or via local registration). Decide how to handle this: link the accounts automatically if the email matches, prompt the user to link accounts, or create a new account. Automatic linking based on email is convenient but only safe if the OAuth provider verifies the email (check the email_verified claim in OpenID Connect).

Scope changes. If your application’s required scopes change after users have already authorised, their existing tokens may not have the new scopes. Detect insufficient scopes (the API will typically return a 403 or a specific error code) and redirect the user through the authorization flow again to request the additional scopes.

Clock skew. Token expiry checks depend on time. If your server’s clock is out of sync with the provider’s clock, tokens may appear expired when they are not (or vice versa). Use NTP to keep your server’s clock synchronised, and add a small tolerance window (30 to 60 seconds) when checking token expiry.

Common Mistakes

  • Omitting the state parameter. This makes your OAuth implementation vulnerable to CSRF attacks. The state parameter is not optional. Treat it as a security requirement.
  • Storing tokens in browser-accessible storage. Access tokens in local storage or unencrypted cookies are vulnerable to XSS attacks. Store tokens server-side, encrypted.
  • Requesting excessive scopes. Requesting every available scope “just in case” means a compromised token grants far more access than necessary. Request only what you need.
  • Not implementing token refresh. Without automatic refresh, users are forced to re-authorise every time their access token expires. That is typically every hour, which makes for a poor experience.
  • Skipping PKCE. Even for server-side applications, PKCE provides meaningful additional security. It is a small implementation cost for significant protection against code interception.

What Good Looks Like

A well-implemented OAuth integration has: the authorization code flow with PKCE, a cryptographically random state parameter verified on every callback, tokens stored encrypted server-side with automatic refresh, scopes limited to what the application actually needs, HTTPS on all endpoints, token revocation on logout, and graceful handling of provider downtime and edge cases. The user experiences smooth authentication without unnecessary re-authorisation prompts, and the security properties hold even if the browser or network is partially compromised.

Next Steps

For securing the broader application that OAuth protects, How to Secure a Laravel Application covers authentication hardening and input validation. For the webhook integrations that often accompany OAuth (like receiving events from the service you authenticated against), How to Set Up Webhook Integrations covers reliable event processing. For API design that uses OAuth tokens for authorization, see How to Structure a REST API. For support building OAuth integrations as part of a larger project, see API Integrations.

Written by

Alex

CEO

I’m a software developer and CEO of Digital Royalty, helping growing teams scale their SaaS platforms without losing quality, visibility, or control. I focus on building structured, maintainable systems with clear processes, reporting, and accountability. With over a decade of experience across agency and in-house roles, I specialise in delivering long-term, scalable solutions that support complex, evolving products.

Portrait of Alexander De Sousa, founder of Digital Royalty
Founder-led
“I’ve put everything I know into how this company works — the standards, the method, the care on every project. It runs through the whole team, and I hold us all to it.”

Alexander De Sousa · Founder LinkedIn

Featured on BBC Radio Solent

Get started

Tell us what you need

A few quick questions, then a straight answer from a real person — usually within a few hours.

Tell us what you're working on

Whether it's a new site, a platform, or a process that shouldn't be manual any more — we'll tell you honestly if we can help.