jpesewang.work
HomeProjectsBlogContact
Back to Blog
Authentication Strategies in Modern Web Applications
Leif Le
September 8, 2025
20 min read

Authentication Strategies in Modern Web Applications

AuthenticationSecurity

Authentication is at the core of every modern web application. Whether you’re building an e-commerce platform, a SaaS product, or an internal dashboard, ensuring secure and seamless login experiences is critical. Today, we’ll explore common authentication strategies—JWT, OAuth, and SSO—and break down how tokens flow from frontend (FE) to backend (BE) in practice.

1. Why Authentication Matters

Authentication is the process of verifying who a user is. In modern systems, this is often paired with authorization—deciding what that user can access. Without a robust authentication strategy, you risk:

  1. Unauthorized data access 🚨
  2. Poor user experience (too many logins, weak session handling)
  3. Vulnerabilities like token theft, replay attacks, or CSRF


2. Authentication Strategies

a) JWT (JSON Web Token)

JWT is one of the most widely used authentication methods today.

  1. How it works:
  2. User logs in with credentials (e.g., email + password).
  3. Backend verifies the credentials and issues a JWT, signed with a secret/private key.
  4. Frontend stores the token (usually in HTTP-only cookies or secure storage).
  5. On each request, the frontend attaches the token (commonly via Authorization: Bearer <token> header).
  6. Backend verifies the signature and decodes the token to identify the user.
  7. Pros:
  8. Stateless (no need for backend session storage).
  9. Works well with APIs and microservices.
  10. Cons:
  11. Revocation is tricky (need token blacklist or short lifetimes).
  12. If stored insecurely (e.g., localStorage), tokens can be stolen via XSS.

b) OAuth 2.0

OAuth is an authorization framework widely used for third-party login (Google, Facebook, GitHub sign-in).

  1. How it works (Authorization Code Flow):
  2. User clicks "Login with Google".
  3. Frontend redirects the user to Google’s OAuth server.
  4. User grants permission.
  5. Google redirects back with an authorization code.
  6. Frontend sends this code to your backend.
  7. Backend exchanges the code for an access token (and sometimes a refresh token).
  8. Backend uses the access token to fetch user info from Google and create a session.
  9. Pros:
  10. Users don’t need new passwords → better UX.
  11. Secure delegation of access.
  12. Cons:
  13. More complex than JWT-only auth.
  14. Requires external identity providers.

c) SSO (Single Sign-On)

SSO allows users to authenticate once and access multiple applications without logging in again. Often powered by SAML, OAuth, or OpenID Connect (OIDC).

  1. How it works:
  2. User logs into an Identity Provider (IdP) (e.g., Okta, Azure AD).
  3. IdP issues a token/assertion (JWT, SAML).
  4. Frontend apps in the same ecosystem accept that token and establish sessions.
  5. Pros:
  6. One login → access to many apps.
  7. Great for enterprise environments.
  8. Cons:
  9. Setup complexity.
  10. Central IdP becomes a single point of failure.


3. How Tokens Work: FE → BE Flow

Using JWT with cookies is one of the most common flows. Instead of storing tokens in localStorage (which can be exposed to XSS), you use HTTP-only cookies—they can’t be accessed via JavaScript, which makes them safer.


Here’s how the lifecycle looks:

Step 1: User Logs In (FE → BE)

The frontend sends credentials to the backend:


// frontend (Next.js API call)
const login = async (email: string, password: string) => {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
credentials: "include", // important: allow cookies
});
return res.json();
};

Step 2: Backend Verifies & Sets JWT in Cookie

On the backend, after verifying the credentials, a JWT is generated and stored in an HTTP-only cookie:


// backend (Next.js route handler - /app/api/auth/login/route.ts)
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";

export async function POST(req: Request) {
const { email, password } = await req.json();

// mock validation
if (email !== "user@example.com" || password !== "123456") {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}

// create JWT
const token = jwt.sign(
{ email, role: "user" },
process.env.JWT_SECRET!,
{ expiresIn: "15m" }
);

// set HTTP-only cookie
const res = NextResponse.json({ success: true });
res.cookies.set("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 60 * 15, // 15 minutes
path: "/",
});

return res;
}

Step 3: Frontend Makes Authenticated Requests

Because the JWT is stored in a cookie, you don’t manually attach the token—the browser includes it automatically.


// frontend fetching protected data
const fetchProfile = async () => {
const res = await fetch("/api/user/profile", {
method: "GET",
credentials: "include", // ensure cookies are sent
});
return res.json();
};

Step 4: Backend Verifies Token on Protected Route

Each request to a protected route will check the cookie:


// backend (Next.js route handler - /app/api/user/profile/route.ts)
import { NextRequest, NextResponse } from "next/server";
import jwt from "jsonwebtoken";

export async function GET(req: NextRequest) {
const token = req.cookies.get("token")?.value;

if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
return NextResponse.json({ user: decoded });
} catch (err) {
return NextResponse.json({ error: "Invalid or expired token" }, { status: 403 });
}
}

Step 5: Token Expiry & Refresh

  1. Access tokens (JWT) should be short-lived (e.g., 15 minutes).
  2. Use a refresh token stored in another secure cookie to issue new JWTs without requiring login every time.

✅ This approach:

  1. Keeps JWTs out of localStorage (mitigates XSS).
  2. Relies on cookies → automatically included in requests.
  3. Works well with Next.js (App Router).



4. Best Practices

  1. ✅ Use short-lived access tokens + refresh tokens.
  2. ✅ Store tokens in HTTP-only cookies to mitigate XSS.
  3. ✅ Use HTTPS everywhere to prevent MITM attacks.
  4. ✅ Implement logout + token revocation flows.
  5. ✅ Consider PKCE flow for SPAs when using OAuth.


5. Choosing the Right Strategy

  1. Small apps / APIs → JWT works well.
  2. Apps needing external logins (Google, GitHub, etc.) → OAuth 2.0 / OIDC.
  3. Enterprise / multi-app ecosystems → SSO with IdP integration.


🚀 Conclusion

Authentication is more than just login—it’s about creating secure, scalable, and user-friendly experiences. By understanding JWT, OAuth, and SSO, and how tokens flow between frontend and backend, you can design systems that balance security with usability.

The “best” approach depends on your context, but the principles remain:

  1. Protect tokens
  2. Keep sessions short-lived
  3. Use trusted identity providers when possible

Related Articles

TypeScript Best Practices for Large Applications

Essential TypeScript patterns and practices that will help you build maintainable applications.

Jan 5, 202410 min read

React Performance Optimization Techniques

Learn advanced techniques to optimize your React applications for better performance.

Dec 28, 202315 min read

Enjoyed this article?

Subscribe to get notified about new articles and updates.