<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged jwt</title>
<link>https://coderlegion.com/tag/jwt</link>
<description>Powered by Question2Answer</description>
<item>
<title>What Is Authentication? A Beginner's Guide (2026)</title>
<link>https://coderlegion.com/25969/what-is-authentication-a-beginners-guide-2026</link>
<description>&lt;p&gt;Every time you type your email and password into a login form, something is quietly deciding whether you actually get in. That decision — proving you are who you say you are — is called &lt;strong&gt;authentication&lt;/strong&gt;, and it's one of those concepts every developer touches constantly but rarely stops to explain from scratch.&lt;/p&gt;
&lt;p&gt;If you've ever wondered what actually happens between &quot;click Sign In&quot; and &quot;welcome back,&quot; or mixed up &lt;em&gt;authentication&lt;/em&gt; with &lt;em&gt;authorization&lt;/em&gt; in an interview, this guide is for you. By the end, you'll know exactly what authentication is, how the major methods work under the hood, and how to build a basic authentication flow yourself.&lt;/p&gt;
&lt;h3 id=&quot;what-is-authentication-quick-answer-&quot;&gt;What Is Authentication? (Quick Answer)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Authentication is the process of verifying that someone is who they claim to be.&lt;/strong&gt; It answers one question: &lt;em&gt;&quot;Who are you?&quot;&lt;/em&gt; This usually happens by checking a password, a code from your phone, a fingerprint, or a token — something only the real user should have or know.&lt;/p&gt;
&lt;p&gt;Authentication is different from &lt;strong&gt;authorization&lt;/strong&gt;, which answers a separate question: &lt;em&gt;&quot;What are you allowed to do?&quot;&lt;/em&gt; A logged-in user is authenticated. Whether that user can delete another person's account is authorization. Mixing these two up is one of the most common beginner mistakes — keep them mentally separate from the start.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Simple way to remember it:&lt;/strong&gt; Authentication = ID check at the door. Authorization = the guest list that decides which rooms you can enter once you're inside.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;why-authentication-matters&quot;&gt;Why Authentication Matters&lt;/h3&gt;
&lt;p&gt;Without proper authentication and authorization, an app that stores personal data could expose private information to unauthorized users. Authentication provides the identity context that many authorization, personalization, and auditing systems rely on.&lt;/p&gt;
&lt;p&gt;It matters for a few concrete reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt; — only the right person sees their own data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt; — prevents account takeover and unauthorized access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personalization&lt;/strong&gt; — apps can remember who you are across visits&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accountability&lt;/strong&gt; — actions can be traced back to a real identity (important for admin tools, financial apps, audit logs)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;authentication-vs-authorization-the-difference-that-trips-everyone-up&quot;&gt;Authentication vs. Authorization: The Difference That Trips Everyone Up&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Authentication &lt;/th&gt;&lt;th&gt;Authorization&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Question it answers &lt;/td&gt;&lt;td&gt;Who are you? &lt;/td&gt;&lt;td&gt;What can you do?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Happens &lt;/td&gt;&lt;td&gt;First &lt;/td&gt;&lt;td&gt;After authentication&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Example &lt;/td&gt;&lt;td&gt;Logging in with a password &lt;/td&gt;&lt;td&gt;Only admins can access &lt;code&gt;/admin&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Failure result &lt;/td&gt;&lt;td&gt;401 Unauthorized &lt;/td&gt;&lt;td&gt;403 Forbidden&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Why the HTTP status codes are confusingly named:&lt;/strong&gt; &lt;code&gt;401 Unauthorized&lt;/code&gt; actually means &quot;you're not authenticated&quot; (log in first), while &lt;code&gt;403 Forbidden&lt;/code&gt; means &quot;you're authenticated, but not allowed to do this.&quot; Yes, &lt;code&gt;401&lt;/code&gt; should really be called &quot;Unauthenticated&quot; — it's a long-standing naming quirk in the HTTP spec that trips up almost every developer at least once.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Related: Role-Based Access Control (RBAC) in Next.js 16&lt;/a&gt; goes deep on the authorization half of this once you're ready for it.&lt;/p&gt;
&lt;h3 id=&quot;how-authentication-works-step-by-step-&quot;&gt;How Authentication Works (Step by Step)&lt;/h3&gt;
&lt;p&gt;At a high level, almost every authentication system follows the same basic loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The user provides credentials&lt;/strong&gt; — a password, a fingerprint, a one-time code, or an existing account from another provider (like signing in with Google)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The server verifies those credentials&lt;/strong&gt; — checking a hashed password, validating a signed token, or confirming a biometric match&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The server issues proof of identity&lt;/strong&gt; — usually a session or a token, stored in a cookie or sent back to the client&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;That proof is sent with future requests&lt;/strong&gt; — so the user doesn't have to log in again on every page&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;User submits credentials
        ↓
Server verifies credentials
        ↓
Server issues session/token
        ↓
Client stores it (cookie)
        ↓
Future requests include it
        ↓
Server verifies it's still valid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last step — verifying the proof of identity on every request — is what keeps a user &quot;logged in&quot; without asking for their password on every single page.&lt;/p&gt;
&lt;h3 id=&quot;the-main-types-of-authentication&quot;&gt;The Main Types of Authentication&lt;/h3&gt;
&lt;p&gt;There isn't just one way to authenticate someone. Here are the methods you'll actually run into as a developer.&lt;/p&gt;
&lt;h4 id=&quot;1-password-based-authentication&quot;&gt;1. Password-Based Authentication&lt;/h4&gt;
&lt;p&gt;The classic: a username/email plus a password, checked against a hashed value stored in your database. Never store plain-text passwords — always hash them with a slow, password-specific hashing algorithm such as &lt;strong&gt;Argon2id&lt;/strong&gt;. If it's not available, &lt;strong&gt;bcrypt&lt;/strong&gt; or another appropriate password hashing algorithm can be used.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/hash-password.ts
import bcrypt from &quot;bcryptjs&quot;;

const SALT_ROUNDS = 12;

export async function hashPassword(plainPassword: string): Promise&amp;lt;string&amp;gt; {
  return bcrypt.hash(plainPassword, SALT_ROUNDS);
}

