सामग्री पर जाएं
·29 मिनट पढ़ने का समय

2026 में Modern Authentication: JWT, Sessions, OAuth, और Passkeys

पूरा authentication landscape: sessions vs JWT कब इस्तेमाल करें, OAuth 2.0 / OIDC flows, refresh token rotation, passkeys (WebAuthn), और Next.js auth patterns जो मैं वास्तव में इस्तेमाल करता हूं।

साझा करें:X / TwitterLinkedIn

Authentication web development का वो एक area है जहां "काम करता है" कभी enough नहीं होता। आपके date picker में bug annoying है। आपके auth system में bug data breach है।

मैंने authentication scratch से implement किया है, providers के बीच migrate किया है, token theft incidents debug किए हैं, और "security बाद में fix करेंगे" decisions का fallout झेला है। यह post वो comprehensive guide है जो मेरे पास शुरू में होनी चाहिए थी। सिर्फ theory नहीं — actual trade-offs, real vulnerabilities, और patterns जो production pressure में टिकते हैं।

हम पूरा landscape cover करेंगे: sessions, JWTs, OAuth 2.0, passkeys, MFA, और authorization। अंत तक, आप समझेंगे कि हर mechanism कैसे काम करता है, कब इस्तेमाल करना है और क्यों alternatives exist करते हैं।

Sessions vs JWT: असली Trade-offs#

यह पहला decision है जो आपके सामने आएगा, और internet बुरी advice से भरा है। मैं बताता हूं कि वास्तव में क्या matter करता है।

Session-Based Authentication#

Sessions original approach हैं। Server एक session record बनाता है, इसे कहीं store करता है (database, Redis, memory), और client को cookie में एक opaque session ID देता है।

typescript
// Simplified session creation
import { randomBytes } from "crypto";
import { cookies } from "next/headers";
 
interface Session {
  userId: string;
  createdAt: Date;
  expiresAt: Date;
  ipAddress: string;
  userAgent: string;
}
 
async function createSession(userId: string, request: Request): Promise<string> {
  const sessionId = randomBytes(32).toString("hex");
 
  const session: Session = {
    userId,
    createdAt: new Date(),
    expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
    ipAddress: request.headers.get("x-forwarded-for") ?? "unknown",
    userAgent: request.headers.get("user-agent") ?? "unknown",
  };
 
  // अपने database या Redis में store करें
  await redis.set(`session:${sessionId}`, JSON.stringify(session), "EX", 86400);
 
  const cookieStore = await cookies();
  cookieStore.set("session_id", sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 86400,
    path: "/",
  });
 
  return sessionId;
}

फायदे real हैं:

  • Instant revocation। Session record delete करो और user immediately logged out। Expiration का wait नहीं। जब आप suspicious activity detect करें तब यह matter करता है।
  • Session visibility। आप users को उनकी active sessions दिखा सकते हैं ("Chrome पर logged in, Windows 11, Istanbul") और उन्हें individual sessions revoke करने दे सकते हैं।
  • छोटा cookie size। Session ID typically 64 characters होता है। Cookie कभी नहीं बढ़ता।
  • Server-side control। आप session data update कर सकते हैं (user को admin promote करो, permissions बदलो) और यह next request पर effect लेता है।

नुकसान भी real हैं:

  • हर request पर database hit। हर authenticated request को session lookup चाहिए। Redis के साथ यह sub-millisecond है, लेकिन फिर भी एक dependency है।
  • Horizontal scaling के लिए shared storage चाहिए। अगर आपके multiple servers हैं, तो सबको same session store access करना होगा। Sticky sessions एक fragile workaround है।
  • CSRF एक concern है। क्योंकि cookies automatically send होती हैं, आपको CSRF protection चाहिए। SameSite cookies largely यह solve करती हैं, लेकिन आपको समझना चाहिए क्यों।

JWT-Based Authentication#

JWTs model flip करते हैं। Server पर session state store करने की बजाय, आप इसे एक signed token में encode करते हैं जो client के पास रहता है।

typescript
import { SignJWT, jwtVerify } from "jose";
 
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
 
async function createAccessToken(userId: string, role: string): Promise<string> {
  return new SignJWT({ sub: userId, role })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("15m")
    .setIssuer("https://akousa.net")
    .setAudience("https://akousa.net")
    .sign(secret);
}
 
async function verifyAccessToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret, {
      issuer: "https://akousa.net",
      audience: "https://akousa.net",
    });
    return payload;
  } catch {
    return null;
  }
}

फायदे:

  • कोई server-side storage नहीं। Token self-contained है। आप signature verify करते हैं और claims पढ़ते हैं। कोई database hit नहीं।
  • Services के across काम करता है। Microservices architecture में, public key वाली कोई भी service token verify कर सकती है। कोई shared session store नहीं चाहिए।
  • Stateless scaling। Session affinity की चिंता किए बिना ज़्यादा servers add करो।

नुकसान — और ये वो हैं जिन्हें लोग gloss over करते हैं:

  • आप JWT revoke नहीं कर सकते। एक बार issue होने के बाद, यह expire होने तक valid रहता है। अगर user का account compromise हो, तो आप force-logout नहीं कर सकते। आप blocklist बना सकते हैं, लेकिन तब आपने server-side state reintroduce कर दी और main advantage खो दिया।
  • Token size। कुछ claims वाले JWTs typically 800+ bytes होते हैं। Roles, permissions, और metadata जोड़ो तो हर request पर kilobytes send हो रहे हैं।
  • Payload readable है। Payload Base64-encoded है, encrypted नहीं। कोई भी decode कर सकता है। JWT में कभी sensitive data मत रखो।
  • Clock skew issues। अगर आपके servers में अलग-अलग clocks हैं (ऐसा होता है), तो expiration checks unreliable हो जाते हैं।

कब कौन सा इस्तेमाल करें#

मेरा rule of thumb:

Sessions इस्तेमाल करो जब: आपके पास monolithic application है, आपको instant revocation चाहिए, आप consumer-facing product बना रहे हैं जहां account security critical है, या आपकी auth requirements बार-बार बदल सकती हैं।

JWTs इस्तेमाल करो जब: आपके पास microservices architecture है जहां services को independently identity verify करनी है, आप API-to-API communication build कर रहे हैं, या आप third-party authentication system implement कर रहे हैं।

Practice में: ज़्यादातर applications को sessions इस्तेमाल करनी चाहिए। "JWTs ज़्यादा scalable हैं" argument तभी apply होता है जब आपके पास actually scaling problem है जो session storage solve नहीं कर सकता — और Redis per second millions session lookups handle करता है। मैंने बहुत से projects देखे हैं जो JWTs choose करते हैं क्योंकि ये ज़्यादा modern sound करते हैं, फिर एक blocklist और refresh token system build करते हैं जो sessions से ज़्यादा complex होता।

JWT Deep Dive#

चाहे आप session-based auth choose करें, आप OAuth, OIDC, और third-party integrations के ज़रिए JWTs से मिलेंगे। Internals समझना non-negotiable है।

JWT की Anatomy#

JWT के तीन parts dots से separate होते हैं: header.payload.signature

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcwOTMxMjAwMCwiZXhwIjoxNzA5MzEyOTAwfQ.
kQ8s7nR2xC...

