A family of products often lives on separate subdomains — app.example.com, docs.example.com, learn.example.com — each with its own codebase and database. Users still expect one login. This guide explains the standard way to provide that with OAuth 2.0 and OpenID Connect, why each piece exists, and the trade-offs to plan for. The examples use a fictional accounts.example.com; the approach applies to any set of web products.

Why not just share a cookie?

The tempting shortcut is a session cookie scoped to .example.com that every subdomain reads. It works until it doesn't:

  • every application must share the same session store and the same secret, so a bug or leak in any one of them exposes all of them;
  • applications written in different languages or frameworks must agree on one session format;
  • a cookie readable by every subdomain is also sent to every subdomain — including ones that never needed it.

The more robust design: one dedicated service owns sign-in, and every product is an independent OAuth 2.0 client of it.

The pieces

1. One identity provider

A single service — here accounts.example.com — is the only place that ever sees passwords or talks to external sign-in providers such as Google. Products never receive a user's credentials; they only learn who the user is after a successful sign-in.

2. Authorisation code flow with PKCE

Signing in to a product works like this:

  1. The product redirects the browser to the identity provider with its client ID, a registered redirect URL, a random state value and a PKCE code_challenge.
  2. The user signs in on the identity provider — or is already signed in there, so this step is instant.
  3. The provider redirects back to the product with a one-time authorisation code.
  4. The product exchanges the code for tokens, proving it started the flow by sending the matching PKCE code_verifier.

PKCE (Proof Key for Code Exchange, RFC 7636) closes a specific hole: if an attacker intercepts the authorisation code, they still cannot exchange it, because they don't have the random verifier that only the original client generated. It was designed for apps that cannot keep a secret, such as single-page applications, and current OAuth security guidance recommends it for every client, including server-side ones.

The state value is checked when the user returns, so a product only accepts a sign-in it actually started. This blocks cross-site request forgery against the login itself.

3. Short-lived, asymmetrically signed tokens

Access tokens are usually signed JWTs that expire quickly — minutes rather than days — with a longer-lived refresh token used to obtain a new one without asking the user to sign in again.

The signing algorithm matters. With a shared-secret algorithm such as HS256, every service that verifies tokens holds the same secret that can create them, so compromising any one API lets an attacker mint tokens for all of them. With an asymmetric algorithm such as RS256 or ES256, only the identity provider holds the private key; services need just the public key, which can verify but never sign.

4. Publishing public keys

OpenID Connect providers publish their public keys as a JSON Web Key Set (JWKS) and describe their endpoints in a discovery document. An API that receives a token fetches the key set once (and caches it), then checks the signature, expiry, issuer and audience locally — no call to the identity provider on every request. Rotating keys means publishing the new key alongside the old one until tokens signed with the old key have expired.

Share identity, not data

Single sign-on should answer "who is this person" — nothing more. Each product looks the user up in its own database by their account identifier and keeps its own data. A breach of one product's database then does not expose the others, and each product can be developed and released independently. Zerethon's products follow this principle: one account across products, with each product keeping its own data — see how the Zerethon products fit together.

Trade-offs and limitations

  • Single sign-out is harder than single sign-on. Signing out of one product ends that product's session; others keep theirs until their tokens expire or the user signs out there too. Short token lifetimes limit the window.
  • The identity provider is a dependency for new sign-ins. If it is down, people already signed in keep working, but nobody can start a new session. Treat it as your most critical service.
  • Redirects add a step. The first visit to each product bounces through the identity provider; afterwards it is a near-instant redirect.
  • Moving an existing product to SSO takes planning. Existing accounts must be linked to the central identity, and an API usually has to accept its old tokens alongside the new ones for a transition period.

Checklist

  1. Use the authorisation code flow with PKCE for every client — never the implicit flow.
  2. Generate a fresh state (and an OpenID Connect nonce) per sign-in and verify them on return.
  3. Sign tokens asymmetrically and give APIs only the public key.
  4. Keep access tokens short-lived; protect refresh tokens like passwords.
  5. Register exact redirect URLs per client; never allow wildcards.
  6. Share identity, not data, between products.