export async function verifyPassword(
  plainPassword: string,
  hashedPassword: string
): Promise&amp;lt;boolean&amp;gt; {
  return bcrypt.compare(plainPassword, hashedPassword);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;bcrypt.hash()&lt;/code&gt; doesn't just scramble the password — it applies a slow, computationally expensive algorithm on purpose, which increases the cost of each password guess and makes large-scale brute-force attacks significantly more expensive. &lt;code&gt;SALT_ROUNDS = 12&lt;/code&gt; is above OWASP's minimum bcrypt work factor of 10, but the right value depends on your server's hardware and traffic. Benchmark your production environment and choose a cost that makes password verification sufficiently expensive without creating unacceptable latency.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;bcrypt limitation:&lt;/strong&gt; bcrypt has a hard 72-byte input limit, and most implementations (including &lt;code&gt;bcryptjs&lt;/code&gt;) silently truncate anything past it rather than erroring. Because UTF-8 characters like emoji or accented letters can take up to 4 bytes each, a password that looks well under the limit in &lt;em&gt;characters&lt;/em&gt; can still get silently cut short in &lt;em&gt;bytes&lt;/em&gt; — which can cause two different passwords to be treated as equivalent after truncation.&lt;br&gt;
OWASP currently recommends enforcing a maximum of 72 bytes (or less, depending on the implementation) when bcrypt is used. Because UTF-8 characters can occupy multiple bytes, a character-count limit alone doesn't necessarily correspond to bcrypt's byte limit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Comparing passwords with a plain &lt;code&gt;===&lt;/code&gt; check after &quot;encrypting&quot; them with something reversible like Base64. That's encoding, not hashing — it can be reversed instantly. Only use a dedicated password-hashing library.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;2-session-based-authentication&quot;&gt;2. Session-Based Authentication&lt;/h4&gt;
&lt;p&gt;After a successful login, the server creates a &lt;strong&gt;session&lt;/strong&gt; record stored in server-side session storage such as a database or shared cache — and gives the browser a random session ID in an &lt;code&gt;httpOnly&lt;/code&gt; cookie. On every request, the server looks up that ID to know who's logged in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/session.ts
import { cookies } from &quot;next/headers&quot;;
import { randomUUID } from &quot;crypto&quot;;

export async function createSession(userId: string, expiresAt: Date) {
  // Generate a random, unguessable session ID — never store the raw
  // userId (or anything else predictable) directly as the cookie's
  // value. The cookie is a reference you look up server-side; it
  // should never double as proof of identity on its own.
  const sessionId = randomUUID();

  // Persist the mapping server-side, e.g.:
  // await db.session.create({ data: { sessionId, userId, expiresAt } });

  const cookieStore = await cookies();

  cookieStore.set(&quot;session_id&quot;, sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: &quot;lax&quot;,
    expires: expiresAt,
    path: &quot;/&quot;,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why each flag matters:&lt;/strong&gt; &lt;code&gt;httpOnly&lt;/code&gt; stops client-side JavaScript from reading the cookie (blocking a common XSS attack path). &lt;code&gt;secure&lt;/code&gt; means it's only sent over HTTPS. &lt;code&gt;sameSite: &quot;lax&quot;&lt;/code&gt; limits when the browser sends the cookie in cross-site requests and provides some CSRF protection. For state-changing operations, you should still implement an appropriate CSRF defense rather than relying on SameSite alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; straightforward server-side revocation; deleting a session record can invalidate that session immediately, and deleting all of a user's sessions can log them out everywhere.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; can require a server-side session lookup on authenticated requests, although caching can reduce the cost.&lt;/p&gt;
&lt;h4 id=&quot;3-token-based-authentication-jwt-&quot;&gt;3. Token-Based Authentication (JWT)&lt;/h4&gt;
&lt;p&gt;Instead of looking anything up server-side, a &lt;strong&gt;JSON Web Token (JWT)&lt;/strong&gt; packs the user's identity directly into a signed token. In a stateless JWT design, the server can verify the token cryptographically without looking up a session record in the database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/auth/jwt.ts
import { SignJWT, jwtVerify } from &quot;jose&quot;;

const secretKey = new TextEncoder().encode(process.env.AUTH_SECRET);

export async function signToken(userId: string, expiresAt: Date) {
  return new SignJWT({})
    .setProtectedHeader({ alg: &quot;HS256&quot; })
    .setSubject(userId)
    .setIssuedAt()
    .setExpirationTime(expiresAt)
    .sign(secretKey);
}

export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secretKey, {
      algorithms: [&quot;HS256&quot;],
    });
    return payload;
  } catch {
    return null; // invalid or expired
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;jose&lt;/code&gt; is the standard, actively maintained library for signing and verifying JWTs using the Web Crypto API — it works in Node.js, edge runtimes, and browsers. &lt;code&gt;setSubject(userId)&lt;/code&gt; stores the user's ID in the token's standard &lt;code&gt;sub&lt;/code&gt; claim. &lt;code&gt;jwtVerify()&lt;/code&gt; automatically rejects the token if the signature doesn't match or if it's expired, so you never write manual expiry checks.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Signing is not encryption.&lt;/strong&gt; A JWT's payload is only base64url-encoded, not encrypted — anyone can decode and read it. The signature only proves it wasn't tampered with. Never put sensitive data (passwords, secrets) inside a JWT payload.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; no database lookup needed to verify identity, works well across distributed/stateless servers.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; harder to instantly revoke a single token before it expires (it's valid until it naturally expires unless you build extra machinery around it).&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 For a deeper side-by-side of these two models — including how each behaves at scale — see &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/session-vs-jwt-for-nextjs-16&quot;&gt;Session vs JWT: Which Should You Use in Next.js 16?&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;4-oauth-openid-connect-social-login-&quot;&gt;4. OAuth / OpenID Connect (Social Login)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;OAuth 2.0&lt;/strong&gt; is an authorization framework that lets an application obtain limited access to resources on behalf of a user. It doesn't, by itself, define how an application verifies the user's identity.&lt;/p&gt;
&lt;p&gt;For &quot;Sign in with Google,&quot; &quot;Sign in with Microsoft,&quot; and similar login flows, applications commonly use &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt; — an identity layer built on top of OAuth 2.0. The identity provider authenticates the user and returns an &lt;strong&gt;ID token&lt;/strong&gt; containing claims about that authentication.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;User clicks &quot;Sign in with GitHub&quot;
        ↓
Redirected to GitHub
        ↓
User authenticates and approves access
        ↓
GitHub redirects back with a code
        ↓
Your server exchanges the code for tokens
        ↓
Your server validates the OIDC identity information
        ↓
Your app creates its own session
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is why OAuth is so popular for beginners and production apps alike — you outsource the hardest parts (password storage, breach risk, 2FA) to a provider that already does it well.&lt;/p&gt;
&lt;h4 id=&quot;5-multi-factor-authentication-mfa-2fa-&quot;&gt;5. Multi-Factor Authentication (MFA / 2FA)&lt;/h4&gt;
&lt;p&gt;MFA requires &lt;strong&gt;more than one&lt;/strong&gt; proof of identity — typically &quot;something you know&quot; (a password) plus &quot;something you have&quot; (a phone, an authenticator app code, a hardware key). Even if a password leaks, the attacker still can't get in without the second factor.&lt;/p&gt;
&lt;h4 id=&quot;6-passwordless-authentication-magic-links-amp-passkeys-&quot;&gt;6. Passwordless Authentication (Magic Links &amp;amp; Passkeys)&lt;/h4&gt;
&lt;p&gt;Two increasingly common alternatives to passwords:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Magic links&lt;/strong&gt; — the user enters their email, gets a one-time sign-in link, and clicking it logs them in. No password to remember or leak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Passkeys (WebAuthn)&lt;/strong&gt; — device-based authentication using Face ID, Windows Hello, or a hardware security key. No shared secret exists to steal in the first place, which makes passkeys resistant to phishing in a way passwords fundamentally aren't.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-minimal-authentication-flow-in-next-js&quot;&gt;A Minimal Authentication Flow in Next.js&lt;/h3&gt;
&lt;p&gt;Here's a simplified but realistic login flow using &lt;strong&gt;Next.js 16&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, and session cookies — the same core pattern used by most production apps, just without the database wiring.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// actions/login.ts
&quot;use server&quot;;

import { redirect } from &quot;next/navigation&quot;;
import { verifyPassword } from &quot;@/lib/auth/hash-password&quot;;
import { createSession } from &quot;@/lib/auth/session&quot;;
import { z } from &quot;zod&quot;;

const loginSchema = z.object({
  email: z.email(),
  password: z.string().min(8),
});

export async function login(prevState: unknown, formData: FormData) {
  const parsed = loginSchema.safeParse({
    email: formData.get(&quot;email&quot;),
    password: formData.get(&quot;password&quot;),
  });

  if (!parsed.success) {
    return { error: &quot;Enter a valid email and password.&quot; };
  }

  // Look up the user in your database (pseudocode)
  const user = await db.user.findUnique({ where: { email: parsed.data.email } });

  if (!user || !(await verifyPassword(parsed.data.password, user.passwordHash))) {
    // Same generic error for both cases — don't reveal which one was wrong
    return { error: &quot;Invalid email or password.&quot; };
  }

  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
  await createSession(user.id, expiresAt);

  redirect(&quot;/dashboard&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; the form data is validated with Zod before touching the database — never trust raw input. Notice the login failure returns the &lt;strong&gt;same generic message&lt;/strong&gt; whether the email doesn't exist or the password is wrong; revealing which one failed makes it easier for attackers to enumerate valid accounts. A working session gets created only after the password check succeeds, then the user is redirected.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Checking authentication only in proxy and assuming that's enough. Proxy-level checks are a good UX shortcut (fast redirect before a page even loads), but sensitive Server Actions, Route Handlers, and data-access functions should independently verify authentication and authorization rather than relying solely on &lt;code&gt;proxy.ts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Building this by hand is a great way to learn, but most production apps reach for a maintained library instead — see &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;How to Add Auth.js (NextAuth v5) in Next.js 16&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;Add Clerk Authentication to Next.js 16&lt;/a&gt;, or (if you're already on Supabase) &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/supabase-auth-vs-firebase-auth&quot;&gt;Supabase Auth vs Firebase Auth&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;common-authentication-mistakes-to-avoid&quot;&gt;Common Authentication Mistakes to Avoid&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Storing plain-text passwords&lt;/strong&gt; — always hash with Argon2id or bcrypt, never store or log the raw password&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storing a predictable value as the session cookie&lt;/strong&gt; — the cookie should be a random, unguessable reference, never the raw user ID or anything else an attacker could construct&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weak or reused session/JWT secrets&lt;/strong&gt; — generate a long, random secret and never commit it to git&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revealing which field was wrong&lt;/strong&gt; — &quot;Invalid email&quot; vs. &quot;Invalid password&quot; tells attackers an account exists&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skipping rate limiting&lt;/strong&gt; — without it, attackers can brute-force passwords with unlimited attempts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relying only on client-side checks&lt;/strong&gt; — a hidden button or disabled form field doesn't stop a direct API request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forgetting to expire sessions/tokens&lt;/strong&gt; — every session and token should have a sane expiry, not live forever&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;what-is-the-difference-between-authentication-and-authorization-&quot;&gt;What is the difference between authentication and authorization?&lt;/h4&gt;
&lt;p&gt;Authentication confirms &lt;em&gt;who&lt;/em&gt; someone is (logging in). Authorization decides &lt;em&gt;what&lt;/em&gt; they're allowed to do once they're recognized (permissions, roles, access control). You always need authentication before authorization can happen.&lt;/p&gt;
&lt;h4 id=&quot;is-a-login-system-the-same-thing-as-authentication-&quot;&gt;Is a login system the same thing as authentication?&lt;/h4&gt;
&lt;p&gt;Yes — a login system is simply the implementation of authentication. Whatever mechanism confirms a user's identity (password, OAuth, passkey) is functioning as authentication, regardless of what you call the feature.&lt;/p&gt;
&lt;h4 id=&quot;what-s-the-difference-between-sessions-and-jwts-&quot;&gt;What's the difference between sessions and JWTs?&lt;/h4&gt;
&lt;p&gt;A session stores authentication/session state on the server and gives the browser only a random reference ID, which allows instant revocation. A JWT stores the identity data directly inside a signed token the server can verify without a database lookup, trading easy revocation for less server-side state.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Keep JWT minimal — a JWT should not be treated as a general-purpose user profile or database replacement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;do-i-need-a-library-to-build-authentication-or-can-i-build-it-myself-&quot;&gt;Do I need a library to build authentication, or can I build it myself?&lt;/h4&gt;
&lt;p&gt;You can build basic password + session authentication yourself using the patterns above, but for anything production-facing, an established library (&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;Auth.js&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;Clerk&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/authjs-vs-better-auth&quot;&gt;Better Auth&lt;/a&gt;, or your database provider's built-in auth) is strongly recommended — they handle edge cases like token rotation, breach detection, and secure defaults that are easy to get subtly wrong by hand.&lt;/p&gt;
&lt;h4 id=&quot;is-two-factor-authentication-2fa-actually-necessary-&quot;&gt;Is two-factor authentication (2FA) actually necessary?&lt;/h4&gt;
&lt;p&gt;For anything holding sensitive data — financial info, personal messages, admin access — yes. 2FA dramatically reduces account takeover risk even when a password leaks, since the attacker also needs the second factor.&lt;/p&gt;
&lt;h4 id=&quot;are-passkeys-better-than-passwords-&quot;&gt;Are passkeys better than passwords?&lt;/h4&gt;
&lt;p&gt;Generally yes, from a security standpoint — passkeys are phishing-resistant because there's no shared secret to trick someone into revealing. Adoption is still growing, though, so most apps currently offer passkeys as an &lt;em&gt;option&lt;/em&gt; alongside passwords rather than a full replacement.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Authentication boils down to one question — &quot;who are you?&quot; — answered through a password, a token, a device, or some combination of them. Once you separate authentication from authorization, the ecosystem becomes easier to understand: passwords and passkeys are authentication mechanisms, sessions and JWTs are ways to maintain authenticated state, and OAuth/OIDC provide delegated authorization and federated identity.&lt;/p&gt;
&lt;p&gt;If you're building this in a real Next.js app, the next logical steps are wiring up role-based authorization on top of your authentication layer, and deciding between session-based and JWT-based auth based on whether instant revocation matters for your product.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/session-vs-jwt-for-nextjs-16&quot;&gt;Session vs JWT: Which Should You Use in Next.js 16?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/authjs-vs-better-auth&quot;&gt;Auth.js vs Better Auth in 2026: Which Should You Use?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;/https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;NextAuth (Auth.js) vs Clerk in 2026: Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/supabase-auth-vs-firebase-auth&quot;&gt;Supabase Auth vs Firebase Auth in 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;free-developer-tools&quot;&gt;Free Developer Tools&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod Schema Generator&lt;/a&gt; — generate a validated schema for your role/permission payloads straight from a JSON sample or TypeScript interface, including a ready-to-use Server Action.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/rls-policy-generator&quot;&gt;Supabase RLS Policy Generator&lt;/a&gt; — pair this guide's application-level RBAC with database-level Row Level Security. Generates owner-only, team, and admin-override policies with the &lt;code&gt;auth.uid()&lt;/code&gt; performance wrapper built in.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25969/what-is-authentication-a-beginners-guide-2026</guid>
<pubDate>Tue, 01 Sep 2026 05:07:56 +0000</pubDate>
</item>
<item>
<title>Session vs JWT: Which Should You Use in Next.js 16?</title>
<link>https://coderlegion.com/25323/session-vs-jwt-which-should-you-use-in-next-js-16</link>
<description>&lt;p&gt;You're building a login flow in Next.js 16, and you hit the question every auth tutorial glosses over: should the logged-in state live in a &lt;strong&gt;session&lt;/strong&gt; or a &lt;strong&gt;JWT&lt;/strong&gt;? Half the guides you find use one, half use the other, and almost none of them tell you &lt;em&gt;why&lt;/em&gt; — or what breaks if you pick wrong.&lt;/p&gt;
&lt;p&gt;Here's the annoying part: both approaches look nearly identical from the outside. A cookie gets set, the user stays logged in, everyone's happy. The difference only shows up later — when you need to log a user out of every device instantly, when your app scales past one server, or when you're debugging why a &quot;revoked&quot; user can still hit your API for another six days.&lt;/p&gt;
&lt;p&gt;By the end of this guide, you'll know exactly how sessions and JWTs work under the hood, see real, working code for both in Next.js 16 — using &lt;code&gt;jose&lt;/code&gt;, Server Actions, and &lt;code&gt;proxy.ts&lt;/code&gt; — and know which one actually fits your project instead of copying whatever the last tutorial you read happened to use.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-is-tricky&quot;&gt;Why This Is Tricky&lt;/h3&gt;
&lt;p&gt;Most explanations of &quot;session vs JWT&quot; get stuck comparing them as if they're two totally separate technologies. They're not. In both approaches, the browser stores a cookie. The real difference is &lt;strong&gt;what that cookie contains, and where the source of truth lives&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session-based auth&lt;/strong&gt;: the cookie holds a random ID. The actual user data lives in your database. Every request that needs to know &quot;who is this?&quot; looks it up server-side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT-based auth&lt;/strong&gt;: the cookie holds the user data itself, cryptographically signed. Your server can verify it's untampered without ever touching a database.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That one distinction — &lt;em&gt;lookup vs. self-contained&lt;/em&gt; — is what drives every tradeoff in this post: revocation, scaling, latency, and how much you need to trust the cookie itself.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If you've used &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;Auth.js (NextAuth v5)&lt;/a&gt;, you've already used both without necessarily noticing — its &lt;code&gt;session: { strategy: &quot;jwt&quot; }&lt;/code&gt; and &lt;code&gt;session: { strategy: &quot;database&quot; }&lt;/code&gt; options are exactly this same choice, just configured for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;This guide uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt; with the App Router&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; in strict mode&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React Compiler&lt;/strong&gt; enabled (no manual &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; needed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS&lt;/strong&gt; for the (minimal) UI examples&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need a real database to follow along — the JWT section has zero dependencies, and the session section uses a generic &lt;code&gt;db&lt;/code&gt; client you can swap for Postgres, Supabase, or whatever you're already using.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-sessions-work&quot;&gt;How Sessions Work&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; a coat check ticket. The ticket itself is just a number — it means nothing on its own. The actual coat (your user data) stays with the attendant (your database). Lose the ticket, and the attendant can also just... stop honoring it. Instantly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;User logs in with the right credentials.&lt;/li&gt;
&lt;li&gt;Your server generates a random token, stores it in a &lt;code&gt;sessions&lt;/code&gt; table alongside the user's ID and an expiry, and sends that token back as an &lt;code&gt;httpOnly&lt;/code&gt; cookie.&lt;/li&gt;
&lt;li&gt;On every request, your server takes the cookie value, looks it up in the database, and either finds a valid session or doesn't.&lt;/li&gt;
&lt;li&gt;To log someone out — including forcibly, from your side — you delete the row. Done. The cookie is now worthless.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; every authenticated request costs a database round trip (unless you cache it), but you get instant, guaranteed revocation.&lt;/p&gt;
&lt;h3 id=&quot;how-jwts-work&quot;&gt;How JWTs Work&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like:&lt;/strong&gt; a sealed, signed letter. Anyone can read what's inside once it's opened, but nobody can forge your signature or quietly edit the contents without breaking the seal. The letter is self-sufficient — you don't need to call anyone to confirm it's real, just check the seal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;User logs in with the right credentials.&lt;/li&gt;
&lt;li&gt;Your server creates a token containing the user's ID (and whatever else you want) and &lt;strong&gt;signs&lt;/strong&gt; it with a secret key.&lt;/li&gt;
&lt;li&gt;On every request, your server checks the signature against that same secret. If it matches, the data inside is trusted — no database lookup required.&lt;/li&gt;
&lt;li&gt;To log someone out, you delete the cookie client-side. But if someone already has a copy of that token, it's still valid — and still verifiable — until it naturally expires. There's no built-in way to say &quot;actually, ignore this one.&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; verification is fast and needs no database, but you give up the ability to instantly kill a single session without extra machinery.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Calling a JWT stored in a cookie a &quot;session&quot; and assuming it behaves like one. It doesn't — a JWT has no server-side record at all unless you deliberately build one (see the revocation section below). If your mental model is &quot;session,&quot; you'll be surprised the first time you try to force-logout a user and nothing happens.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Cookie size limits:&lt;/strong&gt; browsers cap a single cookie at roughly 4KB, and most reverse proxies (Nginx, Cloudflare) reject requests once the &lt;em&gt;total&lt;/em&gt; header size gets much past 8KB. A session-ID cookie is always tiny — it's just a random string. A JWT cookie grows with every claim you cram into the payload (roles, permissions, profile fields). Stuff enough into it and the cookie silently fails to set, or your CDN starts 400-ing requests, with no obvious error pointing back at &quot;your JWT got too big.&quot; Keep JWT payloads minimal — an ID and maybe a role, not a serialized user object.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-set-up-the-shared-pieces&quot;&gt;Step 1: Set Up the Shared Pieces&lt;/h3&gt;
&lt;p&gt;Both approaches need somewhere to keep a signing secret. Add one to your environment file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# .env.local
SESSION_SECRET=your-long-random-secret-here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generate a strong value instead of typing something memorable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node -e &quot;console.log(crypto.randomBytes(32).toString('hex'))&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Committing &lt;code&gt;SESSION_SECRET&lt;/code&gt; to git, or reusing your &lt;code&gt;.env.local&lt;/code&gt; value in production. Treat it exactly like a database password — if it leaks, an attacker can forge valid sessions for any user, for either approach below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-build-jwt-based-auth-with-jose&quot;&gt;Step 2: Build JWT-Based Auth with &lt;code&gt;jose&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;jose&lt;/code&gt; is the standard library for signing and verifying JWTs in an Edge-compatible way — it uses the Web Crypto API instead of Node's &lt;code&gt;crypto&lt;/code&gt; module, so it works identically in Server Actions, Route Handlers, and &lt;code&gt;proxy.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install jose
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;create-the-sign-verify-helpers&quot;&gt;Create the sign/verify helpers&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/jwt/session.ts
import &quot;server-only&quot;;
import { SignJWT, jwtVerify } from &quot;jose&quot;;

const secretKey = process.env.SESSION_SECRET;
const encodedKey = new TextEncoder().encode(secretKey);

export async function encrypt(userId: string, expiresAt: Date) {
  return new SignJWT({})
    .setProtectedHeader({ alg: &quot;HS256&quot; })
    .setSubject(userId) // sub — who this token belongs to
    .setIssuedAt() // iat — when it was issued
    .setExpirationTime(expiresAt) // exp — when it stops being valid
    .sign(encodedKey);
}

export async function decrypt(token: string | undefined = &quot;&quot;) {
  try {
    const { payload } = await jwtVerify(token, encodedKey, {
      algorithms: [&quot;HS256&quot;],
    });
    return payload; // payload.sub is the userId; iat/exp are unix seconds
  } catch {
    // Invalid signature, tampered payload, or expired token
    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; instead of a custom &lt;code&gt;userId&lt;/code&gt; field, this uses the standard, registered JWT claims — &lt;code&gt;sub&lt;/code&gt; (subject, via &lt;code&gt;.setSubject()&lt;/code&gt;), &lt;code&gt;iat&lt;/code&gt; (issued-at, via &lt;code&gt;.setIssuedAt()&lt;/code&gt;), and &lt;code&gt;exp&lt;/code&gt; (expiry, via &lt;code&gt;.setExpirationTime()&lt;/code&gt;). These three are part of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc7519#section-4.1&quot;&gt;JWT spec itself&lt;/a&gt;, not a &lt;code&gt;jose&lt;/code&gt;-specific convention, so any JWT library — in any language — knows how to read them. &lt;code&gt;jwtVerify&lt;/code&gt; automatically checks &lt;code&gt;exp&lt;/code&gt; and rejects an expired token before your code ever sees the payload, so you never write manual date comparisons.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Signing is not encryption.&lt;/strong&gt; &lt;code&gt;SignJWT&lt;/code&gt; — despite this file calling the wrapper functions &lt;code&gt;encrypt&lt;/code&gt;/&lt;code&gt;decrypt&lt;/code&gt; for readability — does &lt;strong&gt;not&lt;/strong&gt; hide the payload. A JWT's contents are only base64url-&lt;em&gt;encoded&lt;/em&gt;, not encrypted, so anyone who intercepts the cookie (or just opens DevTools → Application → Cookies) can decode and read &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;iat&lt;/code&gt;, and &lt;code&gt;exp&lt;/code&gt; in plain text. What the signature guarantees is that the payload &lt;strong&gt;wasn't tampered with&lt;/strong&gt; — change one character and &lt;code&gt;jwtVerify&lt;/code&gt; rejects it. Never put anything genuinely secret (passwords, tokens for other services, PII you wouldn't want exposed) inside a signed JWT. If you actually need to hide the contents too, that's a different primitive — JWE — which &lt;code&gt;jose&lt;/code&gt; supports separately via &lt;code&gt;EncryptJWT&lt;/code&gt;/&lt;code&gt;jwtDecrypt&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Do you still need &lt;code&gt;setExpirationTime&lt;/code&gt; if you also track expiry elsewhere?&lt;/strong&gt; No — and the version above deliberately avoids the redundancy. Compute the expiry date &lt;strong&gt;once&lt;/strong&gt;, and hand that same &lt;code&gt;Date&lt;/code&gt; to both &lt;code&gt;setExpirationTime()&lt;/code&gt; (for the JWT's own &lt;code&gt;exp&lt;/code&gt; claim) and the cookie's &lt;code&gt;expires&lt;/code&gt; option, as shown in the next step. Two separately-computed &quot;7 days from now&quot; values can drift apart by milliseconds and give you two conflicting sources of truth for the same thing — pick one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;create-the-session-cookie-on-login&quot;&gt;Create the session cookie on login&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/jwt/actions.ts
&quot;use server&quot;;

import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { encrypt } from &quot;./session&quot;;

export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
  const session = await encrypt(userId, expiresAt); // same Date used for exp and the cookie
  const cookieStore = await cookies();

  cookieStore.set(&quot;session&quot;, session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: &quot;lax&quot;,
    path: &quot;/&quot;,
  });
}

export async function deleteSession() {
  const cookieStore = await cookies();
  cookieStore.delete(&quot;session&quot;);
  redirect(&quot;/login&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;httpOnly: true&lt;/code&gt; keeps the cookie invisible to client-side JavaScript, which stops it from being stolen via an XSS attack. &lt;code&gt;secure: true&lt;/code&gt; means it's only ever sent over HTTPS. &lt;code&gt;sameSite: &quot;lax&quot;&lt;/code&gt; blocks the cookie from being attached to most cross-site requests, which is your main defense against CSRF here. None of this is JWT-specific — you'd want the exact same flags on a session-ID cookie too.&lt;/p&gt;
&lt;h4 id=&quot;verify-the-session-with-a-data-access-layer&quot;&gt;Verify the session with a Data Access Layer&lt;/h4&gt;
&lt;p&gt;Don't scatter &lt;code&gt;decrypt()&lt;/code&gt; calls across every page. Centralize verification in one cached function — this is what Next.js's own docs call a &lt;strong&gt;Data Access Layer (DAL)&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/jwt/dal.ts
import &quot;server-only&quot;;
import { cache } from &quot;react&quot;;
import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { decrypt } from &quot;./session&quot;;

export const verifySession = cache(async () =&amp;gt; {
  const cookieStore = await cookies();
  const token = cookieStore.get(&quot;session&quot;)?.value;
  const session = await decrypt(token);

  if (!session?.sub) {
    redirect(&quot;/login&quot;);
  }

  return { isAuth: true, userId: session.sub };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; wrapping this in React's &lt;code&gt;cache()&lt;/code&gt; means that no matter how many components on the same page call &lt;code&gt;verifySession()&lt;/code&gt;, the actual decrypt-and-check logic only runs once per request — every subsequent call reuses the result. &lt;code&gt;session.sub&lt;/code&gt; is the standard claim set by &lt;code&gt;.setSubject()&lt;/code&gt; earlier, so this reads it back out under a friendlier &lt;code&gt;userId&lt;/code&gt; name for the rest of your app. Call this at the top of any Server Component, Server Action, or Route Handler that needs to know who's logged in.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;// app/dashboard/page.tsx
import { verifySession } from &quot;@/lib/jwt/dal&quot;;

export default async function DashboardPage() {
  const { userId } = await verifySession();

  return &amp;lt;h1&amp;gt;Welcome back, user {userId}&amp;lt;/h1&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;add-an-optimistic-check-in-proxy-ts&quot;&gt;Add an optimistic check in &lt;code&gt;proxy.ts&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;In Next.js 16, &lt;code&gt;middleware.ts&lt;/code&gt; was renamed to &lt;code&gt;proxy.ts&lt;/code&gt; — same file convention, same idea. It's the first thing that runs before a request reaches a page, which makes it a good place for a &lt;em&gt;fast, optimistic&lt;/em&gt; redirect. Full verification still happens in the DAL above — &lt;code&gt;proxy.ts&lt;/code&gt; just avoids letting an obviously-unauthenticated request render a protected page at all.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// proxy.ts
import { NextRequest, NextResponse } from &quot;next/server&quot;;

const protectedRoutes = [&quot;/dashboard&quot;];
const publicRoutes = [&quot;/login&quot;, &quot;/signup&quot;];

export default function proxy(req: NextRequest) {
  const path = req.nextUrl.pathname;
  const isProtectedRoute = protectedRoutes.some((route) =&amp;gt; path.startsWith(route));
  const isPublicRoute = publicRoutes.includes(path);

  // Optimistic check only — we just look for the cookie's presence.
  // We deliberately don't verify the signature here; that happens
  // in verifySession() inside the Data Access Layer.
  const cookie = req.cookies.get(&quot;session&quot;)?.value;

  if (isProtectedRoute &amp;amp;&amp;amp; !cookie) {
    return NextResponse.redirect(new URL(&quot;/login&quot;, req.nextUrl));
  }

  if (isPublicRoute &amp;amp;&amp;amp; cookie) {
    return NextResponse.redirect(new URL(&quot;/dashboard&quot;, req.nextUrl));
  }

  return NextResponse.next();
}

export const config = {
  matcher: [&quot;/((?!_next/static|_next/image|favicon.ico).*)&quot;],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Common Mistake:&lt;/strong&gt; Treating &lt;code&gt;proxy.ts&lt;/code&gt; as your only security check. It's a UX nicety that avoids a flash of protected content — it is not a substitute for re-verifying the session inside every Server Action and Route Handler that actually touches data. A request can always be sent directly, bypassing whatever page it &quot;should&quot; have come from.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-build-database-backed-sessions&quot;&gt;Step 3: Build Database-Backed Sessions&lt;/h3&gt;
&lt;p&gt;Now the other approach — a real session table your server checks on each request.&lt;/p&gt;
&lt;h4 id=&quot;create-the-sessions-table&quot;&gt;Create the sessions table&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Works for Postgres/Supabase; adjust types for your database
create table sessions (
  token_hash text primary key,
  user_id uuid not null references auth.users(id) on delete cascade,
  expires_at timestamptz not null,
  created_at timestamptz not null default now()
);

create index idx_sessions_user_id on sessions (user_id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; notice the primary key is &lt;code&gt;token_hash&lt;/code&gt;, not &lt;code&gt;token&lt;/code&gt;. We never store the raw session token in the database — only its hash. This mirrors how you'd never store a plaintext password: if your database ever leaked, an attacker with the hashes still couldn't reconstruct a working cookie.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Cleanup:&lt;/strong&gt; expiring a session only stops it from being &lt;em&gt;accepted&lt;/em&gt; — the &lt;code&gt;expires_at &amp;lt; now()&lt;/code&gt; check in &lt;code&gt;verifyDbSession&lt;/code&gt; below handles that. It does &lt;strong&gt;not&lt;/strong&gt; delete the row. Left alone, this table grows forever, one row per login, most of them long past their expiry date. Run a scheduled cleanup — a &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/vercel-cron-generator&quot;&gt;Vercel Cron&lt;/a&gt; job hitting a small Route Handler once a day is enough for most apps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;delete from sessions where expires_at &amp;lt; now();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is purely a housekeeping/performance concern (an unbounded table gets slower to index and back up over time), not a security one — an expired row is already useless for authentication either way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;create-the-session-on-login&quot;&gt;Create the session on login&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/db-session/create.ts
import &quot;server-only&quot;;
import { randomBytes, createHash } from &quot;crypto&quot;;
import { cookies } from &quot;next/headers&quot;;
import { db } from &quot;@/lib/db&quot;;

function hashToken(token: string) {
  return createHash(&quot;sha256&quot;).update(token).digest(&quot;hex&quot;);
}

export async function createDbSession(userId: string) {
  const token = randomBytes(32).toString(&quot;hex&quot;); // goes to the browser
  const tokenHash = hashToken(token); // goes to the database
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

  await db.session.create({
    data: { tokenHash, userId, expiresAt },
  });

  const cookieStore = await cookies();
  cookieStore.set(&quot;session&quot;, token, {
    httpOnly: true,
    secure: true,
    sameSite: &quot;lax&quot;,
    expires: expiresAt,
    path: &quot;/&quot;,
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;randomBytes(32)&lt;/code&gt; generates a token with enough entropy that guessing it is computationally infeasible — this is the piece doing the actual security work, not the hashing. We hash it with SHA-256 purely so a database leak doesn't hand out working session tokens; the browser still holds the real, unhashed token in its cookie.&lt;/p&gt;
&lt;h4 id=&quot;verify-the-session-with-caching-so-it-doesn-t-hit-the-database-on-every-render-&quot;&gt;Verify the session (with caching, so it doesn't hit the database on every render)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/db-session/dal.ts
import &quot;server-only&quot;;
import { cache } from &quot;react&quot;;
import { cookies } from &quot;next/headers&quot;;
import { createHash } from &quot;crypto&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { db } from &quot;@/lib/db&quot;;

function hashToken(token: string) {
  return createHash(&quot;sha256&quot;).update(token).digest(&quot;hex&quot;);
}

export const verifyDbSession = cache(async () =&amp;gt; {
  const cookieStore = await cookies();
  const token = cookieStore.get(&quot;session&quot;)?.value;

  if (!token) redirect(&quot;/login&quot;);

  const tokenHash = hashToken(token);
  const session = await db.session.findUnique({
    where: { tokenHash },
    include: { user: true },
  });

  if (!session || session.expiresAt &amp;lt; new Date()) {
    redirect(&quot;/login&quot;);
  }

  return { isAuth: true, userId: session.userId, user: session.user };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; just like the JWT version, &lt;code&gt;cache()&lt;/code&gt; means one database lookup per request no matter how many components ask &quot;who's logged in?&quot; This is the piece that makes database sessions viable in practice — without it, a page with five components each checking auth would fire five identical queries.&lt;/p&gt;
&lt;h4 id=&quot;revoking-a-session-the-whole-point-of-this-approach&quot;&gt;Revoking a session — the whole point of this approach&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// lib/db-session/delete.ts
&quot;use server&quot;;

import { cookies } from &quot;next/headers&quot;;
import { redirect } from &quot;next/navigation&quot;;
import { createHash } from &quot;crypto&quot;;
import { db } from &quot;@/lib/db&quot;;

function hashToken(token: string) {
  return createHash(&quot;sha256&quot;).update(token).digest(&quot;hex&quot;);
}

export async function deleteDbSession() {
  const cookieStore = await cookies();
  const token = cookieStore.get(&quot;session&quot;)?.value;

  if (token) {
    await db.session.delete({ where: { tokenHash: hashToken(token) } }).catch(() =&amp;gt; {});
  }

  cookieStore.delete(&quot;session&quot;);
  redirect(&quot;/login&quot;);
}

// Force-logout every session for a user — e.g. after a password reset
export async function revokeAllSessionsForUser(userId: string) {
  await db.session.deleteMany({ where: { userId } });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt; &lt;code&gt;revokeAllSessionsForUser&lt;/code&gt; is the function you can't build with plain JWTs — one query, and every device that user is logged in on is instantly signed out, because the next request they make finds no matching row. This is the single biggest practical reason teams reach for database sessions over JWTs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy.ts&lt;/code&gt; looks identical to the JWT version above — it's still just checking whether the &lt;code&gt;session&lt;/code&gt; cookie exists, since that check never needed to know which auth strategy is behind it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;session-vs-jwt-side-by-side&quot;&gt;Session vs JWT: Side-by-Side&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Session (database) &lt;/th&gt;&lt;th&gt;JWT&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Where the data lives&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Server-side (database row) &lt;/td&gt;&lt;td&gt;Inside the token itself&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Cookie contents&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Random, meaningless ID &lt;/td&gt;&lt;td&gt;Signed, self-contained payload&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Instant revocation&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;✅ Delete the row &lt;/td&gt;&lt;td&gt;❌ Not without extra machinery&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Force-logout everywhere&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;✅ One query &lt;/td&gt;&lt;td&gt;❌ Requires a blocklist or short expiry + refresh&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Per-request cost&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;One DB lookup (cacheable per request) &lt;/td&gt;&lt;td&gt;No DB call — just signature verification&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Works well at scale / multi-server&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Needs a shared database (usually already true) &lt;/td&gt;&lt;td&gt;Naturally stateless, nothing to share&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Payload can go stale&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;No — always reads current data &lt;/td&gt;&lt;td&gt;Yes — data inside is frozen until re-issued&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Implementation complexity&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Slightly more (schema, hashing, cleanup) &lt;/td&gt;&lt;td&gt;Slightly less (no schema needed)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Apps where instant revocation matters &amp;lt;br/&amp;gt; (banking, admin tools, anything security-sensitive) &lt;/td&gt;&lt;td&gt;High-traffic, distributed systems where avoiding a &amp;lt;br/&amp;gt;DB hit per request matters&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; &quot;JWTs don't need a database&quot; is true for verification, but most real apps still store &lt;em&gt;something&lt;/em&gt; — a refresh-token table, a denylist, or at minimum a &lt;code&gt;passwordChangedAt&lt;/code&gt; timestamp checked against the token's &lt;code&gt;iat&lt;/code&gt; (issued-at) claim. The stateless promise of JWTs is a little softer in practice than it sounds in theory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;a-closer-look-what-horizontal-scaling-actually-means-here&quot;&gt;A Closer Look: What &quot;Horizontal Scaling&quot; Actually Means Here&lt;/h4&gt;
&lt;p&gt;&quot;Naturally stateless&quot; in the table above is doing a lot of work, so let's unpack it. Say your app runs on five server instances behind a load balancer — a request from the same user could land on a different instance every time.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;With database sessions:&lt;/strong&gt; whichever instance handles the request just needs to reach the &lt;em&gt;same&lt;/em&gt; database everyone else does. In practice this is a non-issue for most Next.js apps, since you already have one shared database — every instance was already talking to it for everything else. The &quot;scaling problem&quot; people worry about here mostly doesn't exist unless you're deliberately running sessions in something instance-local, like in-memory storage (don't do that).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;With JWTs:&lt;/strong&gt; verification needs nothing but the signing secret, which every instance already has as an environment variable. No instance has to coordinate with any other instance, or with a database, to answer &quot;is this token valid?&quot; That's genuinely useful once you're running at the edge — Cloudflare Workers, Vercel Edge Functions — across multiple regions, where a round trip to a single-region database could add real, noticeable latency to &lt;em&gt;every&lt;/em&gt; request.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the honest version: for a typical app with one shared Postgres/Supabase database, this isn't really a deciding factor — you're already &quot;horizontally scaled&quot; for sessions for free. It starts to matter once you're running truly distributed, multi-region, or edge-first infrastructure where a database round trip per request is a real, measured cost, not a hypothetical one.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;sliding-rolling-sessions&quot;&gt;Sliding (Rolling) Sessions&lt;/h3&gt;
&lt;p&gt;So far, every example here uses a &lt;strong&gt;fixed&lt;/strong&gt; expiry — log in, get 7 days, and after 7 days you're logged out no matter how active you were. A &lt;strong&gt;sliding session&lt;/strong&gt; (also called a rolling session) instead extends the expiry every time the user does something, so an active user effectively never gets logged out, while an idle one still expires on schedule.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;With database sessions, this is simple:&lt;/strong&gt; on every successful &lt;code&gt;verifyDbSession()&lt;/code&gt; call, update that row's &lt;code&gt;expires_at&lt;/code&gt; to &quot;now + 7 days&quot; again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Inside verifyDbSession, after confirming the session is valid
await db.session.update({
  where: { tokenHash },
  data: { expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One extra write per (cached) verification — cheap, and the source of truth (the row) is already being touched anyway.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; In production it's common to only extend the session once every few hours instead of on every request to avoid unnecessary writes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;With JWTs, this is more work:&lt;/strong&gt; the &lt;code&gt;exp&lt;/code&gt; claim is baked into the signature at issue time — you can't quietly extend it without re-signing a brand-new token and setting a brand-new cookie on the response. Doable inside a Route Handler or Server Action (create a fresh token with a new &lt;code&gt;expiresAt&lt;/code&gt;, call &lt;code&gt;cookies().set()&lt;/code&gt; again), but it means writing a cookie back on requests that otherwise wouldn't need one, which is easy to forget to wire up consistently across every route.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; If &quot;stay logged in while active, expire after a week of silence&quot; is a real requirement for your app, that alone is a reasonable tie-breaker toward database sessions — it's a one-line &lt;code&gt;update&lt;/code&gt; there versus meaningfully more plumbing with JWTs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;can-you-get-revocation-with-jwts-&quot;&gt;Can You Get Revocation With JWTs?&lt;/h3&gt;
&lt;p&gt;Yes, with a tradeoff. Two common patterns:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Short expiry + refresh tokens.&lt;/strong&gt; Issue an access token that expires in 10–15 minutes, plus a longer-lived refresh token stored in the database. To revoke, you delete the refresh token — the access token is still technically valid, but only for a few more minutes, which is an acceptable blast radius for most apps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A denylist.&lt;/strong&gt; Keep a small, fast-to-check store (Redis is the usual choice) of token IDs (&lt;code&gt;jti&lt;/code&gt; claims) that have been explicitly revoked. Every verification also checks this list. This reintroduces a lookup on every request — at that point, you've mostly reinvented database sessions with extra steps.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Neither of these makes JWTs behave exactly like sessions — they just narrow the gap. If instant, guaranteed revocation is a hard requirement (think: a compromised admin account), database sessions remove the problem entirely instead of managing around it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;which-one-should-you-actually-use-&quot;&gt;Which One Should You Actually Use?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Building a typical SaaS dashboard, internal tool, or content site?&lt;/strong&gt; Database sessions. The DB hit is cheap with caching, and &quot;log this user out right now&quot; is a feature you'll eventually need, even if you don't think so today.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Building something with genuinely massive, distributed, stateless traffic&lt;/strong&gt; (think: an API consumed by thousands of independent clients, or infrastructure where adding a database round trip to every request is a real cost)? JWTs earn their keep here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Already using &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;Auth.js&lt;/a&gt; or &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;Clerk&lt;/a&gt;?&lt;/strong&gt; You don't have to choose by hand — Auth.js exposes &lt;code&gt;session: { strategy: &quot;jwt&quot; | &quot;database&quot; }&lt;/code&gt; directly, and Clerk manages this decision for you behind its own infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not sure yet?&lt;/strong&gt; Default to database sessions. It's the safer, more debuggable choice, and the performance cost is rarely the bottleneck people assume it'll be — cache the lookup with &lt;code&gt;cache()&lt;/code&gt; as shown above and it disappears from your profiler.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions-1&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;can-i-combine-both-approaches-&quot;&gt;Can I combine both approaches?&lt;/h4&gt;
&lt;p&gt;Yes — this is actually what &quot;refresh tokens&quot; are: a short-lived JWT for fast verification, backed by a database-stored refresh token for revocation. You get JWT-speed reads most of the time, with a real revocation point when you need one.&lt;/p&gt;
&lt;h4 id=&quot;is-storing-a-jwt-in-a-cookie-the-same-as-storing-it-in-localstorage-&quot;&gt;Is storing a JWT in a cookie the same as storing it in localStorage?&lt;/h4&gt;
&lt;p&gt;No, and the difference matters for security. An &lt;code&gt;httpOnly&lt;/code&gt; cookie can't be read by client-side JavaScript, so it's protected from XSS attacks that steal tokens. &lt;code&gt;localStorage&lt;/code&gt; is fully readable by any script running on your page — a single XSS vulnerability anywhere in your dependency tree can leak every token stored there. Always use &lt;code&gt;httpOnly&lt;/code&gt; cookies for auth tokens in a Next.js app.&lt;/p&gt;
&lt;h4 id=&quot;does-proxy-ts-need-to-fully-verify-the-session-on-every-request-&quot;&gt;Does &lt;code&gt;proxy.ts&lt;/code&gt; need to fully verify the session on every request?&lt;/h4&gt;
&lt;p&gt;No, and for JWT-based auth verifying in &lt;code&gt;proxy.ts&lt;/code&gt; with &lt;code&gt;jose&lt;/code&gt; is genuinely fine performance-wise since it needs no database call. The stronger rule is: never treat &lt;code&gt;proxy.ts&lt;/code&gt; as your &lt;em&gt;only&lt;/em&gt; check. Always re-verify inside the Server Action or Route Handler that actually reads or writes data — &lt;code&gt;proxy.ts&lt;/code&gt; can be bypassed by hitting that endpoint directly.&lt;/p&gt;
&lt;h4 id=&quot;why-hash-the-session-token-before-storing-it-in-the-database-&quot;&gt;Why hash the session token before storing it in the database?&lt;/h4&gt;
&lt;p&gt;The same reason you hash passwords — if your database is ever compromised (a leaked backup, a misconfigured access policy), the attacker gets a list of hashes they can't turn back into working session cookies. It costs you one &lt;code&gt;sha256&lt;/code&gt; call per request, which is effectively free.&lt;/p&gt;
&lt;h4 id=&quot;which-one-does-auth-js-use-by-default-&quot;&gt;Which one does Auth.js use by default?&lt;/h4&gt;
&lt;p&gt;Auth.js defaults to the &lt;code&gt;&quot;jwt&quot;&lt;/code&gt; strategy unless you configure a database adapter, in which case it switches to &lt;code&gt;&quot;database&quot;&lt;/code&gt; automatically. You can also set &lt;code&gt;session: { strategy: &quot;jwt&quot; }&lt;/code&gt; explicitly even with an adapter configured, if you want JWTs for speed while still persisting user accounts.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;wrapping-up-1&quot;&gt;Wrapping Up&lt;/h3&gt;
&lt;p&gt;Sessions and JWTs aren't competing standards — they're two different answers to &quot;where does the source of truth for a logged-in user live?&quot; Sessions keep it on your server, trading a database lookup for instant, total control over who stays logged in. JWTs keep it in the token itself, trading that control for verification that never has to leave the request.&lt;/p&gt;
&lt;p&gt;For most Next.js apps, start with database sessions — the caching pattern above keeps the performance cost near zero, and you'll be glad revocation is a solved problem the first time you actually need it. Reach for JWTs specifically when you've measured that the database hit matters, not because a tutorial defaulted to it.&lt;/p&gt;
&lt;p&gt;From here, a natural next step is wiring either pattern into a real login form with &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/tools/zod-schema-generator&quot;&gt;Zod&lt;/a&gt; validating the credentials before they ever reach your session logic, or layering &lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;role-based permissions&lt;/a&gt; on top of whichever session strategy you pick.&lt;/p&gt;
&lt;h3 id=&quot;continue-learning-1&quot;&gt;Continue Learning&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-nextauth-in-nextjs-16&quot;&gt;How to Add Auth.js (NextAuth v5) Authentication in Next.js 16 (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/how-to-add-clerk-authentication-in-nextjs-16&quot;&gt;How to Add Clerk Authentication in Next.js 16 (2026 Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/clerk-vs-nextauth-2026&quot;&gt;Clerk vs NextAuth (Auth.js) in 2026: Which One Should You Choose?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/role-based-authentication-nextjs-16&quot;&gt;Role-Based Access Control (RBAC) in Next.js 16 (2026 Beginner's Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devstacked.tech/blog/supabase-auth-vs-firebase-auth&quot;&gt;Supabase Auth vs Firebase Auth: Which One Should You Choose in 2026?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;useful-resources&quot;&gt;Useful Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/panva/jose&quot;&gt;jose Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nextjs.org/docs/app/guides/authentication&quot;&gt;Next.js Authentication Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html&quot;&gt;OWASP Session Management Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25323/session-vs-jwt-which-should-you-use-in-next-js-16</guid>
<pubDate>Tue, 25 Aug 2026 04:12:26 +0000</pubDate>
</item>
<item>
<title>Stop Pasting JWTs Into Random Websites. Here's What I Built Instead.</title>
<link>https://coderlegion.com/19466/stop-pasting-jwts-into-random-websites-heres-what-i-built-instead</link>
<description>&lt;p&gt;&lt;em&gt;By Abu Sufyan · Lead Systems Architect, WebToolkit Pro&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Every week, thousands of developers do something they know they shouldn't.&lt;/p&gt;
&lt;p&gt;They open a new browser tab, navigate to some third-party JWT decoder, and paste a live production token directly into it.&lt;/p&gt;
&lt;p&gt;That token probably contains a user ID. Maybe a role claim. Maybe an internal service identifier that maps to something sensitive in your infrastructure. And it just left your browser.&lt;/p&gt;
&lt;p&gt;I know, because I used to do it too.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-incident-that-made-me-stop&quot;&gt;The Incident That Made Me Stop&lt;/h3&gt;
&lt;p&gt;In late 2024, I was debugging an auth issue on a client's Node.js API — a financial services platform. Standard stuff: the access token wasn't refreshing correctly and I needed to inspect the payload quickly to verify the &lt;code&gt;exp&lt;/code&gt; claim.&lt;/p&gt;
&lt;p&gt;I instinctively opened a well-known JWT decoder site and pasted the token.&lt;/p&gt;
&lt;p&gt;Then I stopped.&lt;/p&gt;
&lt;p&gt;I was working in a production environment. That token belonged to a real user — a client of theirs. It contained a &lt;code&gt;sub&lt;/code&gt; (subject) claim with a UUID that mapped directly to their user database. It contained an &lt;code&gt;iat&lt;/code&gt; and &lt;code&gt;exp&lt;/code&gt;. And most critically, it contained a &lt;code&gt;roles&lt;/code&gt; claim: &lt;code&gt;[&quot;admin&quot;, &quot;finance_read&quot;]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I had just sent a production admin token to a third-party server I knew nothing about.&lt;/p&gt;
&lt;p&gt;The site claimed it was &quot;client-side only.&quot; But I couldn't verify that. There was no open-source repo I could inspect. No audit trail. No CSP headers I could check. Just a text box and a promise.&lt;/p&gt;
&lt;p&gt;That afternoon, I filed an internal incident report and started building a replacement.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-makes-a-jwt-decoder-dangerous&quot;&gt;What Makes a JWT Decoder Dangerous&lt;/h3&gt;
&lt;p&gt;Before I get to what I built, it's worth understanding exactly why this matters technically — not just philosophically.&lt;/p&gt;
&lt;p&gt;A JWT has three parts: header, payload, and signature. The first two are just Base64url-encoded JSON. Decoding them requires zero network traffic. It's pure string manipulation you can do in two lines of JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const [header, payload] = token.split('.');
const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it. There is no cryptographic reason a JWT decoder needs to contact any server whatsoever.&lt;/p&gt;
&lt;p&gt;And yet, when you paste a token into most online decoders, your token travels over the network to a server. That server may or may not be logging requests. It may be behind a CDN that caches POST bodies. It may be owned by a company whose threat model you have no visibility into.&lt;/p&gt;
&lt;p&gt;The attack surface is real:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session hijacking&lt;/strong&gt; if the token is still valid and the attacker can replay it against your API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claim exposure&lt;/strong&gt; — user IDs, email addresses, role names, tenant identifiers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure mapping&lt;/strong&gt; — the &lt;code&gt;iss&lt;/code&gt; (issuer) claim often reveals internal service URLs or identity provider endpoints&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Algorithm fingerprinting&lt;/strong&gt; — the &lt;code&gt;alg&lt;/code&gt; header tells an attacker exactly what signing algorithm you're using, useful for crafting exploit attempts&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architecture-zero-knowledge-client-side-decoding&quot;&gt;The Architecture: Zero-Knowledge Client-Side Decoding&lt;/h3&gt;
&lt;p&gt;What I built — the &lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/jwt-decoder/&quot;&gt;Offline JWT Decoder at WebToolkit Pro&lt;/a&gt; — operates on a simple constraint: &lt;strong&gt;nothing ever leaves the browser tab&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here's the actual execution model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;User pastes token → Browser JS splits on '.'
                  → atob() decodes header + payload
                  → JSON.parse() reconstructs the object
                  → Rendered directly into DOM

No fetch(). No XMLHttpRequest(). No WebSocket. No Service Worker relay.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To verify this yourself: open DevTools → Network tab → paste a token → watch. Zero outbound requests. The network panel stays empty.&lt;/p&gt;
&lt;p&gt;The implementation uses the Web Crypto API for signature verification — which also runs entirely in the browser sandbox. There are no API keys, no backend, no telemetry. The Content Security Policy on the page blocks outbound requests at the browser level, so even if a malicious script injection somehow occurred, it couldn't exfiltrate the token.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;beyond-decoding-the-signature-verification-problem&quot;&gt;Beyond Decoding: The Signature Verification Problem&lt;/h3&gt;
&lt;p&gt;Most online JWT decoders only decode. They don't verify.&lt;/p&gt;
&lt;p&gt;Decoding tells you what's inside the token. Verification tells you whether to trust it.&lt;/p&gt;
&lt;p&gt;The difference matters enormously in debugging scenarios. If you're diagnosing an auth failure, you need to know:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is the token malformed? (decoding fails)&lt;/li&gt;
&lt;li&gt;Is it expired? (check &lt;code&gt;exp&lt;/code&gt; vs current timestamp)&lt;/li&gt;
&lt;li&gt;Is the signature valid against the expected public key? (actual cryptographic verification)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Point 3 is where most tools fall apart. They skip it entirely, leaving you with no way to confirm whether the token was legitimately issued or tampered with.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/jwt-debugger/&quot;&gt;JWT Debugger &amp;amp; Inspector&lt;/a&gt; I built handles all three. You can paste your JWKS (JSON Web Key Set) endpoint URL or paste the raw public key directly, and the tool performs RS256/ES256 signature verification locally using &lt;code&gt;window.crypto.subtle.verify()&lt;/code&gt; — the same Web Crypto primitive your browser uses for HTTPS.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// What's happening under the hood
const key = await crypto.subtle.importKey(
  'jwk',
  jwkPublicKey,
  { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
  false,
  ['verify']
);

const isValid = await crypto.subtle.verify(
  'RSASSA-PKCS1-v1_5',
  key,
  signature,
  data
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No server. No SDK. Pure browser cryptography.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-alg-none-attack-why-your-decoder-needs-to-flag-this&quot;&gt;The &lt;code&gt;alg: none&lt;/code&gt; Attack — Why Your Decoder Needs to Flag This&lt;/h3&gt;
&lt;p&gt;While building this, I added one feature that I think every JWT tool should have: automatic detection of the &lt;code&gt;alg: none&lt;/code&gt; exploit vector.&lt;/p&gt;
&lt;p&gt;Here's the attack. Some JWT libraries — particularly older versions — accept tokens where the algorithm is set to &lt;code&gt;none&lt;/code&gt; and the signature is empty. An attacker who knows this can forge a token with any payload they want:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// Header
{
  &quot;alg&quot;: &quot;none&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}
// Payload
{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;roles&quot;: [&quot;admin&quot;],
  &quot;iat&quot;: 1516239022
}
// Signature: empty string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gets concatenated as &lt;code&gt;base64(header).base64(payload).&lt;/code&gt; — note the trailing dot with no signature.&lt;/p&gt;
&lt;p&gt;The decoder flags this immediately with a warning: &lt;code&gt;⚠ Algorithm &quot;none&quot; detected. This token carries no cryptographic integrity guarantee. Reject in production.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It's a small thing, but it's the kind of context-aware output you only get from a tool built by someone who actually works with these tokens in production.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-learned-building-privacy-first-tools&quot;&gt;What I Learned Building Privacy-First Tools&lt;/h3&gt;
&lt;p&gt;This JWT decoder is one of 150+ tools I've built at &lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site&quot;&gt;WebToolkit Pro&lt;/a&gt;. Every single one follows the same constraint: &lt;strong&gt;your data never leaves your browser&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Password generator. Hash generator. Base64 encoder. Regex tester. JSON formatter. AES encryption tool. All of them execute entirely client-side.&lt;/p&gt;
&lt;p&gt;Building this way taught me a few things:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WebAssembly changed what's possible.&lt;/strong&gt; Operations that used to require a server — like Argon2 password hashing, which is deliberately memory-intensive — can now run in the browser via WASM. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/argon2-hasher/&quot;&gt;Argon2 Hasher&lt;/a&gt; generates production-grade password hashes locally in under a second.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Web Workers are underused.&lt;/strong&gt; For tools that process large inputs (the JSON formatter handles payloads up to 50MB), moving the parsing off the main thread via Web Workers keeps the UI responsive. No spinners, no freezes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The hardest part isn't the crypto — it's the UX.&lt;/strong&gt; Security tools have a reputation for being hostile to use. I spent more time on the output formatting and error messaging than on the underlying algorithms. A tool that correctly identifies an &lt;code&gt;exp&lt;/code&gt; claim mismatch but displays it as a raw timestamp integer has failed its user.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h3&gt;
&lt;p&gt;If you're debugging JWTs today, open DevTools first. Network tab, record. Then go to &lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/jwt-decoder/&quot;&gt;wtkpro.site/tools/jwt-decoder&lt;/a&gt; and paste your token.&lt;/p&gt;
&lt;p&gt;Watch the network panel stay silent.&lt;/p&gt;
&lt;p&gt;That silence is the feature.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/jwt-decoder/&quot;&gt;Offline JWT Decoder&lt;/a&gt; — decode without sending tokens to any server&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/jwt-debugger/&quot;&gt;JWT Debugger &amp;amp; Inspector&lt;/a&gt; — full signature verification with JWKS support&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/jwt-signer/&quot;&gt;JWT Signing Tool&lt;/a&gt; — generate signed tokens locally for testing&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/password-entropy-tester/&quot;&gt;Password Entropy Tester&lt;/a&gt; — offline entropy calculation and crack time estimates&lt;/li&gt;
&lt;li&gt;Full tool directory: &lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site/tools/&quot;&gt;wtkpro.site/tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Founder of &lt;a rel=&quot;nofollow&quot; href=&quot;https://wtkpro.site&quot;&gt;WebToolkit Pro&lt;/a&gt; and a systems architect specializing in V8 performance, security tooling, and zero-knowledge web architectures. Find him on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/abusufyan-netizen&quot;&gt;GitHub&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/webtoolkitpro&quot;&gt;Dev.to&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;security&lt;/code&gt; &lt;code&gt;jwt&lt;/code&gt; &lt;code&gt;webdev&lt;/code&gt; &lt;code&gt;javascript&lt;/code&gt; &lt;code&gt;privacy&lt;/code&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19466/stop-pasting-jwts-into-random-websites-heres-what-i-built-instead</guid>
<pubDate>Tue, 02 Jun 2026 06:43:46 +0000</pubDate>
</item>
<item>
<title>#  How Authentication Really Works (JWT vs Session vs OAuth)</title>
<link>https://coderlegion.com/15033/how-authentication-really-works-jwt-vs-session-vs-oauth</link>
<description>&lt;p&gt;Whenever you log in to a website…&lt;/p&gt;
&lt;p&gt;You enter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Email&lt;/li&gt;
&lt;li&gt;Password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And suddenly…&lt;/p&gt;
&lt;p&gt; You are “logged in”&lt;/p&gt;
&lt;p&gt;But what actually happens behind the scenes?&lt;/p&gt;
&lt;p&gt;How does the system remember you?&lt;/p&gt;
&lt;p&gt;Let’s break it down simply &lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;first-what-is-authentication-&quot;&gt;First: What is Authentication?&lt;/h3&gt;
&lt;p&gt;Authentication =&lt;/p&gt;
&lt;p&gt; “Proving that you are who you say you are”&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Login form&lt;/li&gt;
&lt;li&gt;OTP verification&lt;/li&gt;
&lt;li&gt;Google login&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-basic-flow-before-jwt-session-oauth-&quot;&gt;⚙️ Basic Flow (Before JWT / Session / OAuth)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;User enters email + password&lt;/li&gt;
&lt;li&gt;Server verifies credentials&lt;/li&gt;
&lt;li&gt;Now server needs to “remember” the user&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; This is where things get interesting&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-session-based-authentication&quot;&gt;1. Session-Based Authentication&lt;/h2&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;User logs in&lt;/li&gt;
&lt;li&gt;Server creates a &lt;strong&gt;session&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Stores session in database/memory&lt;/li&gt;
&lt;li&gt;Sends a &lt;strong&gt;session ID&lt;/strong&gt; to browser (via cookie)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;p&gt; Every request → sends session ID&lt;br&gt;
 Server checks → “Yes, this is the user”&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;flow&quot;&gt;Flow&lt;/h3&gt;
&lt;p&gt;Login → Session Created → Session ID stored in browser → Request with ID → Server validates&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-pros&quot;&gt;✅ Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Simple to implement&lt;/li&gt;
&lt;li&gt;Easy to invalidate (logout)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cons&quot;&gt;❌ Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Server has to store sessions&lt;/li&gt;
&lt;li&gt;Not scalable for large systems&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-2-jwt-json-web-token-&quot;&gt;⚡ 2. JWT (JSON Web Token)&lt;/h2&gt;
&lt;h3 id=&quot;how-it-works-1&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;Instead of storing session on server…&lt;/p&gt;
&lt;p&gt; Server gives token to user&lt;/p&gt;
&lt;p&gt;This token contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User ID&lt;/li&gt;
&lt;li&gt;Expiry time&lt;/li&gt;
&lt;li&gt;Signature&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;p&gt; Client stores JWT (localStorage / cookie)&lt;br&gt;
 Sends it with every request&lt;/p&gt;
&lt;p&gt;Server:&lt;/p&gt;
&lt;p&gt; Verifies token → allows access&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;flow-1&quot;&gt;Flow&lt;/h3&gt;
&lt;p&gt;Login → JWT generated → Stored on client → Sent with requests → Verified&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-pros-1&quot;&gt;✅ Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Stateless (no server storage)&lt;/li&gt;
&lt;li&gt;Scalable&lt;/li&gt;
&lt;li&gt;Fast&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cons-1&quot;&gt;❌ Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Hard to revoke instantly&lt;/li&gt;
&lt;li&gt;Needs proper security handling&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-oauth-login-with-google-github-&quot;&gt;3. OAuth (Login with Google, GitHub)&lt;/h2&gt;
&lt;h3 id=&quot;how-it-works-2&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;Instead of creating your own login system…&lt;/p&gt;
&lt;p&gt; You use another provider&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Login with Google”&lt;/li&gt;
&lt;li&gt;“Login with GitHub”&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;flow-2&quot;&gt;Flow&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;User clicks Google login&lt;/li&gt;
&lt;li&gt;Redirect to Google&lt;/li&gt;
&lt;li&gt;User authenticates&lt;/li&gt;
&lt;li&gt;Google sends token back&lt;/li&gt;
&lt;li&gt;Your app trusts Google → logs user in&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-pros-2&quot;&gt;✅ Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;No need to manage passwords&lt;/li&gt;
&lt;li&gt;More secure&lt;/li&gt;
&lt;li&gt;Faster user onboarding&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-cons-2&quot;&gt;❌ Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Dependency on third-party&lt;/li&gt;
&lt;li&gt;Slightly complex setup&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;jwt-vs-session-vs-oauth-simple-comparison-&quot;&gt;JWT vs Session vs OAuth (Simple Comparison)&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature     &lt;/th&gt;&lt;th&gt;Session    &lt;/th&gt;&lt;th&gt;JWT         &lt;/th&gt;&lt;th&gt;OAuth&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Storage     &lt;/td&gt;&lt;td&gt;Server     &lt;/td&gt;&lt;td&gt;Client      &lt;/td&gt;&lt;td&gt;Third-party&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Scalability &lt;/td&gt;&lt;td&gt;Medium     &lt;/td&gt;&lt;td&gt;High        &lt;/td&gt;&lt;td&gt;High&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complexity  &lt;/td&gt;&lt;td&gt;Easy       &lt;/td&gt;&lt;td&gt;Medium      &lt;/td&gt;&lt;td&gt;Medium-Hard&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Use Case    &lt;/td&gt;&lt;td&gt;Small apps &lt;/td&gt;&lt;td&gt;APIs / SPAs &lt;/td&gt;&lt;td&gt;Social login&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-to-use-what-&quot;&gt;When to Use What?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Small app → Session&lt;/li&gt;
&lt;li&gt;Modern API / frontend → JWT&lt;/li&gt;
&lt;li&gt;Social login → OAuth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Many apps use combination (JWT + OAuth)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-common-mistakes&quot;&gt;⚠️ Common Mistakes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Storing JWT insecurely ❌&lt;/li&gt;
&lt;li&gt;Not setting expiry ❌&lt;/li&gt;
&lt;li&gt;Weak session management ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Security matters a lot here&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Authentication looks simple on UI…&lt;/p&gt;
&lt;p&gt;But behind the scenes:&lt;/p&gt;
&lt;p&gt; It’s all about trust, identity, and security&lt;/p&gt;
&lt;p&gt;As a developer:&lt;/p&gt;
&lt;p&gt;Understanding this = real backend skill&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Now next time you click “Login”…&lt;/p&gt;
&lt;p&gt;You know exactly what’s happening &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15033/how-authentication-really-works-jwt-vs-session-vs-oauth</guid>
<pubDate>Fri, 17 Apr 2026 06:14:22 +0000</pubDate>
</item>
<item>
<title>Advance Auth Templete</title>
<link>https://coderlegion.com/10764/advance-auth-templete</link>
<description>&lt;p&gt;&lt;strong&gt; Advance Auth (TypeScript) – Production-Ready Auth Template for Real Projects&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I built a modular, production-grade authentication template so I never have to rewrite auth again, and now you don’t either.&lt;/p&gt;
&lt;p&gt;This is not a demo. This is a fork-and-build foundation for real SaaS, startups, and side projects.&lt;/p&gt;
&lt;p&gt;Repo: &lt;em&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/SAYOUNCDR/Advance-Auth&quot;&gt;https://github.com/SAYOUNCDR/Advance-Auth&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; What’s inside&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A full auth system written in TypeScript + Node + Express, designed the way it should be done in real backends:&lt;/p&gt;
&lt;p&gt;Login &amp;amp; Register with HTTP-only cookies&lt;/p&gt;
&lt;p&gt;JWT access + refresh token rotation&lt;/p&gt;
&lt;p&gt;RBAC (User / Admin roles)&lt;/p&gt;
&lt;p&gt;Google OAuth&lt;/p&gt;
&lt;p&gt;Email verification flow&lt;/p&gt;
&lt;p&gt;Forgot / Reset password&lt;/p&gt;
&lt;p&gt;2FA with TOTP (Google Authenticator)&lt;/p&gt;
&lt;p&gt;Zod validation everywhere&lt;/p&gt;
&lt;p&gt;Modular, clean architecture&lt;/p&gt;
&lt;p&gt;Secure password hashing (bcrypt)&lt;/p&gt;
&lt;p&gt;Token versioning, secure cookie handling&lt;/p&gt;
&lt;p&gt;Ready for scaling and extension&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⚠️ What’s left (being honest)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Only two things are pending:&lt;/p&gt;
&lt;p&gt;Final rate limiting middleware wiring&lt;/p&gt;
&lt;p&gt;A few extra security hardening checks&lt;/p&gt;
&lt;p&gt;The core system is already solid and usable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Why this matters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Auth is boring, repetitive, and easy to mess up.&lt;/p&gt;
&lt;p&gt;Now you can:&lt;/p&gt;
&lt;p&gt;Fork this repo&lt;/p&gt;
&lt;p&gt;Plug in your business logic&lt;/p&gt;
&lt;p&gt;Start building features immediately&lt;/p&gt;
&lt;p&gt;Modify or extend auth without pain&lt;/p&gt;
&lt;p&gt;No rewrites. No hacks. No copy-paste hell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;️ Stack&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;TypeScript&lt;/p&gt;
&lt;p&gt;Node.js + Express&lt;/p&gt;
&lt;p&gt;MongoDB (Mongoose)&lt;/p&gt;
&lt;p&gt;JWT&lt;/p&gt;
&lt;p&gt;Zod&lt;/p&gt;
&lt;p&gt;OAuth&lt;/p&gt;
&lt;p&gt;Nodemailer&lt;/p&gt;
&lt;p&gt;otplib (2FA)&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Who should use this&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Indie hackers&lt;/p&gt;
&lt;p&gt;Startup founders&lt;/p&gt;
&lt;p&gt;Full-stack devs&lt;/p&gt;
&lt;p&gt;Anyone tired of rebuilding auth from scratch&lt;/p&gt;
&lt;p&gt;Fork it, break it, improve it, ship with it.&lt;/p&gt;
&lt;p&gt;If you care about clean backend engineering, this is for you.&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/10764/advance-auth-templete</guid>
<pubDate>Fri, 30 Jan 2026 11:40:29 +0000</pubDate>
</item>
<item>
<title>Building Secure JWT Auth in NestJS: Argon2, Redis Blacklisting, and Token Rotation</title>
<link>https://coderlegion.com/9960/building-secure-jwt-auth-in-nestjs-argon2-redis-blacklisting-and-token-rotation</link>
<description>&lt;p&gt;Imagine you've just watched your first tutorial on authentication. You spin up a login flow, add some JWTs, and call it done. Then you push to production and realize those tutorials skipped the hard parts—token revocation, session management, replay attacks.&lt;/p&gt;
&lt;p&gt;It's not enough to have a login endpoint. Production auth means thinking like an attacker and handling edge cases that basic tutorials ignore. Let's talk about what actually secure authentication looks like in NestJS and how to implement it without the security holes.&lt;/p&gt;
&lt;p&gt;When a developer says they are &quot;implementing auth,&quot; they are usually talking about two distinct but inseparable concepts: &lt;strong&gt;Authentication&lt;/strong&gt; and &lt;strong&gt;Authorization&lt;/strong&gt;. When building robust systems, these go hand in hand.&lt;/p&gt;
&lt;h4 id=&quot;authentication-authn-vs-authorization-authz-&quot;&gt;Authentication (AuthN) vs. Authorization (AuthZ)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt; is about &lt;strong&gt;identity&lt;/strong&gt;. It answers &quot;Who are you?&quot; using credentials like email/password, passkeys, or biometrics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authorization&lt;/strong&gt; is about &lt;strong&gt;permissions&lt;/strong&gt;. It answers &quot;What can you do?&quot; Once we know who you are, we use strategies like Role-Based Access Control (RBAC) to ensure you only touch the resources you're allowed to.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-nestjs-toolkit-passport-and-guards&quot;&gt;The NestJS Toolkit: Passport and Guards&lt;/h3&gt;
&lt;p&gt;In the NestJS ecosystem, we don't reinvent the wheel. We use &lt;strong&gt;Passport.js&lt;/strong&gt;, the industry standard for Node.js authentication. NestJS wraps this in &lt;code&gt;@nestjs/passport&lt;/code&gt; and provides &lt;strong&gt;AuthGuards&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Guards are the &quot;bouncers&quot; of your application. When a request hits a protected route, the Guard intercepts it, extracts the token, and validates it before the request ever reaches your controller logic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private readonly reflector: Reflector,
    private readonly redis: RedisService,
  ) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise&amp;lt;boolean&amp;gt; {
    const isPublic = this.reflector.getAllAndOverride&amp;lt;boolean&amp;gt;(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    if (!request.headers['authorization']) return false;

    const token = request.headers['authorization'].split(' ')[1] as string;

    // Check if the token was manually revoked (logout)
    const isBlacklisted = await this.redis.isTokenBlacklisted(token);
    if (isBlacklisted) return false;

    const result = super.canActivate(context);
    return result instanceof Promise ? result : (result as boolean);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;secure-onboarding-why-argon2-&quot;&gt;Secure Onboarding: Why Argon2?&lt;/h3&gt;
&lt;p&gt;For sign-up, you need to store passwords. I chose &lt;strong&gt;Argon2&lt;/strong&gt; over the more common Bcrypt. Argon2 was the winner of the Password Hashing Competition for a reason: it's &lt;strong&gt;memory-intensive&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Unlike older algorithms, Argon2 allows you to configure memory usage, making it significantly harder to crack using specialized hardware like GPUs or ASICs. It essentially punishes brute-force attempts with slow, high-resource computation.&lt;/p&gt;
&lt;h4 id=&quot;registration-hashing-&quot;&gt;Registration (Hashing)&lt;/h4&gt;
&lt;p&gt;When a user signs up, the plain-text password is transformed into a hash before being stored in the database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// UsersService
import { hash } from '@node-rs/argon2';

async create(data: CreateUserDto): Promise&amp;lt;User&amp;gt; {
  let userData = { ...data };
  if (data.password) {
    const passwordHash = await hash(data.password); //
    userData = { ...userData, password: passwordHash };
  }
  return await this.prisma.user.create({ data: userData });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;login-verification-&quot;&gt;Login (Verification)&lt;/h4&gt;
&lt;p&gt;Crucially, we never &quot;decrypt&quot; a password. When a user logs in:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We retrieve the stored hash from the DB.&lt;/li&gt;
&lt;li&gt;We take the plain-text password provided in the login form.&lt;/li&gt;
&lt;li&gt;We run the provided password through the same hashing process (including the &quot;salt&quot; stored within the hash).&lt;/li&gt;
&lt;li&gt;If the resulting hash matches the stored one, the user is verified.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { verify } from '@node-rs/argon2';

// AuthService
async validateUser(email: string, password: string): Promise&amp;lt;UserEntity | undefined&amp;gt; {
  const user = await this.usersService.findOneByEmail(email);
  if (user &amp;amp;&amp;amp; user.password) {
    /* The verify function automatically creates a hash of the plain
password and compares it against our stored password hash */
    const isPasswordValid = await verify(user.password, password);
    if (isPasswordValid) return new UserEntity(user);
    throw new UnauthorizedException('Password is not valid');
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-dual-token-strategy&quot;&gt;The Dual-Token Strategy&lt;/h3&gt;
&lt;p&gt;Once verified, we generate two tokens.&lt;/p&gt;
&lt;h4 id=&quot;1-the-access-token-short-lived-amp-stateless-&quot;&gt;1. The Access Token (Short-lived &amp;amp; Stateless)&lt;/h4&gt;
&lt;p&gt;Signed with a secret key, this contains the user’s ID and metadata. We set this to &lt;strong&gt;15 minutes&lt;/strong&gt;. This is the industry standard because it limits the &quot;blast radius&quot;—if a token is stolen, it’s only useful for a tiny window.&lt;/p&gt;
&lt;h4 id=&quot;2-the-refresh-token-long-lived-amp-stateful-&quot;&gt;2. The Refresh Token (Long-lived &amp;amp; Stateful)&lt;/h4&gt;
&lt;p&gt;Signed with a &lt;strong&gt;completely different secret&lt;/strong&gt; (this is vital for security), we set this to &lt;strong&gt;7 days&lt;/strong&gt;. Unlike the access token, we store this in our database (Prisma) because it needs to be &lt;strong&gt;stateful&lt;/strong&gt;. We also attach a unique JTI (JWT ID) when signing the token to allow for efficient database lookups after decoding the JWT.&lt;/p&gt;
&lt;p&gt;By storing it, we gain a &quot;Kill Switch.&quot; If we detect suspicious activity, we can manually revoke that specific token in the DB, preventing it from ever being used to generate a new access token. Here's how we create and store the refresh token in our database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { randomUUID } from 'node:crypto';

// JwtRefreshService
async create(payload: CreateRefreshTokenDto): Promise&amp;lt;string&amp;gt; {
  const tokenId = randomUUID(); // Unique ID for this token
  const refresh_token = this.jwtService.sign(
    { ...payload, jti: tokenId },
    {
      secret: this.configService.get('JWT_REFRESH_SECRET'),
      expiresIn: '7d',
    },
  );

  const hashed_refresh_token = await hash(refresh_token);

  await this.prisma.refreshToken.create({
    data: {
      id: tokenId,
      token: hashed_refresh_token,
      userId: payload.sub,
      absolute_limit_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // Hard session limit
      isRevoked: false,
    },
  });

  return refresh_token;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;solving-the-stateless-revocation-problem-with-redis&quot;&gt;Solving the &quot;Stateless Revocation&quot; Problem with Redis&lt;/h3&gt;
&lt;p&gt;The biggest flaw in JWTs is that they are stateless. If a user logs out, their Access Token is still technically valid until those 15 minutes are up.&lt;/p&gt;
&lt;p&gt;To fix this without destroying performance, we use &lt;strong&gt;Redis&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Redis is an in-memory key-value store. Because it lives in RAM, it provides the sub-millisecond read/writes we need to check every single incoming request without slowing down the API. On logout, we &quot;blacklist&quot; the access token in Redis for the remainder of its lifespan. Our &lt;code&gt;JwtAuthGuard&lt;/code&gt; then checks Redis on every request: if the token is in the blacklist, access is denied. When the user logs out, we calculate how long until the access token would naturally expire and blacklist it for exactly that duration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// RedisService implementation
async blacklistToken(token: string, ttl: number): Promise&amp;lt;void&amp;gt; {
  await this.redis.set(`blacklist:${token}`, '1', { EX: ttl }); //
}

async isTokenBlacklisted(token: string): Promise&amp;lt;boolean&amp;gt; {
    const result = await this.redis.exists(`blacklist:${token}`);
    return result === 1;
 }

// JwtAuthGuard
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
    // existing authorization logic

    // Check if the token was manually revoked (logout)
    const isBlacklisted = await this.redis.isTokenBlacklisted(token);
    if (isBlacklisted) return false;

    // return result
   }
}

// Logout logic
async logout(data: { userId: string; access_token: string; refresh_token: string }) {
  const access_token_payload = await this.jwtService.verifyAsync(data.access_token);
  const ttl = access_token_payload.exp - Math.floor(Date.now() / 1000);
  
  if (ttl &amp;gt; 0) {
    await this.redisService.blacklistToken(data.access_token, ttl); //
  }
  // ... proceed to revoke refresh token
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;refresh-token-rotation-amp-absolute-limits&quot;&gt;Refresh Token Rotation &amp;amp; Absolute Limits&lt;/h3&gt;
&lt;p&gt;What if a refresh token is stolen? An attacker could theoretically stay logged in for 7 days. We solve this with &lt;strong&gt;Refresh Token Rotation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Every time a user uses a Refresh Token to get a new Access Token, we:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Invalidate&lt;/strong&gt; the old Refresh Token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Issue&lt;/strong&gt; a brand-new Refresh Token.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-absolute-time-limit&quot;&gt;The Absolute Time Limit&lt;/h4&gt;
&lt;p&gt;To prevent a session from being kept alive forever by rotation, we implement an &lt;strong&gt;Absolute Time Limit&lt;/strong&gt; (e.g., 90 days). This is set at the initial login. Every time a token rotates, that original 90-day timestamp is carried forward. Once we hit that date, no more rotations are allowed—the user must log in with their password again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// rotateRefreshToken logic
async rotateRefreshToken(payload: RotateRefreshTokenDto) {
  const { old_refresh_token_id, time_limit, sub, email, deviceInfo } = payload;
  
  await this.revokeRefreshToken(old_refresh_token_id); // Invalidate old

  const new_refresh_token = this.jwtService.sign(
        {
          email,
          sub,
          deviceInfo,
        },
        {
          secret: this.configService.get&amp;lt;string&amp;gt;('JWT_REFRESH_SECRET'),
          expiresIn: '7d',
        },
      );
  
  // Create new token record with the original time_limit
  await this.prisma.refreshToken.create({
    data: {
      token: await hash(new_refresh_token),
      userId: sub,
      absolute_limit_at: time_limit, // Carry forward absolute limit
      isRevoked: false,
    },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  async revokeRefreshToken(token_id: string) {
    try {
      await this.prisma.refreshToken.update({
        where: {
          id: token_id,
        },
        data: {
          isRevoked: true,
        },
      });
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        if (error.code === 'P2025') {
          throw new NotFoundException('Token not found');
        }
      }
      throw error;
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  @Public()
  @UseGuards(JwtRefreshAuthGuard)
  @Post('refresh')
  async refresh(
    @Request()
    req: RequestType &amp;amp; {
      user: { email: string; id: string; refreshToken: string };
    },
    @Response({ passthrough: true }) res: ResponseType,
  ) {
    if (req.user) {
      const tokens = await this.authService.refreshTokens({
        userId: req.user.id ?? '',
        email: req.user.email ?? '',
        token: req.user.refreshToken ?? '',
      });

      res.cookie('titan_refresh_token', tokens?.refresh_token, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
      });

      return { access_token: tokens?.access_token };
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;reuse-detection-the-nuclear-option-&quot;&gt;Reuse Detection (The &quot;Nuclear&quot; Option)&lt;/h4&gt;
&lt;p&gt;If a token that was already revoked is used, it signals a potential theft. The system responds by revoking all tokens for that user.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// validateToken snippet
if (storedToken?.isRevoked) {
  // Nuke 'em
  await this.prisma.refreshToken.updateMany({
    where: { userId: payload.userId },
    data: { isRevoked: true }, // Revoke all active sessions for this user
  });
  throw new UnauthorizedException('Token reuse detected. Please login again');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;One thing to note is that while we return the access token to the user in the response body, we have to set the refresh token as a http-only cookie.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;  @Post('login')
@UseGuards(LocalAuthGuard)
async login(
  @Request() req: RequestType &amp;amp; { user: UserEntity },
  @Response({ passthrough: true }) res: ResponseType,
) {
  const deviceInfo = req.headers['user-agent'] || 'Unknown Device';

  const { access_token, refresh_token } = await this.authService.login(
    req.user,
    deviceInfo,
  );

  // Securely set the Refresh Token as a cookie
  res.cookie('titan_refresh_token', refresh_token, {
    httpOnly: true, // Prevents JS from reading the cookie
    secure: true,   // Only sent over HTTPS
    sameSite: 'strict', // Protects against CSRF
    path: '/auth/refresh', // Optional: Only send cookie to the refresh endpoint
  });

  // Send the Access Token in the body for the frontend to store in memory
  return { access_token };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason for this is to make the refresh token invisible to the client-side javascript. Even if an attacker successfully runs a script on your page, they cannot &quot;read&quot; the refresh token. We also set secure: true (ensuring it's only sent over HTTPS) and sameSite: strict to protect against CSRF attacks.&lt;/p&gt;
&lt;p&gt;As for the access token, the frontend receives it in the JSON response and typically stores it in memory (a variable). While it's still technically vulnerable to XSS, because it isn't persisted in storage, it's much harder to exfiltrate, and its short 15-minute lifespan limits the damage if it is compromised&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;At the end of the day, writing code alone doesn't build a secure authentication system. We need to constantly balance security, speed, and user experience. By thinking like an attacker, we can turn standard JWTs into a fortified system using tools like Redis and techniques like Refresh Rotation. It takes a bit more effort to set up, but the peace of mind knowing you’ve accounted for edge cases like token theft or stale sessions is well worth the complexity, especially in sensitive applications.&lt;/p&gt;
&lt;h4 id=&quot;summary-of-our-security-workflow&quot;&gt;Summary of our Security Workflow&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory-Hard Hashing:&lt;/strong&gt; Used Argon2 to ensure passwords are resistant to GPU-based cracking.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dual-Token Architecture:&lt;/strong&gt; Balanced security with short-lived (15 min) access tokens and long-lived (7 day) refresh tokens.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-time Revocation:&lt;/strong&gt; Leveraged Redis for sub-millisecond access token blacklisting upon logout.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session Lifecycle Management:&lt;/strong&gt; Implemented 90-day absolute time limits to ensure no session lasts forever.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic Breach Detection:&lt;/strong&gt; Enabled Refresh Token Rotation with a &quot;nuclear&quot; reuse detection trigger that invalidates all user sessions if a stolen token is detected.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;technical-stack-overview&quot;&gt;Technical Stack Overview&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool &lt;/th&gt;&lt;th&gt;Purpose &lt;/th&gt;&lt;th&gt;Key Feature&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;NestJS &lt;/td&gt;&lt;td&gt;Backend Framework &lt;/td&gt;&lt;td&gt;Modular architecture and robust dependency injection.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Passport.js &lt;/td&gt;&lt;td&gt;Auth Logic &lt;/td&gt;&lt;td&gt;Industry-standard middleware for handling JWT strategies.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Argon2 &lt;/td&gt;&lt;td&gt;Password and Token Hashing &lt;/td&gt;&lt;td&gt;Memory-intensive hashing&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Prisma &lt;/td&gt;&lt;td&gt;Database ORM &lt;/td&gt;&lt;td&gt;Type-safe interaction with our stateful Refresh Token storage.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Redis &lt;/td&gt;&lt;td&gt;In-Memory Store &lt;/td&gt;&lt;td&gt;Rapid read/write operations for stateless token revocation (blacklisting).&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;JWT &lt;/td&gt;&lt;td&gt;Tokenization &lt;/td&gt;&lt;td&gt;Secure, encoded payload delivery for AuthN and AuthZ.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/9960/building-secure-jwt-auth-in-nestjs-argon2-redis-blacklisting-and-token-rotation</guid>
<pubDate>Sun, 18 Jan 2026 17:16:38 +0000</pubDate>
</item>
<item>
<title>Authentication Tokens: Your Digital VIP Pass </title>
<link>https://coderlegion.com/5540/authentication-tokens-your-digital-vip-pass</link>
<description>&lt;p&gt;In today's connected world, we all want to access our favorite apps and websites without re-entering our password every two seconds. That's where &lt;strong&gt;authentication tokens&lt;/strong&gt;, or &quot;auth tokens,&quot; come in. Think of them like a digital VIP pass. When you log in with your username and password, the server checks your credentials and, if everything's good, hands you a special token. Instead of sending your password with every request, your app sends this token. The server then uses the token to confirm you're still you, and you can access all the cool stuff you're supposed to.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-it-works-a-simple-flow&quot;&gt;How It Works: A Simple Flow&lt;/h3&gt;
&lt;p&gt;The basic process is pretty straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Login:&lt;/strong&gt; You enter your username and password on an application's login screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification:&lt;/strong&gt; The application sends your credentials to its server for verification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Generation:&lt;/strong&gt; If your credentials are correct, the server generates a unique auth token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Received:&lt;/strong&gt; The server sends this token back to your application, which stores it, usually in a cookie or local storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access:&lt;/strong&gt; From now on, whenever you need to access a protected resource, like your profile page or your shopping cart, the application includes the token in the request header.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token Validation:&lt;/strong&gt; The server receives the request, validates the token, and grants access if it's valid.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7036098958547073519&quot; alt=&quot;A Simple Flow&quot;&gt;&lt;/p&gt;
&lt;p&gt;This system is great for &lt;strong&gt;scalability&lt;/strong&gt; because the server doesn't need to remember who is logged in. This makes the server &lt;strong&gt;stateless&lt;/strong&gt;, which is a huge plus for performance and building microservice architectures.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;types-of-tokens-you-ll-encounter&quot;&gt;Types of Tokens You'll Encounter&lt;/h3&gt;
&lt;p&gt;There are a few different types of tokens, each with its own pros and cons.&lt;/p&gt;
&lt;h4 id=&quot;1-json-web-tokens-jwts-&quot;&gt;1. JSON Web Tokens (JWTs)&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;JWT&lt;/strong&gt; (pronounced &quot;jot&quot;) is a popular type of token that's &lt;strong&gt;self-contained&lt;/strong&gt;. This means all the necessary user information is right inside the token itself. A JWT is made up of three parts, separated by periods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Header:&lt;/strong&gt; Describes the token type and the signing algorithm.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Payload:&lt;/strong&gt; Contains &quot;claims,&quot; which are statements about the user and the token. This is where you'd find user ID, roles, and the token's expiration date.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signature:&lt;/strong&gt; A unique signature created using a secret key. This is what the server uses to verify that the token hasn't been tampered with.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because a JWT contains everything the server needs to know, the server doesn't have to perform a database lookup for every request. This makes them really fast and efficient.&lt;/p&gt;
&lt;p&gt;Here's a quick Python example using the &lt;code&gt;PyJWT&lt;/code&gt; library to create a JWT:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import jwt
import datetime

payload_data = {
    'sub': '1234567890',
    'name': 'John Doe',
    'iat': datetime.datetime.now(datetime.timezone.utc),
    'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=30)
}

secret_key = 'your_super_secret_key' # In a real app, use an environment variable!

token = jwt.encode(payload_data, secret_key, algorithm='HS256')

print(token)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then decode and verify this token on the server side to get the payload data.&lt;/p&gt;
&lt;h4 id=&quot;2-opaque-tokens&quot;&gt;2. Opaque Tokens&lt;/h4&gt;
&lt;p&gt;Unlike JWTs, opaque tokens are just random strings of characters. The client can't read anything from them. They are essentially a &lt;strong&gt;reference&lt;/strong&gt; to a record stored in the server's database. When a server receives an opaque token, it has to go to the database to look up the token and retrieve the associated user data and permissions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros &amp;amp; Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JWTs:&lt;/strong&gt; Fast, scalable, and don't require database lookups. The downside is that once issued, they can't be easily revoked before they expire.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opaque Tokens:&lt;/strong&gt; More secure since no information is exposed to the client. They are also easily revocable. The trade-off is that they require a database lookup on every request, which can add overhead.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-hybrid-tokens&quot;&gt;3. Hybrid Tokens&lt;/h4&gt;
&lt;p&gt;A common and secure pattern is to use a &lt;strong&gt;hybrid approach&lt;/strong&gt;. This combines the best of both worlds. You'll often see this with OAuth 2.0:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Short-lived Access Token:&lt;/strong&gt; This is a JWT, used for accessing resources. It's valid for a short time (e.g., 15 minutes), so if it's stolen, the attacker doesn't have much time to use it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-lived Refresh Token:&lt;/strong&gt; This is an opaque token, used to get a new access token once the old one expires. It's stored securely and can be revoked by the server if needed. This prevents a user from having to log in again every 15 minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach balances the performance benefits of JWTs with the security and revocability of opaque tokens.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-which-one-should-you-use-&quot;&gt;Conclusion: Which One Should You Use?&lt;/h3&gt;
&lt;p&gt;The choice of token depends on your application's needs. For a simple app where you want to keep things fast and stateless, a &lt;strong&gt;JWT&lt;/strong&gt; might be the perfect fit. For a system that requires a high level of security and granular control over token revocation, a &lt;strong&gt;hybrid model&lt;/strong&gt; is often the way to go. No matter which you choose, understanding how they work is a crucial skill for any developer building secure and scalable applications. &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/5540/authentication-tokens-your-digital-vip-pass</guid>
<pubDate>Wed, 24 Sep 2025 13:05:12 +0000</pubDate>
</item>
<item>
<title>Understanding JWT: From Basics to Advanced Security</title>
<link>https://coderlegion.com/1280/understanding-jwt-from-basics-to-advanced-security</link>
<description>
&lt;h2 id=&quot;introduction-the-evolution-of-web-authentication&quot;&gt;Introduction: The Evolution of Web Authentication&lt;/h2&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1666969295767-4db7ff1f8fec?crop=entropy&amp;amp;cs=tinysrgb&amp;amp;fit=max&amp;amp;fm=jpg&amp;amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDYwfHxUb2tlbnxlbnwwfHx8fDE3NDAwODM4MDN8MA&amp;amp;ixlib=rb-4.0.3&amp;amp;q=80&amp;amp;w=2000&quot; alt=&quot;Understanding JWT: From Basics to Advanced Security&quot;&gt;&lt;p&gt;Early days of the web, authentication was relatively simple - users would log in, and servers would store their session information. However, as applications grew more complex and moved toward distributed systems and microservices, this traditional approach faced scalability challenges. JSON Web Tokens (JWTs), a revolutionary solution that changed how we handle authentication and authorization in modern web applications.&lt;/p&gt;&lt;p&gt;JWTs represent a paradigm shift in web security. Instead of the server keeping track of who's logged in, it issues a secure, self-contained token that holds all necessary information about the user. Think of it like a digital passport - once issued by a trusted authority, it can be verified anywhere without needing to check back with the issuing country. This approach has become increasingly important in our interconnected world of cloud services, mobile applications, and API-driven architectures.&lt;/p&gt;&lt;h2 id=&quot;part-1-jwt-fundamentals&quot;&gt;Part 1: JWT Fundamentals&lt;/h2&gt;&lt;h3 id=&quot;what-is-a-jwt&quot;&gt;What is a JWT?&lt;/h3&gt;&lt;p&gt;A JSON Web Token is a compact, self-contained way to securely transmit information between parties as a JSON object. Think of it as a digital passport that proves your identity and permissions in the web world. Each JWT contains three essential parts:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Header: The metadata section that describes the token's type and the signing algorithm used&lt;/li&gt;&lt;li&gt;Payload: The actual data (called &quot;claims&quot;) about the user and additional metadata&lt;/li&gt;&lt;li&gt;Signature: A verification seal that ensures the token hasn't been tampered with&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;When these three parts are combined, you get something that looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;the-anatomy-of-a-jwt-understanding-the-structure&quot;&gt;The Anatomy of a JWT: Understanding the Structure&lt;/h3&gt;&lt;p&gt;Before diving into how JWTs work, let's examine their structure in detail. A JWT consists of three distinct parts, each encoded in Base64URL format and separated by dots:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;1-header-the-metadata&quot;&gt;1. Header (The Metadata)&lt;/h3&gt;&lt;p&gt;The header contains information about how the token itself is constructed. It typically includes:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,    // The algorithm used for signing
  &quot;typ&quot;: &quot;JWT&quot;       // The type of token
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;2-payload-the-claims&quot;&gt;2. Payload (The Claims)&lt;/h3&gt;&lt;p&gt;The payload carries the actual information about the user and token metadata. It contains claims, which are statements about an entity (typically the user) and additional metadata. &lt;/p&gt;&lt;p&gt;There are three types of claims:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Public Claims&lt;/strong&gt;: Custom claims registered in the IANA JSON Web Token Registry&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Private Claims&lt;/strong&gt;: Custom claims created for sharing information between parties&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Registered Claims&lt;/strong&gt;: Predefined claims like:&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;iss&quot;: &quot;auth.service.com&quot;,  // Issuer
  &quot;sub&quot;: &quot;user123&quot;,           // Subject
  &quot;exp&quot;: 1516239022,         // Expiration Time
  &quot;iat&quot;: 1516232822          // Issued At
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;3-signature-the-verification-seal&quot;&gt;3. Signature (The Verification Seal)&lt;/h3&gt;&lt;p&gt;The signature is created by taking the encoded header, encoded payload, and a secret key, then applying the algorithm specified in the header. For example, with HMAC SHA256:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret
)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This signature ensures that the token hasn't been modified in transit and verifies its authenticity.&lt;/p&gt;&lt;h3 id=&quot;how-does-jwt-work-in-practice&quot;&gt;How Does JWT Work in Practice?&lt;/h3&gt;&lt;p&gt;The journey of a JWT follows a simple flow:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: When you log in successfully, the server creates a JWT containing information about your identity and permissions&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Token Storage&lt;/strong&gt;: Your application receives and stores this token&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Authorization&lt;/strong&gt;: For subsequent requests, your application includes this token in the header&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Verification&lt;/strong&gt;: The server checks the token's signature and grants access if it's valid&lt;/li&gt;&lt;/ol&gt;&lt;figure class=&quot;kg-card kg-image-card&quot;&gt;&lt;img src=&quot;https://guptadeepak.com/content/images/2025/02/JWT-Flow-deepak-gupta.png&quot; class=&quot;kg-image&quot; alt=&quot;Understanding JWT: From Basics to Advanced Security&quot; width=&quot;1760&quot; height=&quot;1948&quot; sizes=&quot;(min-width: 720px) 720px&quot;&gt;&lt;/figure&gt;&lt;p&gt;This process eliminates the need for the server to store session information, making JWTs perfect for modern, scalable applications.&lt;/p&gt;&lt;h2 id=&quot;part-2-the-security-challenge&quot;&gt;Part 2: The Security Challenge&lt;/h2&gt;&lt;h3 id=&quot;common-jwt-vulnerabilities&quot;&gt;Common JWT Vulnerabilities&lt;/h3&gt;&lt;p&gt;While JWTs are powerful, they're not immune to security issues. Here are the key vulnerabilities to watch out for:&lt;/p&gt;&lt;h4 id=&quot;signature-verification-flaws&quot;&gt;Signature Verification Flaws&lt;/h4&gt;&lt;p&gt;The signature is like a tamper-evident seal on a package. If the verification process is flawed or the secret key is weak, attackers can modify the token's contents without detection. This is particularly dangerous because it could allow unauthorized access to protected resources.&lt;/p&gt;&lt;h4 id=&quot;algorithm-confusion-attacks&quot;&gt;Algorithm Confusion Attacks&lt;/h4&gt;&lt;p&gt;JWTs support multiple signing algorithms, including a dangerous &quot;none&quot; algorithm. If your server isn't configured properly, an attacker might trick it into accepting an unsigned token, effectively bypassing security checks altogether.&lt;/p&gt;&lt;h4 id=&quot;client-side-storage-risks&quot;&gt;Client-Side Storage Risks&lt;/h4&gt;&lt;p&gt;Many applications store JWTs in browser storage (localStorage or sessionStorage), making them vulnerable to Cross-Site Scripting (XSS) attacks. Think of this like keeping your house key in an obvious place - if someone gains access to your browser's JavaScript environment, they could steal your token.&lt;/p&gt;&lt;h4 id=&quot;the-revocation-challenge&quot;&gt;The Revocation Challenge&lt;/h4&gt;&lt;p&gt;Unlike traditional session tokens, JWTs don't have a built-in way to be invalidated before they expire. This means if a token is compromised, it remains valid until its expiration time, potentially giving attackers a window of opportunity.&lt;/p&gt;&lt;h2 id=&quot;part-3-building-a-secure-jwt-implementation&quot;&gt;Part 3: Building a Secure JWT Implementation&lt;/h2&gt;&lt;h3 id=&quot;essential-security-practices&quot;&gt;Essential Security Practices&lt;/h3&gt;&lt;p&gt;Let's explore how to implement JWTs securely:&lt;/p&gt;&lt;h4 id=&quot;1-secure-communication&quot;&gt;1. Secure Communication&lt;/h4&gt;&lt;p&gt;Always transmit JWTs over HTTPS. This encrypts the token during transmission, preventing eavesdropping attacks. It's like sending sensitive documents through a secure courier service instead of regular mail.&lt;/p&gt;&lt;h4 id=&quot;2-smart-token-storage&quot;&gt;2. Smart Token Storage&lt;/h4&gt;&lt;p&gt;Instead of using browser storage, use HttpOnly cookies with the Secure flag enabled. This approach prevents JavaScript access to the token while ensuring it's only transmitted over secure connections. The cookie settings might look like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Set-Cookie: access_token=&amp;lt;token&amp;gt;;
            HttpOnly;
            Secure;
            SameSite=Strict;
            Path=/api
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;3-token-lifecycle-management&quot;&gt;3. Token Lifecycle Management&lt;/h4&gt;&lt;p&gt;Implement a robust token lifecycle strategy:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Set short expiration times for access tokens (15-30 minutes)&lt;/li&gt;&lt;li&gt;Use refresh tokens for obtaining new access tokens&lt;/li&gt;&lt;li&gt;Maintain a token blacklist for revoked tokens&lt;/li&gt;&lt;li&gt;Implement token rotation to regularly replace active tokens&lt;/li&gt;&lt;/ul&gt;&lt;h4 id=&quot;4-payload-security&quot;&gt;4. Payload Security&lt;/h4&gt;&lt;p&gt;Be mindful of what you include in your token payload:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Never store sensitive data like passwords or credit card numbers&lt;/li&gt;&lt;li&gt;Minimize the payload size to reduce overhead&lt;/li&gt;&lt;li&gt;Consider encrypting certain claims if they contain sensitive information&lt;/li&gt;&lt;/ul&gt;&lt;h3 id=&quot;advanced-security-measures&quot;&gt;Advanced Security Measures&lt;/h3&gt;&lt;p&gt;Going beyond the basics:&lt;/p&gt;&lt;h4 id=&quot;1-fingerprinting&quot;&gt;1. Fingerprinting&lt;/h4&gt;&lt;p&gt;Include device or context information in the token to detect potential token theft:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;fingerprint&quot;: &quot;device_id|user_agent|ip_hash&quot;,
  &quot;exp&quot;: 1516239022
}
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&quot;2-audience-validation&quot;&gt;2. Audience Validation&lt;/h4&gt;&lt;p&gt;Specify and validate the intended recipient of the token to prevent token reuse across different services:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &quot;aud&quot;: &quot;&lt;a href=&quot;https://api.myservice.com&quot; rel=&quot;nofollow&quot;&gt;https://api.myservice.com&lt;/a&gt;&quot;,
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;exp&quot;: 1516239022
}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;part-4-the-future-of-jwt-security&quot;&gt;Part 4: The Future of JWT Security&lt;/h2&gt;&lt;h3 id=&quot;emerging-security-enhancements&quot;&gt;Emerging Security Enhancements&lt;/h3&gt;&lt;p&gt;The security landscape is constantly evolving, and JWT security is no exception. Here are some promising developments:&lt;/p&gt;&lt;h4 id=&quot;1-quantum-resistant-algorithms&quot;&gt;1. Quantum-Resistant Algorithms&lt;/h4&gt;&lt;p&gt;As quantum computing advances, new signing algorithms resistant to quantum attacks are being developed. These will ensure JWTs remain secure even in a post-quantum world.&lt;/p&gt;&lt;h4 id=&quot;2-dynamic-validation&quot;&gt;2. Dynamic Validation&lt;/h4&gt;&lt;p&gt;Future implementations might include real-time validation systems that can instantly revoke tokens based on security events or user behavior changes.&lt;/p&gt;&lt;h4 id=&quot;3-enhanced-standards&quot;&gt;3. Enhanced Standards&lt;/h4&gt;&lt;p&gt;The JWT standard continues to evolve with new security features and best practices. Stay updated with the latest JWT specifications and security recommendations from the IETF.&lt;/p&gt;&lt;h2&gt;Conclusion&lt;/h2&gt;&lt;p&gt;JWTs represent a powerful solution for modern web authentication and authorization, but their security requires careful consideration and implementation. By understanding both the fundamentals and security challenges, and by implementing robust security measures, you can harness the benefits of JWTs while maintaining strong security.&lt;/p&gt;&lt;p&gt;Learn on JWT, OAuth, OIDC, and SAML: &lt;a rel=&quot;nofollow&quot; href=&quot;https://guptadeepak.com/demystifying-jwt-oauth-oidc-and-saml-a-technical-guide/&quot;&gt;https://guptadeepak.com/demystifying-jwt-oauth-oidc-and-saml-a-technical-guide/&lt;/a&gt;&lt;/p&gt;&lt;hr&gt;&lt;h3 id=&quot;quick-reference-jwt-security-checklist&quot;&gt;Quick Reference: JWT Security Checklist&lt;/h3&gt;&lt;p&gt;&amp;#10003; Use strong, unique signing keys&lt;br&gt;&amp;#10003; Implement proper algorithm validation&lt;br&gt;&amp;#10003; Store tokens securely (HttpOnly cookies)&lt;br&gt;&amp;#10003; Set appropriate token expiration&lt;br&gt;&amp;#10003; Implement token rotation&lt;br&gt;&amp;#10003; Validate token claims thoroughly&lt;br&gt;&amp;#10003; Monitor for security updates&lt;br&gt;&amp;#10003; Use HTTPS exclusively&lt;br&gt;&amp;#10003; Implement proper error handling&lt;br&gt;&amp;#10003; Regular security audits&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/1280/understanding-jwt-from-basics-to-advanced-security</guid>
<pubDate>Fri, 28 Feb 2025 19:34:07 +0000</pubDate>
</item>
</channel>
</rss>