6 May 2026 · Mark Bakker
Stateless by design — an OAuth2 server that holds zero user state
Most authorization servers store sessions, tokens, and credentials. Ours stores none of them. Here is what moved where, and what we gave up for it.
When people ask how our Authorization Hub works, the one-sentence answer is: it's a production OAuth2 authorization server that holds zero user session state. No password hashes. No session cookies pointing at a session table. No long-lived refresh tokens in a database. If the hub process goes down and comes back, it forgets nothing, because it never knew anything.
This isn't a gimmick. It's the security posture we wanted.
What OAuth2 actually needs to remember
Before we could ship a stateless Hub, we had to ask: what state does OAuth2 genuinely require?
Walking through the RFC 6749 authorization code flow, there are four pieces of state a server typically holds:
- The authorization code — short-lived, single-use, must survive one round-trip.
- The access token — presented with every API call, must be verifiable by any resource server.
- The refresh token — longer-lived, must be exchangeable for new access tokens, must be revocable.
- The user's authentication session — "is this user still logged in?"
The trick is that only #1 needs to live on disk, and only briefly. Everything else can be redesigned.
Where we put each piece
Authorization codes live in PostgreSQL for their ten-second lifetime, single-use, deleted immediately on exchange. That table never grows.
Access tokens are signed JWTs. They carry their own claims, their own expiry, and their signature is verifiable by any consumer who has the JWKS. Hub doesn't need to remember issuing them — a verifier checks the signature and trusts the claims.
Refresh tokens are JWTs too, but they're tracked in families. When a refresh token is used, we check its family root; if the family already saw a previous use, we know the token was replayed and we invalidate the entire family. No one token holder can go rogue without revealing themselves. The family metadata is small — a UUID per family and a counter — so even at scale it's cheap.
User sessions live with the federation member (Okta, Azure AD, your SAML IdP — whoever does the actual authentication). Hub never sees passwords. When a user lands on /oauth2/authorize, Hub figures out which IdP they belong to and redirects them there. When they come back, there's a short-lived FED_TOKEN cookie that carries their verified identity for the duration of the authorization flow, then gets revoked. By the time Hub issues the access token, the cookie is gone.
The Vault-Transit part
There's one more piece that makes this work: the signing keys. A stateless JWT server is only as secure as the keys that sign its tokens. If an attacker gets the key, they can forge tokens forever — stateless or not.
We delegate signing to HashiCorp Vault Transit. Hub never holds the private key. Every token signature is an HTTP call to Vault:
POST /v1/transit/sign/hub-signing-key
{
"input": "<base64-encoded JWT payload>"
}
Response:
{
"data": { "signature": "vault:v1:..." }
}
If the Hub process is compromised, the attacker gets the ability to request signatures from Vault for as long as the process is running — but they can't steal the key and sign tokens offline at their leisure. Rotation is one API call. Revocation is instant.
The cost is a Vault round-trip per token. For interactive flows (human logins, web sessions) this is imperceptible — a few milliseconds on top of everything else. For machine-to-machine or high-throughput token issuance you'd want to batch or cache, but that's a separate problem.
What we gave up
Statelessness has trade-offs we chose to accept:
No "log everyone out" button. You can't invalidate a session by flipping a database flag, because there are no sessions. Revocation works at the refresh-token family level (per user) or the key level (platform-wide). For a per-user kill switch, you rotate their refresh token family.
Federation is required. Hub isn't a complete identity system out of the box. It needs a federation member — an IdP that does the actual authentication. If you want local username/password, you implement a federation member that does it. We think that's a feature: passwords belong in one place, not scattered across every OAuth2 server.
Vault is a hard dependency. No Vault, no signatures, no tokens. You can run Vault on your own infrastructure or use HashiCorp Cloud Platform — but you can't run Hub without one. For most deployments this is fine; Vault is a mature component and its failure modes are well-understood.
Why this matters
If someone compromises the Hub process tomorrow, here's what they get: the ability to see current in-flight authorization flows and the ability to request signatures from Vault until we notice and rotate the Vault key. What they don't get: passwords (we don't have them), session databases (we don't have them), persistent tokens (we don't have them), historical user data (we don't have it).
That's the promise we wanted the Authorization Hub to make. Getting there meant throwing out a lot of default assumptions about how an OAuth2 server works. It was worth it.
Want to see it running? Request access and we'll wire up your first federation member.