// Security

Sessions, JWTs or OAuth? Choosing the Right Authentication for Your Web App

A practical comparison of server sessions, JSON Web Tokens and OAuth 2.0 with PKCE, with clear advice on which one fits a web app, a mobile app or an API.

Sessions, JWTs or OAuth? Choosing the Right Authentication for Your Web App

Authentication is the part of an application where a small mistake becomes a big incident. It is also where teams spend a lot of time debating tools. This article cuts through the debate: what each approach is, where it fits, and the defaults we would choose.

Authentication and authorisation

Two words that get mixed up:

  • Authentication answers "who are you?"
  • Authorisation answers "what are you allowed to do?"

Everything below is about the first. Get it right, and then enforce authorisation on the server for every request.

Option 1: Server-side sessions

The server creates a random session ID after login, stores the session data (in memory, Redis or a database), and sends the ID to the browser in a cookie. On each request, the server looks the session up.

Boring wins: server session + HttpOnly cookie.

Set-Cookie: sid=9f2c...; HttpOnly; Secure; SameSite=Lax; Path=/

Strengths

  • You can end a session instantly, for a logout, a password change or a suspected theft.
  • The browser never holds anything that can be read by JavaScript when the cookie is HttpOnly.
  • Simple to reason about.

Weaknesses

  • Needs shared session storage once you run more than one server.
  • Less natural for mobile apps and third-party API clients.

Option 2: JSON Web Tokens (JWT)

A JWT is a signed token that carries claims such as the user ID and an expiry. The server can verify it without looking anything up.

JWTs are hard to revoke. Keep them short-lived.

Strengths

  • Stateless verification, which suits service-to-service calls.
  • Easy to pass between different services and domains.

Weaknesses

  • Revocation is hard. A valid token stays valid until it expires, unless you keep a deny list, which brings back the state you wanted to avoid.
  • Storage is risky. Keeping a token in localStorage exposes it to any cross-site scripting bug.
  • Mistakes in verification, such as accepting the wrong algorithm, have caused serious breaches.

JWTs are a good tool for short-lived tokens between services. They are a poor default for keeping a person logged in to a website.

Option 3: OAuth 2.0 and OpenID Connect

OAuth lets your app obtain access without ever seeing the user's password, by delegating login to an identity provider such as Google, Microsoft or your own company login. OpenID Connect adds a standard way to get the user's identity on top of it.

For browser and mobile apps the recommended flow is authorization code with PKCE:

sequenceDiagram
    actor U as User
    participant A as App
    participant AS as Auth server
    participant API as API

    U->>A: Start login
    A->>A: Generate verifier
    A->>A: Create code challenge
    A->>AS: Authorization request
    AS->>U: Request authentication
    U->>AS: Authenticate
    AS-->>A: Return authorization code
    A->>AS: Send code and verifier
    AS->>AS: Verify PKCE challenge
    AS-->>A: Return access token
    A->>API: Request with token
    API-->>A: Return protected data
    A-->>U: Show application

PKCE protects the code exchange, so that a stolen authorisation code is useless without the secret that only your app holds.

Use this when you want "Sign in with Google" or single sign-on for business customers, or when you are building the identity provider yourself and many apps will rely on it.

Which one should you pick

Situation Recommended
A web app where users log in to your own site Server sessions with an HttpOnly cookie
"Sign in with Google" or customer SSO OAuth 2.0 / OpenID Connect, with PKCE
Mobile app talking to your API OAuth with PKCE, short-lived access token plus a refresh token in secure storage
Service-to-service calls inside your system Short-lived JWTs or mutual TLS
Public API for developers API keys for identification, OAuth for acting on behalf of users

For most business web applications, boring server sessions are the right answer.

Whatever you choose, do these things

  1. Hash passwords properly with Argon2id or bcrypt. Never store them in a reversible form or with a fast hash such as plain SHA-256.
  2. Offer multi-factor authentication, at least for admin accounts. An authenticator app or a passkey is much stronger than SMS.
  3. Rate-limit login and password reset endpoints, and lock or slow down after repeated failures.
  4. Use HTTPS everywhere and mark cookies Secure.
  5. Set SameSite on cookies and add CSRF protection for state-changing requests.
  6. Rotate the session ID after login to prevent session fixation.
  7. Give password reset links a short life and make them single-use.
  8. Log authentication events, such as sign-ins, failures and resets, so you can investigate.

Common mistakes

  • Putting long-lived tokens in localStorage.
  • Trusting the client to say who it is, for example reading a userId from the request body.
  • Writing your own cryptography or password storage.
  • Checking permissions only in the interface and not on the server.
  • Leaving default or shared admin accounts in place.

Conclusion

You do not need the most modern option. You need the one that matches your clients, can be revoked when something goes wrong, and avoids keeping secrets where scripts can read them. For a website, that is usually server sessions. For delegated login, it is OAuth with PKCE. For internal service calls, it is short-lived tokens.

Hash with Argon2id or bcrypt. Never a fast hash.

Filed underAuthenticationSecurityOAuthJWT