Authentication Patterns in Modern Web Apps: JWT, Sessions, and Passkeys | SoniNow Blog

Limited TimeLearn More

authenticationjwtpasskeyssecurityweb development

Authentication Patterns in Modern Web Apps: JWT, Sessions, and Passkeys

Published

2026-06-23

Read Time

4 mins

Authentication Patterns in Modern Web Apps: JWT, Sessions, and Passkeys

Authentication is the security boundary of every web application. The choice between JWT, session-based auth, and passkeys affects your security posture, user experience, and operational complexity. Here is a practical comparison with implementation guidance for each approach.

Session-Based Authentication: The Server-Managed Approach

Session auth stores a session identifier in an HTTP-only cookie and keeps session data on the server — typically in Redis or PostgreSQL. It is the most straightforward model for traditional server-rendered applications.

// Next.js App Router session-based auth with Iron Session
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'

interface SessionData {
  userId: string
  role: 'user' | 'admin'
}

export async function getSession() {
  const cookieStore = await cookies()
  const session = await getIronSession<SessionData>(cookieStore, {
    password: process.env.SESSION_SECRET!,
    cookieName: 'session',
  })
  return session
}

export async function login(email: string, password: string) {
  const user = await authenticateUser(email, password)
  const session = await getSession()
  session.userId = user.id
  session.role = user.role
  await session.save()
}

Session auth gives you the ability to revoke sessions instantly — delete the session from the database and the user is logged out. The trade-off is server-side storage costs and the need for a shared session store in distributed deployments.

JWT Authentication: Stateless but Manageable

JSON Web Tokens encode user claims in a signed token. No server-side storage is needed — the token itself is the source of truth. This makes JWTs ideal for APIs and microservice architectures.

import { SignJWT, jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

export async function createToken(payload: { userId: string; role: string }) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m')
    .sign(secret)
}

export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret)
    return payload as { userId: string; role: string }
  } catch {
    return null
  }
}

Keep JWT expiration short — 15 minutes for access tokens. Use refresh tokens with longer expiry (7 days) stored in an HTTP-only cookie. This limits the damage window if a token is compromised while maintaining a seamless user experience.

Passkeys: The Passwordless Future

Passkeys use WebAuthn to authenticate users with biometrics or device PIN. They replace passwords entirely and are phishing-resistant by design. Browser support is now universal across major platforms.

import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server'

export async function startPasskeyRegistration(user: { id: string; name: string }) {
  const options = await generateRegistrationOptions({
    rpName: 'Your App',
    rpID: process.env.RP_ID!,
    userName: user.name,
    userID: Buffer.from(user.id),
  })
  // Store challenge temporarily for verification
  await redis.set(`challenge:${user.id}`, options.challenge, { ex: 120 })
  return options
}

The authentication flow is faster than any password-based system — users authenticate in under a second with their fingerprint or face. Adoption is accelerating as major platforms (Apple, Google, Microsoft) push passkeys as the primary authentication method.

Comparison Matrix

Criteria           | Sessions       | JWT              | Passkeys
-------------------|----------------|------------------|------------------
Server storage     | Required       | None             | Public key only
Revocation         | Instant        | Token expiry     | N/A
Phishing resistant | No             | No               | Yes
User experience    | Cookie-based   | Token header     | Biometric/PIN
Cross-domain       | Difficult      | Easy             | Requires RP ID
Implementation     | Simple         | Moderate         | Complex

Hybrid Strategy for Production

Most production applications benefit from a hybrid approach. Use session-based auth for the main web application — the server has control, revocation is instant, and cookie handling is well-understood. Use JWT tokens for API access from mobile apps or third-party integrations where sending cookies is impractical. Layer passkeys on top as an authentication method that replaces password-based login entirely.

// Hybrid approach in middleware
export async function authenticate(request: NextRequest) {
  // Check session cookie first
  const sessionCookie = request.cookies.get('session')
  if (sessionCookie) {
    const user = await verifySessionCookie(sessionCookie.value)
    if (user) return user
  }

  // Fall back to JWT Bearer token
  const authHeader = request.headers.get('Authorization')
  if (authHeader?.startsWith('Bearer ')) {
    const user = await verifyToken(authHeader.slice(7))
    if (user) return user
  }

  return null
}

Authentication decisions affect every layer of your application. Choosing the right pattern depends on your architecture, security requirements, and user base. At SoniNow, we design authentication systems that balance security with usability — from session management to passkey enrollment.

Need authentication architecture advice? Contact SoniNow and let us help you build a secure, user-friendly auth system for your web application.