Ink&Horizon
HomeBlogTutorialsLanguages
Ink&Horizon— where knowledge meets the horizon —Learn to build exceptional software. Tutorials, guides, and references for developers — from first brushstroke to masterwork.

Learn

  • Blog
  • Tutorials
  • Languages

Company

  • About Us
  • Contact Us
  • Privacy Policy

Account

  • Sign In
  • Register
  • Profile
Ink & Horizon

© 2026 InkAndHorizon. All rights reserved.

Privacy PolicyTerms of Service
Back to Blog
Backend

Authentication in 2026: Passkeys, WebAuthn, OAuth 2.1 & Zero-Trust

The complete guide to modern auth — from passwords to passwordless, PKCE to passkeys

2026-03-15 26 min read
ContentsThe Authentication Landscape in 2026JWT Deep Dive: How Tokens Actually WorkRefresh Token Rotation: The Security PatternPasskeys & WebAuthn: The Passwordless FutureOAuth 2.1 with PKCE: Third-Party Login Done RightToken Storage: Where to Put Your TokensInterview Questions: Authentication & SecurityKey Takeaways

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

Passkeys replace passwords — cryptographic keys protected by biometrics.
OAuth 2.1 makes PKCE mandatory — implicit grant is officially dead.
Zero-trust: every request must prove identity, regardless of network.
JWTs remain the standard for API-to-API auth and session tokens.

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.

Snippet
// 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

JWT = Header.Payload.Signature (three Base64-encoded parts).
Stateless verification: server checks signature, no DB lookup needed.
Cannot be invalidated before expiry — keep access tokens SHORT (15 min).
Always verify the issuer (iss) claim to prevent cross-service token reuse.
Use RS256 (asymmetric) for microservices — services can verify without knowing the secret.

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.

Snippet
// 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.

Snippet
// 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

Passkeys = FIDO2/WebAuthn cryptographic key pairs.
Private key stays on the device — server only stores the public key.
Phishing-resistant: passkeys are domain-bound, cannot be reused on fake sites.
Synced across devices via iCloud Keychain / Google Password Manager.
navigator.credentials.create() — browser API for creating passkeys.

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.

Snippet
// 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

HttpOnly cookie: immune to XSS, needs CSRF protection. Best for refresh tokens.
Memory (JS variable): immune to both XSS and CSRF. Cleared on page refresh.
localStorage: vulnerable to XSS. Use ONLY with strict CSP headers.
Best pattern: access token in memory + refresh token in HttpOnly cookie.
Always set Secure, SameSite=Strict, and short MaxAge on auth cookies.

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.

Key Takeaways

Passkeys are the future — phishing-resistant, biometric-protected, cross-platform.
OAuth 2.1: PKCE mandatory, implicit grant removed, refresh token rotation recommended.
JWT: stateless, short-lived (15min), signed with RS256 for microservices.
Store: access token in memory, refresh token in HttpOnly cookie.
Refresh token rotation: new token on every refresh, detect reuse = revoke all.
For production: use Auth.js/Clerk/Supabase. For interviews: know the internals.
AS
Article Author
Ashutosh
Lead Developer

Related Knowledge

Tutorial

Python Async Patterns

5m read
Tutorial

Go Concurrency in Practice

5m read
Tutorial

Java Virtual Threads

5m read
Article

Understanding Closures in JavaScript: The Complete 2026 Guide

22 min read
Article

React 19 Server Components: The Definitive 2026 Guide

28 min read
Article

Next.js 15 App Router Masterclass: Everything You Need to Know

25 min read