Helpful information ...
OAuth2 for Web Applications: A Practical Guide for Developers
OAuth2 for Web Applications: A Practical Guide for Developers
For modern web applications, the default choice is the Authorization Code flow with the PKCE extension, upgraded with OpenID Connect when you also need user identity data. Tokens should be in JWT format, stored outside localStorage, and refresh tokens should use rotation with the S256 method. Without these three elements, an implementation doesn't reach the standard we expect from a production system today.
In short:
- For a secure OAuth2 implementation, always use the Authorization Code flow with the PKCE extension, and store tokens properly, outside localStorage.
- JWT tokens are suited to stateless validation, while OIDC adds user identity when social login is needed.
- Never store tokens in localStorage; it's safer to use httpOnly, Secure, and SameSite cookies with refresh token rotation.
- When developing APIs for production, verify token signatures with JWKS, check their lifetime, and correctly set
issandaudfor protection.- Register your application with the server up front, configure CORS correctly, and use well-established libraries to reduce the risk of bugs and security holes.
Table of Contents
- When to use OAuth2, when JWT alone is enough, and when to add OIDC
- Recommended flows and PKCE: Authorization Code, Client Credentials, and when to use which
- Practical implementation for SPAs and backends: registration, token endpoint, CORS
- Token storage, refresh token rotation, and best practices for SPAs
- Security controls and advanced cases: JWKS, signed JWTs, aud/iss, rate limiting
- Common implementation mistakes and how to avoid them
- Why choose well-established libraries and an experienced partner
- The author's perspective: what really matters in an OAuth2 implementation
- How Moxy Web helps with a secure OAuth2 implementation
- Sources
- Frequently asked questions
When to use OAuth2, when JWT alone is enough, and when to add OIDC
OAuth2 is a framework for delegated authorization, not a login protocol. It solves a specific problem: how to let an application access resources on a user's behalf without ever seeing their password. This is essential for third-party integrations — for example, when an application reads a user's calendar through an external API.
JWT is a token format, not an authorization protocol in itself. RFC 9068 standardizes the use of JWT as an OAuth2 access token, which means the resource server can validate the token locally, without calling back to the authorization server. The upside is speed; the downside is that you can't immediately revoke it before it expires.
You add OIDC when you need an answer to "who is this user," not just "what can they access." OpenID Connect sits on top of OAuth2 and adds an ID token with user data, standardizing login.
In practice, this means:
- Use OAuth2 for API access and delegated authorization.
- Choose JWT as your token format when you want stateless validation without a session database.
- Add OIDC for any "sign in with Google" feature or similar social login, since without it you don't have reliable identity data.
Recommended flows and PKCE: Authorization Code, Client Credentials, and when to use which
The Authorization Code flow with the PKCE extension is today the only sensible choice for applications with a user interface, whether that's an SPA, a mobile app, or a classic server-rendered application. RFC 7636 describes a mechanism that prevents interception of the authorization code during the browser redirect.
The process works like this:
- The client generates a random
code_verifierstring and uses SHA-256 to derive acode_challengefrom it. - The user is redirected to the authorization server with the
code_challengeand theS256method included in the request. - After the user logs in, the server returns an authorization code to the registered
redirect_uri. - The client sends the code, along with the original
code_verifier, to the token endpoint. - The server verifies that the
code_verifiermatches the previously sentcode_challenge, and issues an access token and a refresh token.
The Client Credentials flow is meant for a completely different case: server-side services with no user context, such as communication between two microservices. There's no browser redirect involved, so PKCE isn't needed.
The Implicit flow and Resource Owner Password Credentials (ROPC) are no longer recommended. The Implicit flow exposes the token in the URL fragment without additional protection, and ROPC requires the application to see the user's password directly. The OAuth 2.1 draft formally removes both flows and makes PKCE a mandatory part of every Authorization Code flow.
Pro tip: Before writing your own logic to generate a code_verifier, check whether your OAuth client library for your language already does this automatically. Manual generation often results in strings that are too short or predictable.
Practical implementation for SPAs and backends: registration, token endpoint, CORS
Before you write a single line of code, you need to register your application with the authorization server. This includes:
- Defining the
redirect_uri, which must be an exact matching string, not just a wildcard pattern. - Obtaining a
client_id— for public clients (SPAs, mobile apps), without aclient_secret. - Restricting the allowed redirect domains to prevent open-redirect abuse.
Once the user finishes logging in, the SPA sends a POST request to the token endpoint with the authorization code, the code_verifier, and the client_id. This request goes directly from the browser, so the authorization server must correctly set CORS headers for your application's domain, otherwise the browser will block the request before it even reaches the server.
Your test environment should mirror the production domain as closely as possible, since differences in CORS configuration between local development and production often only show up once you go live.
Token storage, refresh token rotation, and best practices for SPAs
Never store tokens in localStorage. Any script that runs on the page due to an XSS vulnerability can read localStorage in its entirety. A safer approach is httpOnly, Secure, and SameSite cookies, which JavaScript can't access, and which the browser only sends to the correct domain over HTTPS.
Refresh token rotation means each refresh token can only be used once. With every exchange, the server issues a new refresh token and invalidates the old one. If someone steals an old token and tries to use it after the legitimate user has already replaced it, the system detects this as a sign of abuse and can revoke the entire session.
Access tokens should have a short lifetime, while refresh tokens can last longer, but with rotation on every use.
Pro tip: Set up automatic token refresh a few minutes before expiration, not only after receiving a 401 error. That way the user never notices any interruption to their session.