Header — algorithm और token type declare करता है:

json
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload — claims contain करता है। Standard claims के short names होते हैं:

json
{
  "sub": "user_123",       // Subject (यह किसके बारे में है)
  "iss": "https://auth.example.com",  // Issuer (किसने बनाया)
  "aud": "https://api.example.com",   // Audience (किसे accept करना चाहिए)
  "iat": 1709312000,       // Issued At (Unix timestamp)
  "exp": 1709312900,       // Expiration (Unix timestamp)
  "role": "admin"          // Custom claim
}

Signature — prove करता है कि token में tamper नहीं हुआ। Encoded header और payload को secret key से sign करके बनाया जाता है।

RS256 vs HS256: यह वास्तव में Matter करता है#

HS256 (HMAC-SHA256) — symmetric। वही secret sign और verify करता है। Simple, लेकिन tokens verify करने वाली हर service के पास secret होना चाहिए। अगर उनमें से कोई भी compromise हो, तो attacker tokens forge कर सकता है।

RS256 (RSA-SHA256) — asymmetric। Private key sign करती है, public key verify करती है। सिर्फ auth server को private key चाहिए। कोई भी service public key से verify कर सकती है। अगर verification service compromise हो, तो attacker tokens पढ़ सकता है लेकिन forge नहीं कर सकता।

typescript
import { SignJWT, jwtVerify, importPKCS8, importSPKI } from "jose";
 
// RS256 — जब multiple services tokens verify करें तो इसे इस्तेमाल करो
const privateKeyPem = process.env.JWT_PRIVATE_KEY!;
const publicKeyPem = process.env.JWT_PUBLIC_KEY!;
 
async function signWithRS256(payload: Record<string, unknown>) {
  const privateKey = await importPKCS8(privateKeyPem, "RS256");
 
  return new SignJWT(payload)
    .setProtectedHeader({ alg: "RS256", typ: "JWT" })
    .setIssuedAt()
    .setExpirationTime("15m")
    .sign(privateKey);
}
 
async function verifyWithRS256(token: string) {
  const publicKey = await importSPKI(publicKeyPem, "RS256");
 
  const { payload } = await jwtVerify(token, publicKey, {
    algorithms: ["RS256"], // CRITICAL: हमेशा algorithms restrict करो
  });
 
  return payload;
}

नियम: जब भी tokens service boundaries cross करें RS256 इस्तेमाल करो। HS256 सिर्फ तब इस्तेमाल करो जब same service sign और verify दोनों करे।

alg: none Attack#

यह सबसे famous JWT vulnerability है, और यह शर्मनाक रूप से simple है। कुछ JWT libraries पहले:

  1. Header से alg field पढ़ती थीं
  2. जो भी algorithm specified हो वो इस्तेमाल करती थीं
  3. अगर alg: "none" हो, तो signature verification पूरी तरह skip कर देती थीं

Attacker एक valid JWT ले सकता था, payload बदल सकता था (जैसे "role": "admin" set करना), alg को "none" set कर सकता था, signature हटा सकता था, और भेज सकता था। Server accept कर लेता।

typescript
// VULNERABLE — कभी ऐसा मत करो
function verifyJwt(token: string) {
  const [headerB64, payloadB64, signature] = token.split(".");
  const header = JSON.parse(atob(headerB64));
 
  if (header.alg === "none") {
    // "कोई signature नहीं चाहिए" — CATASTROPHIC
    return JSON.parse(atob(payloadB64));
  }
 
  // ... signature verify करो
}

Fix simple है: हमेशा expected algorithm explicitly specify करो। Token को कभी यह मत बताने दो कि खुद को कैसे verify करना है।

typescript
// SAFE — algorithm hardcoded है, token से नहीं पढ़ा
const { payload } = await jwtVerify(token, key, {
  algorithms: ["RS256"], // सिर्फ RS256 accept करो — header ignore करो
});

Modern libraries जैसे jose यह default रूप से correctly handle करती हैं, लेकिन फिर भी defense in depth के लिए explicitly algorithms option pass करनी चाहिए।

Algorithm Confusion Attack#

ऊपर वाले से related: अगर server RS256 accept करने के लिए configured है, तो attacker:

  1. Server की public key ले सकता है (यह public है, आखिरकार)
  2. alg: "HS256" के साथ token बना सकता है
  3. Public key को HMAC secret के रूप में इस्तेमाल करके sign कर सकता है

अगर server alg header पढ़ता है और HS256 verification पर switch करता है, तो public key (जो सबको पता है) shared secret बन जाती है। Signature valid है। Attacker ने token forge कर लिया।

फिर से, fix वही है: token header से algorithm पर कभी trust मत करो। हमेशा hardcode करो।

Refresh Token Rotation#

अगर आप JWTs इस्तेमाल करते हैं, तो आपको refresh token strategy चाहिए। Long-lived access token भेजना मुसीबत मांगना है — अगर चोरी हो जाए, तो attacker के पास पूरी lifetime तक access है।

Pattern:

  • Access token: short-lived (15 minutes)। API requests के लिए इस्तेमाल होता है।
  • Refresh token: long-lived (30 days)। सिर्फ नया access token लेने के लिए इस्तेमाल होता है।
typescript
import { randomBytes } from "crypto";
 
interface RefreshTokenRecord {
  tokenHash: string;
  userId: string;
  familyId: string;  // Related tokens को साथ group करता है
  used: boolean;
  expiresAt: Date;
  createdAt: Date;
}
 
async function issueTokenPair(userId: string) {
  const familyId = randomBytes(16).toString("hex");
 
  const accessToken = await createAccessToken(userId);
  const refreshToken = randomBytes(64).toString("hex");
  const refreshTokenHash = await hashToken(refreshToken);
 
  // Refresh token record store करो
  await db.refreshToken.create({
    data: {
      tokenHash: refreshTokenHash,
      userId,
      familyId,
      used: false,
      expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
      createdAt: new Date(),
    },
  });
 
  return { accessToken, refreshToken };
}

हर Use पर Rotation#

हर बार जब client refresh token इस्तेमाल करके new access token लेता है, आप नया refresh token issue करते हैं और पुराना invalidate करते हैं:

typescript
async function rotateTokens(incomingRefreshToken: string) {
  const tokenHash = await hashToken(incomingRefreshToken);
  const record = await db.refreshToken.findUnique({
    where: { tokenHash },
  });
 
  if (!record) {
    // Token exist नहीं करता — possible theft
    return null;
  }
 
  if (record.expiresAt < new Date()) {
    // Token expired
    await db.refreshToken.delete({ where: { tokenHash } });
    return null;
  }
 
  if (record.used) {
    // यह TOKEN पहले ही USE HO CHUKA।
    // कोई इसे replay कर रहा है — या legitimate user
    // या attacker। किसी भी तरह, पूरी family kill करो।
    await db.refreshToken.deleteMany({
      where: { familyId: record.familyId },
    });
 
    console.error(
      `Refresh token reuse detected for user ${record.userId}, family ${record.familyId}. All tokens in family invalidated.`
    );
 
    return null;
  }
 
  // Current token को used mark करो (delete मत करो — reuse detection के लिए चाहिए)
  await db.refreshToken.update({
    where: { tokenHash },
    data: { used: true },
  });
 
  // Same family ID के साथ new pair issue करो
  const newRefreshToken = randomBytes(64).toString("hex");
  const newRefreshTokenHash = await hashToken(newRefreshToken);
 
  await db.refreshToken.create({
    data: {
      tokenHash: newRefreshTokenHash,
      userId: record.userId,
      familyId: record.familyId,  // Same family
      used: false,
      expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
      createdAt: new Date(),
    },
  });
 
  const newAccessToken = await createAccessToken(record.userId);
 
  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

Family Invalidation क्यों Matter करती है#

इस scenario पर विचार करें:

  1. User login करता है, refresh token A मिलता है
  2. Attacker refresh token A चुरा लेता है
  3. Attacker A इस्तेमाल करके new pair लेता है (access token + refresh token B)
  4. User A (जो उसके पास अभी भी है) इस्तेमाल करके refresh करने की कोशिश करता है

Reuse detection के बिना, user को बस error मिलता है। Attacker token B के साथ continue करता है। User फिर से login करता है, कभी नहीं जानता कि उसका account compromise हुआ था।

Reuse detection और family invalidation के साथ: जब user पहले से used token A इस्तेमाल करने की कोशिश करता है, system reuse detect करता है, family में हर token invalidate करता है (B सहित), और user और attacker दोनों को re-authenticate करने पर मजबूर करता है। User को "कृपया फिर से login करें" prompt मिलता है और शायद समझ जाए कि कुछ गलत है।

यह Auth0, Okta, और Auth.js का approach है। यह perfect नहीं — अगर attacker legitimate user से पहले token इस्तेमाल करता है, तो legitimate user वो बन जाता है जो reuse alert trigger करता है। लेकिन bearer tokens के साथ हम सबसे अच्छा यही कर सकते हैं।

OAuth 2.0 और OIDC#

OAuth 2.0 और OpenID Connect "Sign in with Google/GitHub/Apple" के पीछे के protocols हैं। इन्हें समझना essential है चाहे आप library इस्तेमाल करें, क्योंकि जब चीज़ें break होंगी — और होंगी — आपको protocol level पर जानना चाहिए कि क्या हो रहा है।

Key अंतर#

OAuth 2.0 एक authorization protocol है। यह जवाब देता है: "क्या यह application इस user का data access कर सकता है?" Result एक access token है जो specific permissions (scopes) grant करता है।

OpenID Connect (OIDC) OAuth 2.0 के ऊपर बना authentication layer है। यह जवाब देता है: "यह user कौन है?" Result एक ID token (एक JWT) है जिसमें user identity information होती है।

जब आप "Sign in with Google" करते हैं, आप OIDC इस्तेमाल कर रहे होते हैं। Google आपकी app को बताता है कि user कौन है (authentication)। आप OAuth scopes भी request कर सकते हैं उनके calendar या drive access करने के लिए (authorization)।

PKCE के साथ Authorization Code Flow#

यह वो flow है जो आपको web applications के लिए इस्तेमाल करना चाहिए। PKCE (Proof Key for Code Exchange) originally mobile apps के लिए design किया गया था लेकिन अब सभी clients के लिए recommended है, server-side applications सहित।

typescript
import { randomBytes, createHash } from "crypto";
 
// Step 1: PKCE values generate करो और user को redirect करो
function initiateOAuthFlow() {
  // Code verifier: random 43-128 character string
  const codeVerifier = randomBytes(32)
    .toString("base64url")
    .slice(0, 43);
 
  // Code challenge: verifier का SHA256 hash, base64url-encoded
  const codeChallenge = createHash("sha256")
    .update(codeVerifier)
    .digest("base64url");
 
  // State: CSRF protection के लिए random value
  const state = randomBytes(16).toString("hex");
 
  // Redirect करने से पहले दोनों session में store करो (server-side!)
  // Code_verifier कभी cookie या URL parameter में मत रखो
  session.codeVerifier = codeVerifier;
  session.oauthState = state;
 
  const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
  authUrl.searchParams.set("client_id", process.env.GOOGLE_CLIENT_ID!);
  authUrl.searchParams.set("redirect_uri", "https://example.com/api/auth/callback/google");
  authUrl.searchParams.set("response_type", "code");
  authUrl.searchParams.set("scope", "openid email profile");
  authUrl.searchParams.set("state", state);
  authUrl.searchParams.set("code_challenge", codeChallenge);
  authUrl.searchParams.set("code_challenge_method", "S256");
 
  return authUrl.toString();
}
typescript
// Step 2: Callback handle करो
async function handleOAuthCallback(request: Request) {
  const url = new URL(request.url);
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");
  const error = url.searchParams.get("error");
 
  // Provider से errors check करो
  if (error) {
    throw new Error(`OAuth error: ${error}`);
  }
 
  // Verify करो कि state match करता है (CSRF protection)
  if (state !== session.oauthState) {
    throw new Error("State mismatch — possible CSRF attack");
  }
 
  // Authorization code को tokens के लिए exchange करो
  const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: code!,
      redirect_uri: "https://example.com/api/auth/callback/google",
      client_id: process.env.GOOGLE_CLIENT_ID!,
      client_secret: process.env.GOOGLE_CLIENT_SECRET!,
      code_verifier: session.codeVerifier, // PKCE: prove करता है कि हमने यह flow शुरू किया
    }),
  });
 
  const tokens = await tokenResponse.json();
  // tokens.access_token — Google को API calls के लिए
  // tokens.id_token — user identity वाला JWT (OIDC)
  // tokens.refresh_token — new access tokens लेने के लिए
 
  // Step 3: ID token verify करो और user info extract करो
  const idTokenPayload = await verifyGoogleIdToken(tokens.id_token);
 
  return {
    googleId: idTokenPayload.sub,
    email: idTokenPayload.email,
    name: idTokenPayload.name,
    picture: idTokenPayload.picture,
  };
}

तीन Endpoints#

हर OAuth/OIDC provider ये expose करता है:

  1. Authorization endpoint — जहां आप user को login और permissions grant करने के लिए redirect करते हैं। Authorization code return करता है।
  2. Token endpoint — जहां आपका server authorization code को access/refresh/ID tokens के लिए exchange करता है। यह server-to-server call है।
  3. UserInfo endpoint — जहां आप access token इस्तेमाल करके additional user profile data fetch कर सकते हैं। OIDC के साथ, इसका ज़्यादातर हिस्सा ID token में पहले से होता है।

State Parameter#

state parameter OAuth callback पर CSRF attacks prevent करता है। इसके बिना:

  1. Attacker अपनी machine पर OAuth flow शुरू करता है, authorization code मिलता है
  2. Attacker एक URL craft करता है: https://yourapp.com/callback?code=ATTACKER_CODE
  3. Attacker victim को इस पर click कराता है (email link, hidden image)
  4. आपकी app attacker का code exchange करती है और attacker का Google account victim की session से link कर देती है

state के साथ: आपकी app एक random value generate करती है, session में store करती है, और authorization URL में include करती है। जब callback आता है, आप verify करते हैं कि state match करता है। Attacker यह forge नहीं कर सकता क्योंकि उसके पास victim की session का access नहीं है।

