loginos

How do I share a login session across multiple domains?

loginos teamUpdated 6 min read

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.

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.

ApproachWorks across different domains?What you build
One cookie for the parent domainNo, only for subdomains like app.example.com and shop.example.comNothing extra: set Domain=example.com on the session cookie
A central login (identity provider)Yes, any number of domainsA redirect route and a callback route on each site
Hand a token from one site to the otherYes, but you secure it yourselfOne-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.

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.

Browser
Your server
loginos
1/6Your site sends the browser to loginos with your app key, a state and a PKCE challenge.

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>.

Express
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("/");
});

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.

One account for every domain you run

loginos is an OpenID Connect provider with passkeys, a user pool shared by all of your domains, roles in every token, and account linking for the users you already have. Add your platform and its domains in a few minutes.

Keep reading

All answers