Security controls and advanced cases: JWKS, signed JWTs, aud/iss, rate limiting
A resource server must never blindly trust every token that arrives in a request header. Certain checks are mandatory:
- Signature validation via JWKS (JSON Web Key Set), which allows for asymmetric verification without sharing a secret key between services.
- Checking the
iss(issuer) andaud(intended audience) fields, so a token issued for one service can't be used on another. - Short token lifetimes combined with regularly refreshing public keys from the JWKS endpoint.
Real-world examples show that checking iss and aud together with short-lived tokens is one of the most reliable combinations for production APIs. Rate limiting and a web application firewall act as an additional layer — they don't replace correct token validation, but they complement it against automated attacks.
Common implementation mistakes and how to avoid them
Three things cause the most problems:
- Using the Implicit flow, or secrets in a public client. Public clients (SPAs, mobile apps) must never contain a
client_secretin their code, since it's always accessible to the user through the browser or by decompiling the app. - A missing
stateparameter. Without it, the application is vulnerable to CSRF attacks during the authorization flow; CORS settings also need to be checked before every deployment. - No revocation or logging. Without a mechanism for revoking tokens and logging unusual login patterns, an attack can often go unnoticed for weeks.
Why choose well-established libraries and an experienced partner
When developing web applications and online stores, we build security into the architecture from the start, not tacked on at the end of the project. For projects that require login through third-party providers or single sign-on within a company, our guide to integrating SSO with Microsoft Entra ID or our general overview of security protocols for web applications can help. For a technical audit of an existing implementation, you can reach out to the team directly.
The author's perspective: what really matters in an OAuth2 implementation
Developers often optimize for login speed first and token security second. That's the wrong order. Get PKCE, proper token storage, and rotation right first — only then think about user experience. Established libraries and automated security testing are cheaper than fixing an incident after a token has already leaked in production.
— Ziga
How Moxy Web helps with a secure OAuth2 implementation
Instead of building an OAuth2 flow from scratch and risking mistakes like the Implicit flow or secrets in a public client, you can leave the implementation to a team that gets the security architecture right the first time. When developing web applications, online stores, and integrations with third-party systems, we handle authorization flows, SSO connections, and technical audits of existing solutions, and we can also take care of hosting and maintenance after launch. If you need an audit of your current login implementation, or a new application with security built in from the start, start a conversation about your project at Moxy Web.
Sources
For further reading, see RFC 7636 for PKCE, RFC 9068 for the JWT profile for access tokens, and OpenID Connect Core for the identity layer. Practical examples of the Authorization Code flow with PKCE can be found in the Auth0 documentation, and general user trust in secure logins is also covered in this article on building trust online.
Frequently asked questions
What is PKCE and why is it mandatory?
PKCE is an extension to the Authorization Code flow that prevents interception of the authorization code by using a dynamically generated code_verifier and code_challenge. RFC 7636 describes it as the standard, and the OAuth 2.1 draft requires it for all clients.
What's the difference between OAuth2 and JWT?
OAuth2 is a framework for authorizing access, while JWT is a token format commonly used within an OAuth2 flow. RFC 9068 standardizes JWT as an access token for stateless validation.
Should I store tokens in localStorage?
No. Because of the risk of XSS attacks, httpOnly, Secure, and SameSite cookies — which JavaScript can't access — are the safer choice.
When should I use Client Credentials instead of Authorization Code?
Use Client Credentials for communication between server-side services with no user context, and Authorization Code with PKCE whenever a user login is involved.
How does Moxy Web help with an OAuth2 implementation?
When developing web applications, we build in secure authorization architecture, SSO integrations, and technical audits of existing implementations, which reduces the risk of security issues after launch.
Recommended