Next.js App Router के साथ Auth.js (NextAuth)#

Auth.js ज़्यादातर Next.js projects में मेरी पहली पसंद है। यह OAuth dance, session management, database persistence, और CSRF protection handle करता है। यहां एक production-ready setup है।

Basic Configuration#

typescript
// src/lib/auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import { verifyPassword } from "@/lib/password";
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
 
  // Better security के लिए database sessions (JWT नहीं) इस्तेमाल करो
  session: {
    strategy: "database",
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60,   // हर 24 घंटे session extend करो
  },
 
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      // Specific scopes request करो
      authorization: {
        params: {
          scope: "openid email profile",
          prompt: "consent",
          access_type: "offline", // Refresh token लो
        },
      },
    }),
 
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
 
    // Email/password login (carefully इस्तेमाल करो)
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      authorize: async (credentials) => {
        if (!credentials?.email || !credentials?.password) {
          return null;
        }
 
        const user = await prisma.user.findUnique({
          where: { email: credentials.email as string },
        });
 
        if (!user || !user.passwordHash) {
          return null;
        }
 
        const isValid = await verifyPassword(
          credentials.password as string,
          user.passwordHash
        );
 
        if (!isValid) {
          return null;
        }
 
        return {
          id: user.id,
          email: user.email,
          name: user.name,
          image: user.image,
        };
      },
    }),
  ],
 
  callbacks: {
    // Control करो कि कौन sign in कर सकता है
    async signIn({ user, account }) {
      // Banned users के लिए sign-in block करो
      if (user.id) {
        const dbUser = await prisma.user.findUnique({
          where: { id: user.id },
          select: { banned: true },
        });
        if (dbUser?.banned) return false;
      }
      return true;
    },
 
    // Session में custom fields जोड़ो
    async session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
        // Database से role fetch करो
        const dbUser = await prisma.user.findUnique({
          where: { id: user.id },
          select: { role: true },
        });
        session.user.role = dbUser?.role ?? "user";
      }
      return session;
    },
  },
 
  pages: {
    signIn: "/login",
    error: "/auth/error",
    verifyRequest: "/auth/verify",
  },
});

Route Handler#

typescript
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
 
export const { GET, POST } = handlers;

Middleware Protection#

typescript
// src/middleware.ts
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
 
export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isAuthPage = req.nextUrl.pathname.startsWith("/login")
    || req.nextUrl.pathname.startsWith("/register");
  const isProtectedRoute = req.nextUrl.pathname.startsWith("/dashboard")
    || req.nextUrl.pathname.startsWith("/settings")
    || req.nextUrl.pathname.startsWith("/admin");
  const isAdminRoute = req.nextUrl.pathname.startsWith("/admin");
 
  // Logged-in users को auth pages से दूर redirect करो
  if (isLoggedIn && isAuthPage) {
    return NextResponse.redirect(new URL("/dashboard", req.nextUrl));
  }
 
  // Unauthenticated users को login पर redirect करो
  if (!isLoggedIn && isProtectedRoute) {
    const callbackUrl = encodeURIComponent(req.nextUrl.pathname);
    return NextResponse.redirect(
      new URL(`/login?callbackUrl=${callbackUrl}`, req.nextUrl)
    );
  }
 
  // Admin role check करो
  if (isAdminRoute && req.auth?.user?.role !== "admin") {
    return NextResponse.redirect(new URL("/dashboard", req.nextUrl));
  }
 
  return NextResponse.next();
});
 
export const config = {
  matcher: [
    "/dashboard/:path*",
    "/settings/:path*",
    "/admin/:path*",
    "/login",
    "/register",
  ],
};

Server Components में Session इस्तेमाल करना#

typescript
// src/app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const session = await auth();
 
  if (!session?.user) {
    redirect("/login");
  }
 
  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p>Role: {session.user.role}</p>
    </div>
  );
}

Client Components में Session इस्तेमाल करना#

typescript
"use client";
 
import { useSession } from "next-auth/react";
 
export function UserMenu() {
  const { data: session, status } = useSession();
 
  if (status === "loading") {
    return <div>Loading...</div>;
  }
 
  if (status === "unauthenticated") {
    return <a href="/login">Sign In</a>;
  }
 
  return (
    <div>
      <img
        src={session?.user?.image ?? "/default-avatar.png"}
        alt={session?.user?.name ?? "User"}
      />
      <span>{session?.user?.name}</span>
    </div>
  );
}

Passkeys (WebAuthn)#

Passkeys सालों में सबसे significant authentication improvement हैं। ये phishing-resistant, replay-resistant हैं, और password-related vulnerabilities की पूरी category eliminate कर देती हैं। अगर आप 2026 में नया project शुरू कर रहे हैं, तो आपको passkeys support करनी चाहिए।

Passkeys कैसे काम करती हैं#

Passkeys public-key cryptography इस्तेमाल करती हैं, biometrics या device PINs से backed:

  1. Registration: Browser एक key pair generate करता है। Private key device पर रहती है (secure enclave में, biometrics से protected)। Public key आपके server को भेजी जाती है।
  2. Authentication: Server एक challenge (random bytes) भेजता है। Device challenge को private key से sign करता है (biometric verification के बाद)। Server stored public key से signature verify करता है।

कोई shared secret कभी network cross नहीं करता। कुछ phish करने को नहीं, कुछ leak होने को नहीं, कुछ stuff करने को नहीं।

Passkeys Phishing-Resistant क्यों हैं#

