Front PageProjectsBlogAbout
Language
Building a Custom OAuth2 Server with Cookie and Header-Based Authentication
January 28, 20254 min read

Building a Custom OAuth2 Server with Cookie and Header-Based Authentication

How to implement a custom OAuth2 storage model, scope-based access control, browser/mobile authentication strategies, and CSRF protection without relying on a hosted auth vendor.

  • oauth2
  • security
  • node.js
  • authentication

Why Build a Custom OAuth2 Server

Hosted authentication platforms are often the right choice. But sometimes the authorization model is opinionated enough that a custom implementation is justified.

Typical reasons include:

  • tiered access where upgrades and downgrades must change scopes precisely
  • a single API serving both browser clients and native mobile clients
  • custom token persistence requirements
  • the need to keep authorization logic close to payment and entitlement systems

In that situation, an OAuth2 library can provide the protocol mechanics while the application implements the persistence model.

The Storage Model

Most OAuth2 server libraries treat storage as an interface contract. You provide methods that load users, load clients, save tokens, and validate credentials. The protocol engine handles grant flows, token issuance, expiration checks, and error semantics.

A robust storage model usually persists:

  • access tokens
  • refresh tokens
  • client definitions
  • user records

For document databases, token collections pair well with TTL indexes. Expired token records are removed automatically by the database engine, which is much cleaner than building a separate cleanup worker just to delete stale credentials.

The design principle is simple: let the protocol layer handle OAuth2, and let the storage layer handle persistence rules.

Scope Design Matters More Than People Think

A flat role model breaks down quickly once there are multiple user tiers and paid entitlements.

A better model uses explicit scopes with clear semantics:

| Scope Type | Responsibility | |---|---| | Base scopes | Fundamental authenticated capabilities | | Write scopes | Mutating actions such as profile updates | | Tier scopes | Which content or features a user can access | | Admin scopes | Operational or back-office access | | Override scopes | Emergency or support-level elevation |

One subtle but important idea is mutual exclusivity. Some scopes are not hierarchical. They represent distinct states. When a user upgrades from one tier to another, the old tier scope may need to be removed, not merely supplemented.

That distinction prevents entitlement drift.

Browser and Mobile Clients Should Not Authenticate the Same Way

Web browsers and native mobile clients have different threat models. Treating them identically usually produces awkward tradeoffs.

For browsers, cookie-based authentication is often the safest default:

  • credentials are stored in httpOnly cookies
  • the browser attaches them automatically
  • client-side JavaScript cannot read them

That removes a large category of token theft through frontend runtime compromise.

For native mobile clients, bearer tokens in the Authorization header are usually more appropriate. Native clients are not exposed to the same cross-site request patterns as browsers, so the surrounding security controls differ.

The key point is not the specific transport. The key point is that authentication strategy should follow the client threat model.

CSRF Protection for Browser Clients

Once browser clients authenticate with cookies, CSRF protection becomes mandatory.

One of the simplest effective patterns is double-submit cookies:

  1. set a CSRF token cookie
  2. require the client to echo the token in a request header
  3. compare them server-side

The comparison itself should be timing-safe. Otherwise, byte-by-byte timing leakage can turn a token check into a side-channel problem.

Native mobile clients, by contrast, often bypass this entirely because they authenticate via headers and are not vulnerable to the same browser-origin behavior.

Bot Protection Around Account Flows

Authentication systems are abuse magnets. Registration, password reset, and login-related flows get hammered by bots long before more interesting parts of the system do.

That is why it is worth layering challenge-based verification onto high-risk account operations:

  • account creation
  • password reset
  • email verification triggers
  • credential recovery flows

The challenge mechanism does not replace rate limiting. It complements it. Challenge verification raises the effort required for abuse; rate limiting constrains volume.

Token Lifetime Is a Security and UX Tradeoff

Long-lived tokens reduce user friction, but they increase the blast radius of credential leakage. Short-lived tokens reduce exposure, but they increase refresh traffic and re-authentication frequency.

The right answer depends on:

  • how sensitive the data is
  • whether refresh tokens exist
  • how often users are active
  • how painful re-authentication is on the client

There is no universally correct number. What matters is that token lifetime is chosen deliberately rather than inherited accidentally from a tutorial.

Design Lessons

  1. OAuth2 protocol handling and token persistence should be separated cleanly.
  2. TTL indexes are a better expiration mechanism than manual cleanup jobs.
  3. Scope systems should model business states explicitly, not just hierarchy.
  4. Browser and native clients deserve different authentication strategies.
  5. Cookie-based browser auth must be paired with CSRF protection.
  6. Account-related endpoints need bot controls in addition to authentication rules.
Explore more articles