DEV Community

cio fea
cio fea

Posted on Originally published at loginos.io AI-assisted

How do I share a login session across multiple domains?

I’m building loginos, a sign-in service for products that run on more than one domain. This question comes up a lot, so here is the full answer: the general approach first, then how loginos handles it.

Short answer: You can’t share a session cookie between two different domains, because browsers only send a cookie to the domain that set it. Instead, each domain keeps its own session and gets it from one central login, an OpenID Connect provider. Someone who is already signed in there is sent straight back with a one-time code, without seeing a form.

You run more than one site, like app.example.com, example.net and shop.example.org, and you want someone who signs in on one of them to be signed in on the others. The obvious idea is to share the session cookie. That works for subdomains, but not for different domains, and no setting changes that. What you share instead is the sign-in: every domain asks the same place who the person is.

Why can’t two domains share a session cookie?

A cookie belongs to the site that set it. When example.com sets session=abc, the browser sends that cookie back to example.com and, if the cookie has Domain=example.com, to its subdomains. It never sends it to example.net. This rule is what stops any website from reading your bank’s session cookie, so browsers will not relax it.

Third-party cookies used to offer a workaround: an example.net page could load something from example.com and get that domain’s cookies with it. Safari and Firefox block third-party cookies by default, and storage inside iframes is now partitioned by the top-level site. Tricks built on hidden iframes or postMessage between your domains break for many of your users, often without an error.

What are your options?

There are three ways to sign someone in on more than one site. Which one fits depends on whether your sites share a parent domain.

Approach Works across different domains? What you build
One cookie for the parent domain No, only for subdomains like app.example.com and shop.example.com Nothing extra: set Domain=example.com on the session cookie
A central login (identity provider) Yes, any number of domains A redirect route and a callback route on each site
Hand a token from one site to the other Yes, but you secure it yourself One-time tokens, expiry, replay protection, redirect checks

One parent-domain cookie is the simplest when it fits. Everything under example.com can read a cookie set with Domain=example.com. It stops at the edge of that domain.

Handing tokens across means site A creates a short-lived token and redirects to site B with it in the URL, and site B trades it for a session. It works, but you end up writing a small identity provider: tokens that work once, expire fast, are bound to the target site, and never land in logs. Most teams who start here move to a real provider by the third domain.

A central login is the standard answer, and it is what “single sign-on” means on the web. One service, the identity provider, keeps its own session on its own domain. Every one of your sites sends people there to sign in and gets them back with a one-time code.

How a central login shares the sign-in

The protocol most providers speak is OpenID Connect (OIDC), a layer on top of OAuth 2.0 that tells your app who signed in. Here is what happens when someone who signed in on app.example.com opens example.net for the first time:

  1. example.net has no session for them, so it redirects the browser to the provider, with its own address as the return address.
  2. The provider sees its own session cookie, set when they signed in on app.example.com. It doesn’t show a form.
  3. The provider sends the browser straight back to example.net with a code that works once.
  4. The server of example.net exchanges the code for the person’s identity, then sets its own session cookie.

The person sees a short redirect and is signed in. Each domain still has its own session, and nothing crosses between domains except the browser itself, carrying a one-time code. That is why this works without third-party cookies.

Tip: To check for a sign-in without ever showing a page, add prompt=none to the redirect. The provider answers at once: with a code if the person is signed in, or with error=login_required if not.

What to watch out for

  • Check the return address. The provider must only send codes to addresses you registered. A provider that accepts any return address hands sign-in codes to whoever asks.
  • Exchange codes on your server. The code is worth nothing without your app’s secret, so the exchange belongs on the server, not in the browser.
  • Use state and PKCE. state ties the callback to the browser that started the sign-in, and PKCE stops a stolen code from being used.
  • Sign-out is per domain. Signing out of one site clears that site’s session. To end the central session too, send the browser to the provider’s logout address.
  • Don’t merge accounts by email unless the provider has verified the address. Use a stable user id (sub in OpenID Connect).