जब passkey बनाई जाती है, यह origin (जैसे https://example.com) से bound होती है। Browser passkey को सिर्फ उसी exact origin पर इस्तेमाल करेगा जिस पर बनाई गई थी। अगर attacker https://exarnple.com पर lookalike site बनाता है, तो passkey simply offer ही नहीं होगी। यह browser enforce करता है, user vigilance नहीं।

यह passwords से fundamentally अलग है, जहां users routinely phishing sites पर अपने credentials enter करते हैं क्योंकि page सही दिखता है।

SimpleWebAuthn के साथ Implementation#

SimpleWebAuthn वो library है जो मैं recommend करता हूं। यह WebAuthn protocol correctly handle करती है और अच्छी TypeScript types हैं।

typescript
// Server-side: Registration
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from "@simplewebauthn/server";
import type {
  GenerateRegistrationOptionsOpts,
  VerifiedRegistrationResponse,
} from "@simplewebauthn/server";
 
const rpName = "akousa.net";
const rpID = "akousa.net";
const origin = "https://akousa.net";
 
async function startRegistration(userId: string, userEmail: string) {
  // User की existing passkeys exclude करने के लिए लो
  const existingCredentials = await db.credential.findMany({
    where: { userId },
    select: { credentialId: true, transports: true },
  });
 
  const options: GenerateRegistrationOptionsOpts = {
    rpName,
    rpID,
    userID: new TextEncoder().encode(userId),
    userName: userEmail,
    attestationType: "none", // ज़्यादातर apps को attestation नहीं चाहिए
    excludeCredentials: existingCredentials.map((cred) => ({
      id: cred.credentialId,
      transports: cred.transports,
    })),
    authenticatorSelection: {
      residentKey: "preferred",
      userVerification: "preferred",
    },
  };
 
  const registrationOptions = await generateRegistrationOptions(options);
 
  // Challenge temporarily store करो — verification के लिए चाहिए
  await redis.set(
    `webauthn:challenge:${userId}`,
    registrationOptions.challenge,
    "EX",
    300 // 5 minute expiry
  );
 
  return registrationOptions;
}
 
async function finishRegistration(userId: string, response: unknown) {
  const expectedChallenge = await redis.get(`webauthn:challenge:${userId}`);
 
  if (!expectedChallenge) {
    throw new Error("Challenge expired or not found");
  }
 
  let verification: VerifiedRegistrationResponse;
  try {
    verification = await verifyRegistrationResponse({
      response: response as any,
      expectedChallenge,
      expectedOrigin: origin,
      expectedRPID: rpID,
    });
  } catch (error) {
    throw new Error(`Registration verification failed: ${error}`);
  }
 
  if (!verification.verified || !verification.registrationInfo) {
    throw new Error("Registration verification failed");
  }
 
  const { credential } = verification.registrationInfo;
 
  // Credential database में store करो
  await db.credential.create({
    data: {
      userId,
      credentialId: credential.id,
      publicKey: Buffer.from(credential.publicKey),
      counter: credential.counter,
      transports: credential.transports ?? [],
    },
  });
 
  // Clean up
  await redis.del(`webauthn:challenge:${userId}`);
 
  return { verified: true };
}
typescript
// Server-side: Authentication
import {
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from "@simplewebauthn/server";
 
async function startAuthentication(userId?: string) {
  let allowCredentials;
 
  // अगर हम user को जानते हैं (जैसे उसने email enter किया), उसकी passkeys तक limit करो
  if (userId) {
    const credentials = await db.credential.findMany({
      where: { userId },
      select: { credentialId: true, transports: true },
    });
    allowCredentials = credentials.map((cred) => ({
      id: cred.credentialId,
      transports: cred.transports,
    }));
  }
 
  const options = await generateAuthenticationOptions({
    rpID,
    allowCredentials,
    userVerification: "preferred",
  });
 
  // Verification के लिए challenge store करो
  const challengeKey = userId
    ? `webauthn:auth:${userId}`
    : `webauthn:auth:${options.challenge}`;
 
  await redis.set(challengeKey, options.challenge, "EX", 300);
 
  return options;
}
 
async function finishAuthentication(
  response: any,
  expectedChallenge: string,
  userId: string
) {
  const credential = await db.credential.findUnique({
    where: { credentialId: response.id },
  });
 
  if (!credential) {
    throw new Error("Credential not found");
  }
 
  const verification = await verifyAuthenticationResponse({
    response,
    expectedChallenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
    credential: {
      id: credential.credentialId,
      publicKey: credential.publicKey,
      counter: credential.counter,
      transports: credential.transports,
    },
  });
 
  if (!verification.verified) {
    throw new Error("Authentication verification failed");
  }
 
  // IMPORTANT: Replay attacks prevent करने के लिए counter update करो
  await db.credential.update({
    where: { credentialId: response.id },
    data: {
      counter: verification.authenticationInfo.newCounter,
    },
  });
 
  return { verified: true, userId: credential.userId };
}
typescript
// Client-side: Registration
import { startRegistration as webAuthnRegister } from "@simplewebauthn/browser";
 
async function registerPasskey() {
  // अपने server से options लो
  const optionsResponse = await fetch("/api/auth/webauthn/register", {
    method: "POST",
  });
  const options = await optionsResponse.json();
 
  try {
    // यह browser की passkey UI trigger करता है (biometric prompt)
    const credential = await webAuthnRegister(options);
 
    // Verification के लिए credential अपने server को भेजो
    const verifyResponse = await fetch("/api/auth/webauthn/register/verify", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(credential),
    });
 
    const result = await verifyResponse.json();
    if (result.verified) {
      console.log("Passkey successfully register हो गई!");
    }
  } catch (error) {
    if ((error as Error).name === "NotAllowedError") {
      console.log("User ने passkey registration cancel किया");
    }
  }
}

Attestation vs Assertion#

दो terms जो आप देखेंगे:

  • Attestation (registration): नई credential बनाने का process। Authenticator अपनी identity और capabilities "attest" करता है। ज़्यादातर applications के लिए, आपको attestation verify करने की ज़रूरत नहीं — attestationType: "none" set करो।
  • Assertion (authentication): Existing credential इस्तेमाल करके challenge sign करने का process। Authenticator "assert" करता है कि user वो है जो claim करता है।

MFA Implementation#

Passkeys के साथ भी, आपको ऐसे scenarios मिलेंगे जहां TOTP से MFA चाहिए — passwords के साथ second factor के रूप में passkeys, या उन users को support करना जिनके devices passkeys support नहीं करते।

TOTP (Time-Based One-Time Passwords)#

TOTP Google Authenticator, Authy, और 1Password के पीछे का protocol है। यह इस तरह काम करता है:

  1. Server एक random secret generate करता है (base32-encoded)
  2. User secret contain करने वाला QR code scan करता है
  3. Server और authenticator app दोनों secret और current time से same 6-digit code compute करते हैं
  4. Codes हर 30 seconds बदलते हैं
typescript
import { createHmac, randomBytes } from "crypto";
 
// User के लिए TOTP secret generate करो
function generateTOTPSecret(): string {
  const buffer = randomBytes(20);
  return base32Encode(buffer);
}
 
function base32Encode(buffer: Buffer): string {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  let result = "";
  let bits = 0;
  let value = 0;
 
  for (const byte of buffer) {
    value = (value << 8) | byte;
    bits += 8;
    while (bits >= 5) {
      result += alphabet[(value >>> (bits - 5)) & 0x1f];
      bits -= 5;
    }
  }
 
  if (bits > 0) {
    result += alphabet[(value << (5 - bits)) & 0x1f];
  }
 
  return result;
}
 
// QR code के लिए TOTP URI generate करो
function generateTOTPUri(
  secret: string,
  userEmail: string,
  issuer: string = "akousa.net"
): string {
  const encodedIssuer = encodeURIComponent(issuer);
  const encodedEmail = encodeURIComponent(userEmail);
  return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
}
typescript
// TOTP code verify करो
function verifyTOTP(secret: string, code: string, window: number = 1): boolean {
  const secretBuffer = base32Decode(secret);
  const now = Math.floor(Date.now() / 1000);
 
  // Current time step और adjacent ones check करो (clock drift tolerance)
  for (let i = -window; i <= window; i++) {
    const timeStep = Math.floor(now / 30) + i;
    const expectedCode = generateTOTPCode(secretBuffer, timeStep);
 
    // Timing attacks prevent करने के लिए constant-time comparison
    if (timingSafeEqual(code, expectedCode)) {
      return true;
    }
  }
 
  return false;
}
 
function generateTOTPCode(secret: Buffer, timeStep: number): string {
  // Time step को 8-byte big-endian buffer में convert करो
  const timeBuffer = Buffer.alloc(8);
  timeBuffer.writeBigInt64BE(BigInt(timeStep));
 
  // HMAC-SHA1
  const hmac = createHmac("sha1", secret).update(timeBuffer).digest();
 
  // Dynamic truncation
  const offset = hmac[hmac.length - 1] & 0x0f;
  const code =
    ((hmac[offset] & 0x7f) << 24) |
    ((hmac[offset + 1] & 0xff) << 16) |
    ((hmac[offset + 2] & 0xff) << 8) |
    (hmac[offset + 3] & 0xff);
 
  return (code % 1_000_000).toString().padStart(6, "0");
}
 
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  const bufA = Buffer.from(a);
  const bufB = Buffer.from(b);
  return createHmac("sha256", "key").update(bufA).digest()
    .equals(createHmac("sha256", "key").update(bufB).digest());
}

