The Authentication Landscape in 2026
Authentication is undergoing its biggest transformation in 20 years. Passwords are being replaced by Passkeys — cryptographic key pairs stored on your device and protected by biometrics (Face ID, fingerprint). Apple, Google, and Microsoft have made Passkeys the default sign-in method across their platforms.
OAuth 2.1 consolidates years of security best practices (PKCE mandatory, implicit grant removed, refresh token rotation) into a single spec. Meanwhile, zero-trust architecture means every request is authenticated and authorized — no implicit trust based on network location.
For developers, this means you need to understand: JWTs (still the workhorse for API auth), Passkeys/WebAuthn (replacing passwords), OAuth 2.1 (third-party login), and token management patterns (rotation, revocation, storage).
Key Takeaways
JWT Deep Dive: How Tokens Actually Work
A JSON Web Token (JWT) is a URL-safe, self-contained token consisting of three Base64-encoded parts: Header (algorithm + token type), Payload (claims — user data, expiry, issuer), and Signature (cryptographic proof that the token has not been tampered with).
JWTs are "stateless" — the server does not need to store them in a database to verify them. It just checks the signature using its secret key. This makes them ideal for microservices where a central session store is impractical.
But JWTs have a critical limitation: you cannot invalidate them before expiry. Once a token is signed, it is valid until the exp claim expires. This is why access tokens must be short-lived (15 minutes max) and refresh tokens handle renewal.
// JWT Structure (decoded) // Header { "alg": "HS256", "typ": "JWT" } // Payload (claims) { "sub": "user_123", // Subject (user ID) "name": "Ashutosh", // Custom claim "role": "admin", // Custom claim "iat": 1711929600, // Issued at "exp": 1711930500, // Expires (15 min later) "iss": "inkandhorizon.com" // Issuer } // Signature HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) // ✅ Node.js: Create and verify JWTs import jwt from 'jsonwebtoken'; // Sign (create) token const token = jwt.sign( { userId: 'user_123', role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '15m', issuer: 'inkandhorizon.com' } ); // Verify token try { const decoded = jwt.verify(token, process.env.JWT_SECRET, { issuer: 'inkandhorizon.com', // Verify issuer claim }); console.log(decoded.userId); // 'user_123' } catch (err) { if (err.name === 'TokenExpiredError') { console.log('Token expired — use refresh token'); } }
Key Takeaways
Refresh Token Rotation: The Security Pattern
Refresh token rotation is the gold standard for session management. Each time a client uses a refresh token to get a new access token, the server also issues a NEW refresh token and invalidates the old one.
If an attacker steals a refresh token and the legitimate user also uses it, the server detects the reuse (an already-invalidated token being presented) and invalidates ALL tokens for that user — forcing re-login.
This pattern provides the security of short-lived sessions with the UX of long-lived sessions.
// Refresh Token Rotation Implementation async function refreshTokens(oldRefreshToken: string) { // 1. Verify the refresh token const decoded = jwt.verify(oldRefreshToken, REFRESH_SECRET); // 2. Check if token has been used before (replay detection) const storedToken = await db.refreshToken.findUnique({ where: { token: oldRefreshToken }, }); if (!storedToken || storedToken.revoked) { // ⚠️ TOKEN REUSE DETECTED — possible theft! // Revoke ALL tokens for this user (nuclear option) await db.refreshToken.updateMany({ where: { userId: decoded.userId }, data: { revoked: true }, }); throw new Error('Token reuse detected — all sessions invalidated'); } // 3. Revoke the old refresh token (one-time use) await db.refreshToken.update({ where: { token: oldRefreshToken }, data: { revoked: true }, }); // 4. Issue new token pair const newAccessToken = jwt.sign( { userId: decoded.userId, role: decoded.role }, ACCESS_SECRET, { expiresIn: '15m' } ); const newRefreshToken = jwt.sign( { userId: decoded.userId, type: 'refresh' }, REFRESH_SECRET, { expiresIn: '7d' } ); // 5. Store the new refresh token await db.refreshToken.create({ data: { token: newRefreshToken, userId: decoded.userId, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }, }); return { accessToken: newAccessToken, refreshToken: newRefreshToken }; }
Passkeys & WebAuthn: The Passwordless Future
Passkeys are the FIDO2/WebAuthn standard for passwordless authentication. Instead of passwords, users authenticate with biometrics (Face ID, fingerprint) or device PINs. The credentials are cryptographic key pairs: the private key stays on the device, the public key is stored on the server.
Passkeys are phishing-resistant: unlike passwords, they are bound to the specific domain. If a user is tricked into visiting a fake site, the passkey simply does not work because the domain does not match. This eliminates the #1 attack vector for account compromise.
All major platforms now support Passkeys: iOS/macOS (iCloud Keychain), Android (Google Password Manager), Windows (Windows Hello), and Chrome/Safari/Firefox. In 2026, Passkeys are ready for production.
// WebAuthn Registration (creating a passkey) // Server: Generate challenge app.post('/api/auth/passkey/register/options', async (req, reply) => { const user = await getAuthenticatedUser(req); const options = await generateRegistrationOptions({ rpName: 'Ink & Horizon', rpID: 'inkandhorizon.com', userID: user.id, userName: user.email, userDisplayName: user.name, attestationType: 'none', authenticatorSelection: { residentKey: 'required', // Discoverable credential (passkey) userVerification: 'preferred', // Biometric if available }, }); // Store challenge for verification await storeChallenge(user.id, options.challenge); return options; }); // Client: Create passkey with browser API const options = await fetch('/api/auth/passkey/register/options', { method: 'POST' }); const credential = await navigator.credentials.create({ publicKey: options, }); // Send credential to server for verification and storage await fetch('/api/auth/passkey/register/verify', { method: 'POST', body: JSON.stringify(credential), });
Key Takeaways
OAuth 2.1 with PKCE: Third-Party Login Done Right
OAuth 2.1 is the upcoming consolidation of OAuth 2.0 best practices. The biggest change: PKCE (Proof Key for Code Exchange) is now MANDATORY for all clients — not just mobile apps. The implicit grant (sending tokens in URL fragments) is removed entirely.
PKCE prevents authorization code interception attacks. The client generates a random code_verifier, sends its hash (code_challenge) with the authorization request, and proves possession of the original verifier when exchanging the code for tokens.
In practice, "Login with Google/GitHub" uses this flow. Libraries like NextAuth.js / Auth.js handle the complexity for you, but you must understand the flow for interviews.
// OAuth 2.1 Authorization Code + PKCE Flow // Step 1: Generate PKCE pair const codeVerifier = crypto.randomUUID() + crypto.randomUUID(); const codeChallenge = btoa( String.fromCharCode(...new Uint8Array( await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)) )) ).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); // Step 2: Redirect user to authorization server const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth'); authUrl.searchParams.set('client_id', CLIENT_ID); authUrl.searchParams.set('redirect_uri', 'https://inkandhorizon.com/callback'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', 'openid email profile'); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); authUrl.searchParams.set('state', crypto.randomUUID()); // CSRF protection window.location.href = authUrl.toString(); // Step 3: Exchange code for tokens (server-side) 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: authorizationCode, redirect_uri: 'https://inkandhorizon.com/callback', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, code_verifier: codeVerifier, // Proves we initiated the request }), });
Token Storage: Where to Put Your Tokens
Where you store tokens determines your security posture. There is no perfect solution — only trade-offs between XSS resistance, CSRF resistance, and developer convenience.
HttpOnly cookies are the most secure storage for web apps: they are immune to XSS (JavaScript cannot read them) and send automatically with same-origin requests. But they require CSRF protection (SameSite=Strict or CSRF tokens).
localStorage is the most common (and most dangerous) storage: it is accessible to any JavaScript on the page, making it vulnerable to XSS attacks. Use it for access tokens ONLY if you have strong CSP (Content Security Policy) headers.
The recommended pattern: access token in memory (JavaScript variable), refresh token in HttpOnly Secure SameSite=Strict cookie. This gives you XSS immunity for the refresh token and CSRF immunity for the access token.
Key Takeaways
Interview Questions: Authentication & Security
Q1: What is the difference between authentication and authorization? → Authentication = "who are you?" Authorization = "what can you do?"
Q2: Why should access tokens be short-lived? → They cannot be revoked before expiry. Short TTL (15min) limits the window of compromise.
Q3: What is refresh token rotation? → Each refresh generates a new refresh token and invalidates the old one. Detects token theft via reuse detection.
Q4: Why is the implicit grant removed in OAuth 2.1? → Tokens in URL fragments are logged in browser history, proxy logs, and referrer headers. PKCE + auth code is safer.
Q5: What are Passkeys? → FIDO2/WebAuthn cryptographic credentials protected by biometrics. Domain-bound (phishing-resistant). Replace passwords.
Q6: Where should you store JWTs in a web app? → Access token in memory, refresh token in HttpOnly cookie. Never in localStorage for sensitive apps.
Q7: What is PKCE? → Proof Key for Code Exchange. Client proves it initiated the auth request by sending a code_verifier that matches the original code_challenge.
Q8: How do you handle JWT invalidation if tokens are stateless? → Keep a blocklist of revoked token IDs (jti claim) with TTL matching token expiry. Trade-off: adds a DB lookup.
Q9: What is zero-trust architecture? → Never trust, always verify. Every request must prove identity and authorization regardless of network location.
Q10: How does WebAuthn prevent phishing? → Passkeys are bound to the RP ID (domain). A fake domain cannot use a passkey registered for the real domain.
Key Takeaways
Authentication in 2026 is a layered problem: JWTs for API tokens, Passkeys for user login, OAuth 2.1 for third-party delegation, and refresh token rotation for session management. No single technology solves everything.
The critical patterns: short-lived access tokens (15min) + refresh token rotation with reuse detection, HttpOnly cookies for token storage, PKCE for all OAuth flows, and Passkeys as the preferred passwordless method.
For production: use established libraries (Auth.js, Clerk, Supabase Auth) instead of rolling your own. But for interviews, you must understand every layer of the auth stack from HMAC signing to PKCE code challenges.