How loginos does it

loginos is an OpenID Connect provider built for exactly this: one product on many domains. You create a platform, add your domains (as many as you have), and they all share one pool of people. Someone who creates an account on one domain is already a member on the others.

On each domain you add two routes: one that redirects to loginos, and a callback that exchanges the code. The callback path is the same on every domain, /api/auth/login/single/callback by default, and loginos only sends people back to https://<one of your domains><callback path>.

const LOGINOS = "https://loginos.io";

// The callback, on every one of your domains.
app.get("/api/auth/login/single/callback", async (req, res) => {
  const [state, verifier] = (req.cookies.lo_flow ?? "").split(".");
  if (!state || req.query.state !== state) return res.status(400).send("Sign-in expired, try again.");

  const r = await fetch(`${LOGINOS}/oauth/token`, {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: String(req.query.code ?? ""),
      redirect_uri: `https://${req.get("host")}/api/auth/login/single/callback`,
      code_verifier: verifier,
      client_id: process.env.LOGINOS_APP_KEY,
      client_secret: process.env.LOGINOS_APP_SECRET,
    }),
  });
  if (!r.ok) return res.status(401).send("Sign-in failed.");
  const { user } = await r.json(); // sub, preferred_username, roles, permissions…
  req.session.user = user; // this domain's own session
  res.redirect("/");
});
Enter fullscreen mode Exit fullscreen mode

The quickstart has the redirect route and complete versions for Next.js, Express and PHP. A few things come with it:

  • No form on the second domain. Someone signed in to loginos who has used your platform before comes straight back.
  • Domains that come and go. Add domains in the console or from your server, cover subdomains with a verified wildcard, or let your server confirm new domains as people sign in.
  • No redirect at all, if you like. Chrome and Edge can show their own “Continue as” prompt on your page.
  • Passkeys, and roles and permissions in every token, on every domain.

Frequently asked questions

Can I share cookies between example.com and example.net?

No. Browsers send a cookie only to the domain that set it and, with the Domain attribute, to that domain’s subdomains. example.com and example.net are different sites, so each needs its own session, issued after one central sign-in.

Do subdomains count as different domains?

Not for cookies. A cookie set with Domain=example.com is sent to app.example.com and shop.example.com too. If all your sites are subdomains of one domain, a parent-domain cookie can be enough. As soon as you add a second domain, you need a central login.

Will people have to sign in again on my second domain?

Not with a central login. When they reach the second domain, it redirects them to the provider, which recognizes them from its own session and sends them back with a code. They see a quick redirect, not a form.

Does this still work when third-party cookies are blocked?

Yes. The redirect flow only uses first-party cookies: the provider reads its own cookie on its own domain, and your site sets its own cookie on yours. It doesn’t depend on iframes or third-party storage.

What happens when someone signs out?

Each site clears its own session. The provider’s session stays until it ends or the person signs out there, so they may come back without a form. If you want sign-out to end everything, also send the browser to the provider’s logout address.


Try loginos

loginos is an OpenID Connect provider that gives every domain of your product one shared sign-in:

  • Any number of domains, one user pool. Add them in the console or from your server, cover subdomains with a verified wildcard, or let your server approve new ones as people sign in.
  • Passkeys built in. Face ID, fingerprints and security keys, with passwords as a fallback, and no WebAuthn code in your app.
  • Roles and permissions in every token, the same on every domain.
  • Keep the users you have. People link their existing account once, and every token carries your own user id.
  • “Continue as” on your own page in Chrome and Edge, with a small loginos window everywhere else.
  • Standard OpenID Connect, so Auth.js, Passport or any OIDC library works. There are no paid plans today, and you can start without a card.

Create a platform in a few minutes, follow the quickstart, or read more answers like this one. This answer is kept up to date at loginos.io/answers.

Top comments (0)