Backup Codes#

Users अपने phone खो देते हैं। MFA setup के दौरान हमेशा backup codes generate करो:

typescript
import { randomBytes, createHash } from "crypto";
 
function generateBackupCodes(count: number = 10): string[] {
  return Array.from({ length: count }, () =>
    randomBytes(4).toString("hex").toUpperCase() // 8-character hex codes
  );
}
 
async function storeBackupCodes(userId: string, codes: string[]) {
  // Store करने से पहले codes hash करो — passwords की तरह treat करो
  const hashedCodes = codes.map((code) =>
    createHash("sha256").update(code).digest("hex")
  );
 
  await db.backupCode.createMany({
    data: hashedCodes.map((hash) => ({
      userId,
      codeHash: hash,
      used: false,
    })),
  });
 
  // Plain codes एक बार user को save करने के लिए return करो
  // इसके बाद, हमारे पास सिर्फ hashes हैं
  return codes;
}
 
async function verifyBackupCode(userId: string, code: string): Promise<boolean> {
  const codeHash = createHash("sha256")
    .update(code.toUpperCase().replace(/\s/g, ""))
    .digest("hex");
 
  const backupCode = await db.backupCode.findFirst({
    where: {
      userId,
      codeHash,
      used: false,
    },
  });
 
  if (!backupCode) return false;
 
  // Used mark करो — हर backup code exactly एक बार काम करता है
  await db.backupCode.update({
    where: { id: backupCode.id },
    data: { used: true, usedAt: new Date() },
  });
 
  return true;
}

Recovery Flow#

MFA recovery वो part है जो ज़्यादातर tutorials skip करते हैं और ज़्यादातर real applications बिगाड़ते हैं। यहां मैं क्या implement करता हूं:

  1. Primary: Authenticator app से TOTP code
  2. Secondary: 10 backup codes में से एक
  3. Last resort: Email-based recovery 24-hour waiting period और user के दूसरे verified channels पर notification के साथ

Waiting period critical है। अगर attacker ने user का email compromise कर लिया है, तो आप उन्हें तुरंत MFA disable नहीं करने देना चाहते। 24-hour delay legitimate user को email notice करने और intervene करने का समय देता है।

typescript
async function initiateAccountRecovery(email: string) {
  const user = await db.user.findUnique({ where: { email } });
  if (!user) {
    // Reveal मत करो कि account exist करता है या नहीं
    return { message: "If that email exists, we've sent recovery instructions." };
  }
 
  const recoveryToken = randomBytes(32).toString("hex");
  const tokenHash = createHash("sha256").update(recoveryToken).digest("hex");
 
  await db.recoveryRequest.create({
    data: {
      userId: user.id,
      tokenHash,
      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
      status: "pending",
    },
  });
 
  // Recovery link के साथ email भेजो
  await sendEmail(email, {
    subject: "Account Recovery Request",
    body: `
      आपके account पर MFA disable करने का request आया है।
      अगर यह आप थे, तो 24 घंटे बाद नीचे दिए link पर click करें: ...
      अगर यह आप नहीं थे, तो कृपया तुरंत अपना password बदलें।
    `,
  });
 
  return { message: "If that email exists, we've sent recovery instructions." };
}

Authorization Patterns#

Authentication बताता है कौन हो। Authorization बताता है क्या करने की अनुमति है। इसे गलत करना वही है जिससे आप news में आते हैं।

RBAC vs ABAC#

RBAC (Role-Based Access Control): Users के roles होते हैं, roles के permissions होते हैं। Simple, reason करने में आसान, ज़्यादातर applications के लिए काम करता है।

typescript
// RBAC — straightforward role checks
type Role = "user" | "editor" | "admin" | "super_admin";
 
const ROLE_PERMISSIONS: Record<Role, string[]> = {
  user: ["read:own_profile", "update:own_profile", "read:posts"],
  editor: ["read:own_profile", "update:own_profile", "read:posts", "create:posts", "update:posts"],
  admin: [
    "read:own_profile", "update:own_profile",
    "read:posts", "create:posts", "update:posts", "delete:posts",
    "read:users", "update:users",
  ],
  super_admin: ["*"], // Wildcards के साथ careful रहो
};
 
function hasPermission(role: Role, permission: string): boolean {
  const permissions = ROLE_PERMISSIONS[role];
  return permissions.includes("*") || permissions.includes(permission);
}
 
// API route में usage
export async function DELETE(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const session = await auth();
  if (!session?.user) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  if (!hasPermission(session.user.role as Role, "delete:posts")) {
    return Response.json({ error: "Forbidden" }, { status: 403 });
  }
 
  const { id } = await params;
  await db.post.delete({ where: { id } });
  return Response.json({ success: true });
}

ABAC (Attribute-Based Access Control): Permissions user, resource, और context की attributes पर depend करती हैं। ज़्यादा flexible लेकिन ज़्यादा complex।

typescript
// ABAC — जब RBAC enough न हो
interface PolicyContext {
  user: {
    id: string;
    role: string;
    department: string;
    clearanceLevel: number;
  };
  resource: {
    type: string;
    ownerId: string;
    classification: string;
    department: string;
  };
  action: string;
  environment: {
    ipAddress: string;
    time: Date;
    mfaVerified: boolean;
  };
}
 
function evaluatePolicy(context: PolicyContext): boolean {
  const { user, resource, action, environment } = context;
 
  // Users हमेशा अपने resources पढ़ सकते हैं
  if (action === "read" && resource.ownerId === user.id) {
    return true;
  }
 
  // Admins अपने department में कोई भी resource पढ़ सकते हैं
  if (
    action === "read" &&
    user.role === "admin" &&
    user.department === resource.department
  ) {
    return true;
  }
 
  // Classified resources को MFA और minimum clearance चाहिए
  if (resource.classification === "confidential") {
    if (!environment.mfaVerified) return false;
    if (user.clearanceLevel < 3) return false;
  }
 
  // Business hours के बाहर destructive actions block
  if (action === "delete") {
    const hour = environment.time.getHours();
    if (hour < 9 || hour > 17) return false;
  }
 
  return false; // Default deny
}

"Boundary पर Check करो" नियम#

यह सबसे important authorization principle है: हर trust boundary पर permissions check करो, सिर्फ UI level पर नहीं।

typescript
// BAD — सिर्फ component में check कर रहे
function DeleteButton({ post }: { post: Post }) {
  const { data: session } = useSession();
 
  // यह button hide करता है, लेकिन deletion prevent नहीं करता
  if (session?.user?.role !== "admin") return null;
 
  return <button onClick={() => deletePost(post.id)}>Delete</button>;
}
 
// ALSO BAD — server action में check कर रहे लेकिन API route में नहीं
async function deletePostAction(postId: string) {
  const session = await auth();
  if (session?.user?.role !== "admin") throw new Error("Forbidden");
  await db.post.delete({ where: { id: postId } });
}
// Attacker अभी भी directly POST /api/posts/123 call कर सकता है
 
// GOOD — हर boundary पर check करो
// 1. UI में button hide करो (UX, security नहीं)
// 2. Server action में check करो (defense in depth)
// 3. API route में check करो (actual security boundary)
// 4. Optionally, middleware में check करो (route-level protection के लिए)

UI check user experience के लिए है। Server check security के लिए है। कभी सिर्फ एक पर rely मत करो।

Next.js Middleware में Permission Checks#

Middleware हर matched request से पहले चलता है। यह coarse-grained access control के लिए अच्छी जगह है:

typescript
// "क्या इस user को इस section में access करने की अनुमति है?"
// Fine-grained checks ("क्या यह user इस POST को edit कर सकता है?") route handler में belong करते हैं
// क्योंकि middleware के पास request body या route params का आसान access नहीं होता।
 
export default auth((req) => {
  const path = req.nextUrl.pathname;
  const role = req.auth?.user?.role;
 
  // Route-level access control
  const routeAccess: Record<string, Role[]> = {
    "/admin": ["admin", "super_admin"],
    "/editor": ["editor", "admin", "super_admin"],
    "/dashboard": ["user", "editor", "admin", "super_admin"],
  };
 
  for (const [route, allowedRoles] of Object.entries(routeAccess)) {
    if (path.startsWith(route)) {
      if (!role || !allowedRoles.includes(role as Role)) {
        return NextResponse.redirect(new URL("/unauthorized", req.nextUrl));
      }
    }
  }
 
  return NextResponse.next();
});

Common Vulnerabilities#

ये वो attacks हैं जो मैं real codebases में सबसे ज़्यादा देखता हूं। इन्हें समझना essential है।

Session Fixation#

Attack: Attacker आपकी site पर एक valid session बनाता है, फिर victim को वो session ID इस्तेमाल करने के लिए trick करता है (जैसे URL parameter से या subdomain के ज़रिए cookie set करके)। जब victim login करता है, attacker की session अब एक authenticated user के पास है।

Fix: Successful authentication के बाद हमेशा session ID regenerate करो। कभी pre-authentication session ID को post-authentication session में carry over मत होने दो।

typescript
async function login(credentials: { email: string; password: string }, request: Request) {
  const user = await verifyCredentials(credentials);
  if (!user) throw new Error("Invalid credentials");
 
  // CRITICAL: पुरानी session delete करो और नई बनाओ
  const oldSessionId = getSessionIdFromCookie(request);
  if (oldSessionId) {
    await redis.del(`session:${oldSessionId}`);
  }
 
  // नई ID के साथ पूरी तरह नई session बनाओ
  const newSessionId = await createSession(user.id, request);
  return newSessionId;
}

CSRF (Cross-Site Request Forgery)#

Attack: User आपकी site पर logged in है। वो एक malicious page visit करता है जो आपकी site को request करती है। क्योंकि cookies automatically send होती हैं, request authenticated है।

Modern fix: SameSite cookies। SameSite: Lax set करना (जो अब ज़्यादातर browsers में default है) cross-origin POST requests पर cookies send होने से रोकता है, जो ज़्यादातर CSRF scenarios cover करता है।

typescript
// SameSite=Lax ज़्यादातर CSRF scenarios cover करता है:
// - Cross-origin POST, PUT, DELETE पर cookies block करता है
// - Cross-origin GET (top-level navigation) पर cookies allow करता है
//   यह fine है क्योंकि GET requests में side effects नहीं होने चाहिए
 
cookieStore.set("session_id", sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",  // यह आपकी CSRF protection है
  maxAge: 86400,
  path: "/",
});

JSON accept करने वाले APIs के लिए, आपको additional protection free में मिलती है: Content-Type: application/json header HTML forms से set नहीं किया जा सकता, और CORS दूसरे origins पर JavaScript को custom headers वाली requests करने से रोकता है।

अगर आपको stronger guarantees चाहिए (जैसे आप form submissions accept करते हैं), तो double-submit cookie pattern या synchronizer token इस्तेमाल करो। Auth.js यह आपके लिए handle करता है।

OAuth में Open Redirects#

Attack: Attacker एक OAuth callback URL craft करता है जो authentication के बाद उसकी site पर redirect करता है: https://yourapp.com/callback?redirect_to=https://evil.com/steal-token

अगर आपका callback handler blindly redirect_to parameter पर redirect करता है, user attacker की site पर पहुंचता है, potentially URL में tokens के साथ।

typescript
// VULNERABLE
async function handleCallback(request: Request) {
  const url = new URL(request.url);
  const redirectTo = url.searchParams.get("redirect_to") ?? "/";
  // ... user authenticate करो ...
  return Response.redirect(redirectTo); // https://evil.com हो सकता है!
}
 
// SAFE
async function handleCallback(request: Request) {
  const url = new URL(request.url);
  const redirectTo = url.searchParams.get("redirect_to") ?? "/";
 
  // Redirect URL validate करो
  const safeRedirect = sanitizeRedirectUrl(redirectTo, request.url);
  // ... user authenticate करो ...
  return Response.redirect(safeRedirect);
}
 
function sanitizeRedirectUrl(redirect: string, baseUrl: string): string {
  try {
    const url = new URL(redirect, baseUrl);
    const base = new URL(baseUrl);
 
    // सिर्फ same origin पर redirects allow करो
    if (url.origin !== base.origin) {
      return "/";
    }
 
    // सिर्फ path redirects allow करो (कोई javascript: या data: URIs नहीं)
    if (!url.pathname.startsWith("/")) {
      return "/";
    }
 
    return url.pathname + url.search;
  } catch {
    return "/";
  }
}

Referrer से Token Leakage#

अगर आप URLs में tokens रखते हैं (मत रखो), तो जब users links click करते हैं तो ये Referer header के ज़रिए leak होंगे। इसने real breaches cause किए हैं, GitHub पर भी।

नियम:

  • Authentication के लिए कभी URL query parameters में tokens मत रखो
  • Referrer-Policy: strict-origin-when-cross-origin (या stricter) set करो
  • अगर URLs में tokens रखने ही हैं (जैसे email verification links), तो उन्हें single-use और short-lived बनाओ
typescript
// अपने Next.js middleware या layout में
const headers = new Headers();
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");

JWT Key Injection#

एक कम well-known attack: कुछ JWT libraries jwk या jku header support करती हैं जो verifier को बताता है कि public key कहां ढूंढें। Attacker:

  1. अपना key pair generate कर सकता है
  2. अपने payload के साथ JWT बना सकता है और अपनी private key से sign कर सकता है
  3. jwk header अपनी public key की तरफ point करा सकता है

अगर आपकी library blindly jwk header से key fetch करके इस्तेमाल करती है, signature verify हो जाता है। Fix: token को कभी अपनी verification key specify करने मत दो। हमेशा अपनी configuration से keys इस्तेमाल करो।

2026 में मेरा Auth Stack#

सालों authentication systems build करने के बाद, यहां मैं आज वास्तव में क्या इस्तेमाल करता हूं।

ज़्यादातर Projects के लिए: Auth.js + PostgreSQL + Passkeys#

नए projects के लिए यह मेरा default stack है:

  • Auth.js (v5) heavy lifting के लिए: OAuth providers, session management, CSRF, database adapter
  • PostgreSQL Prisma adapter के साथ session और account storage के लिए
  • Passkeys SimpleWebAuthn से primary login method के रूप में नए users के लिए
  • Email/password fallback के रूप में उन users के लिए जो passkeys इस्तेमाल नहीं कर सकते
  • TOTP MFA password-based logins के लिए second factor के रूप में

Session strategy database-backed है (JWT नहीं), जो मुझे instant revocation और simple session management देती है।

typescript
// नए project के लिए मेरा typical auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Passkey from "next-auth/providers/passkey";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: "database" },
  providers: [
    Google,
    GitHub,
    Passkey({
      // Auth.js v5 में built-in passkey support है
      // यह internally SimpleWebAuthn इस्तेमाल करता है
    }),
  ],
  experimental: {
    enableWebAuthn: true,
  },
});

कब Clerk या Auth0 इस्तेमाल करें#

मैं managed auth provider तब चुनता हूं जब:

  • Project को enterprise SSO चाहिए (SAML, SCIM)। SAML correctly implement करना multi-month project है। Clerk इसे out of the box करता है।
  • Team में कोई security expertise नहीं। अगर team में कोई PKCE explain नहीं कर सकता, उन्हें scratch से auth build नहीं करनी चाहिए।
  • Time to market cost से ज़्यादा matter करता है। Auth.js free है लेकिन properly set up करने में days लगते हैं। Clerk एक afternoon में हो जाता है।
  • Compliance guarantees चाहिए (SOC 2, HIPAA)। Managed providers compliance certification handle करते हैं।

Managed providers के trade-offs:

  • Cost: Clerk per monthly active user charge करता है। Scale पर, यह बढ़ता है।
  • Vendor lock-in: Clerk या Auth0 से migrate करना painful है। आपकी user table उनके servers पर है।
  • Customization limits: अगर आपका auth flow unusual है, आप provider की opinions से लड़ेंगे।
  • Latency: हर auth check third-party API को जाता है। Database sessions के साथ, यह local query है।

जो मैं Avoid करता हूं#

  • अपना crypto roll करना। मैं JWTs के लिए jose, passkeys के लिए @simplewebauthn/server, passwords के लिए bcrypt या argon2 इस्तेमाल करता हूं। कभी hand-rolled नहीं।
  • Passwords SHA256 में store करना। bcrypt (cost factor 12+) या argon2id इस्तेमाल करो। SHA256 बहुत fast है — attacker GPU के साथ per second billions hashes try कर सकता है।
  • Long-lived access tokens। Maximum 15 minutes। Longer sessions के लिए refresh token rotation इस्तेमाल करो।
  • Cross-service verification के लिए symmetric secrets। अगर multiple services को tokens verify करनी हैं, RS256 public/private key pair के साथ इस्तेमाल करो।
  • Insufficient entropy वाले custom session IDs। Minimum crypto.randomBytes(32) इस्तेमाल करो। UUID v4 acceptable है लेकिन raw random bytes से कम entropy।

Password Hashing: सही तरीका#

चूंकि हमने mention किया — यहां 2026 में passwords properly hash कैसे करें:

typescript
import { hash, verify } from "@node-rs/argon2";
 
// Argon2id recommended algorithm है
// Web application के लिए ये reasonable defaults हैं
async function hashPassword(password: string): Promise<string> {
  return hash(password, {
    memoryCost: 65536,  // 64 MB
    timeCost: 3,        // 3 iterations
    parallelism: 4,     // 4 threads
  });
}
 
async function verifyPassword(
  password: string,
  hashedPassword: string
): Promise<boolean> {
  try {
    return await verify(hashedPassword, password);
  } catch {
    return false;
  }
}

bcrypt के ऊपर argon2id क्यों? Argon2id memory-hard है, जिसका मतलब है कि इसे attack करने के लिए सिर्फ CPU power नहीं बल्कि बड़ी मात्रा में RAM भी चाहिए। यह GPU और ASIC attacks को significantly ज़्यादा महंगा बनाता है। bcrypt अभी भी fine है — broken नहीं है — लेकिन argon2id नए projects के लिए better choice है।

Security Checklist#

कोई भी authentication system ship करने से पहले, verify करो:

  • Passwords argon2id या bcrypt (cost 12+) से hashed हैं
  • Login के बाद sessions regenerate हो रही हैं (session fixation prevent करता है)
  • Cookies HttpOnly, Secure, SameSite=Lax या Strict हैं
  • JWTs algorithms explicitly specify करते हैं (alg header पर कभी trust मत करो)
  • Access tokens 15 minutes या कम में expire होते हैं
  • Refresh token rotation reuse detection के साथ implemented है
  • OAuth state parameter verified हो रहा है (CSRF protection)
  • Redirect URLs allowlist के against validate हो रहे हैं
  • Login, registration, और password reset endpoints पर rate limiting applied है
  • Failed login attempts IP के साथ log हो रहे हैं लेकिन passwords के साथ नहीं
  • N failed attempts के बाद account lockout (progressive delays के साथ, permanent lock नहीं)
  • Password reset tokens single-use हैं और 1 hour में expire होते हैं
  • MFA backup codes passwords की तरह hashed हैं
  • CORS सिर्फ known origins allow करने के लिए configured है
  • Referrer-Policy header set है
  • JWT payloads में कोई sensitive data नहीं (ये किसी के भी द्वारा readable हैं)
  • WebAuthn counter verified और updated हो रहा है (credential cloning prevent करता है)

यह list exhaustive नहीं है, लेकिन वो vulnerabilities cover करती है जो मैंने production systems में सबसे ज़्यादा देखी हैं।

Wrapping Up#

Authentication उन domains में से एक है जहां landscape evolve होता रहता है, लेकिन fundamentals same रहते हैं: identity verify करो, minimum necessary credentials issue करो, हर boundary पर permissions check करो, और breach assume करो।

2026 में सबसे बड़ा shift passkeys का mainstream जाना है। Browser support universal है, platform support (iCloud Keychain, Google Password Manager) UX seamless बनाता है, और security properties genuinely हमारे पास पहले जो था उससे superior हैं। अगर आप नई application build कर रहे हैं, passkeys को अपना primary login method बनाओ और passwords को fallback treat करो।

दूसरा सबसे बड़ा shift यह है कि अपना auth roll करना justify करना harder हो गया है। Auth.js v5, Clerk, और similar solutions hard parts correctly handle करते हैं। Custom जाने का एकमात्र कारण तब है जब आपकी requirements genuinely किसी existing solution में fit नहीं होतीं — और वो ज़्यादातर developers जितना सोचते हैं उससे rarer है।

जो भी choose करो, अपनी auth को वैसे test करो जैसे attacker करता। Tokens replay करने, signatures forge करने, ऐसे routes access करने जहां नहीं करनी चाहिए, और redirect URLs manipulate करने की कोशिश करो। Launch से पहले जो bugs आप ढूंढते हैं वो news नहीं बनते।

संबंधित पोस्ट