# How can users log in once and stay logged in on all my websites?

> Keep one long-lived session at a central login, an OpenID Connect provider, and a separate session on each of your websites. When a site has no session, it asks the provider silently with prompt=none, and people who are signed in there come straight back without a form. Each site renews its own session this way, so people stay logged in everywhere until the central session ends.

You run several websites and want people to sign in once, then stay signed in on all of them for days. No second sign-in form, and no “Sign in” button that flashes before the page knows who they are.

Browsers never share a cookie between two domains, so this always starts with a central login that all of your sites trust. [How do I share a login session across multiple domains?](https://loginos.io/answers/share-login-session-across-domains.md) covers that part. This answer covers what comes after the first sign-in.

## How does “log in once, stay logged in everywhere” work?

Staying logged in everywhere rests on two kinds of session, kept in two places. The **central session** belongs to the identity provider, the service where people actually sign in. A **site session** belongs to one of your websites, which creates it after a sign-in.

| | Central session | Site session |
| --- | --- | --- |
| Lives on | The provider’s domain | Each of your domains |
| Starts when | The person signs in at the provider | The site exchanges a sign-in code |
| Length set by | The provider | You, for each site |
| Ends with | A sign-out there, or expiry | A sign-out on that site, expiry, or your server |

A site session answers “who is this?” on every request, without a network call. The central session lets a site get a sign-in without asking. When a site session is missing or has ended, the site renews it from the central session with one redirect. So people stay logged in everywhere for as long as the central session lasts.

## How do you sign people in silently when a page loads?

OpenID Connect has a parameter for this check: `prompt=none`. With it, the provider must not show any page ([OpenID Connect Core, section 3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)). It sends the browser straight back, with a code if the person is signed in and has approved your site, or with an error such as `login_required`.

Three rules keep the check invisible:

1. **Check before you render.** Decide on the server, before any HTML goes out. Someone who comes back with a code gets a page that already shows their name, so no “Sign in” button flashes and changes.
2. **Use a top-level redirect, not a hidden iframe.** Inside an iframe, the provider’s cookie is a third-party cookie. Safari blocks those and Firefox partitions them, so the iframe never sees the central session. [How do I share authentication cookies between two domains?](https://loginos.io/answers/share-cookies-between-domains.md) explains why.
3. **Check once, then remember the answer.** Set a cookie before the redirect that says the check ran, so a `login_required` can’t start a loop.

Run the check only on real page loads: `GET` requests with `Sec-Fetch-Dest: document`, not API calls or images. Skip crawlers too. Googlebot, for one, clears cookies between page loads, so it would be redirected on every visit.

```js title="Express"
// After cookie-parser and express-session, before your pages.
import crypto from "node:crypto";

const LOGINOS = "https://loginos.io";
const CALLBACK = "/api/auth/login/single/callback";
const b64url = (b) => b.toString("base64url");
const opts = { httpOnly: true, secure: true, sameSite: "lax" };

// One silent check per browser session, before the page renders.
app.use((req, res, next) => {
  const isPage = req.method === "GET" && req.get("sec-fetch-dest") === "document";
  const isBot = /bot|crawl|spider/i.test(req.get("user-agent") ?? "");
  if (!isPage || isBot || req.path === CALLBACK || req.session.user || req.cookies.lo_checked) return next();

  const state = b64url(crypto.randomBytes(16));
  const verifier = b64url(crypto.randomBytes(32));
  res.cookie("lo_checked", "1", opts); // no maxAge: kept until the browser closes
  res.cookie("lo_flow", { state, verifier, back: req.originalUrl }, { ...opts, maxAge: 600_000 });
  const params = new URLSearchParams({
    client_id: process.env.LOGINOS_APP_KEY,
    redirect_uri: `https://${req.get("host")}${CALLBACK}`,
    response_type: "code",
    scope: "openid profile email",
    prompt: "none", // never show a page: a code, or login_required / consent_required
    state,
    code_challenge: b64url(crypto.createHash("sha256").update(verifier).digest()),
    code_challenge_method: "S256",
  });
  res.redirect(`${LOGINOS}/oauth/authorize?${params}`);
});

// The callback handles silent checks and normal sign-ins alike.
app.get(CALLBACK, async (req, res) => {
  const flow = req.cookies.lo_flow ?? {};
  res.clearCookie("lo_flow", opts);
  if (!flow.state || req.query.state !== flow.state) return res.status(400).send("Sign-in expired, try again.");
  const back = /^\/(?![\/\\])/.test(flow.back ?? "") ? flow.back : "/";
  if (req.query.error) return res.redirect(back); // login_required or consent_required: show the page signed out

  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")}${CALLBACK}`,
      code_verifier: flow.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();
  req.session.regenerate((err) => {
    if (err) return res.status(500).send("Sign-in failed.");
    req.session.user = user; // this domain's own session
    res.redirect(back);
  });
});
```

Pages that require sign-in don’t need `prompt=none`. Redirect as usual: anyone with a central session comes straight back, and everyone else needs the form anyway.

## How long should each session last?

The provider sets the length of the central session. You set each site session, and silent sign-in lets you keep those short: when one ends, the next page load renews it without a form.

Short site sessions also pick up changes faster. Each renewal is a fresh sign-in, so a sign-out at the provider, a blocked account or new roles reach the site within one session length. For sensitive areas, OWASP’s [Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) suggests idle timeouts of 2 to 5 minutes for high-value applications and 15 to 30 minutes for low-risk ones. It also suggests an absolute limit, like 4 to 8 hours for all-day use.

| What ended | What the person sees |
| --- | --- |
| A site session, while the central session lives | Nothing: the next page load renews it silently |
| The central session, while a site session lives | Still signed in on that site until its session ends, then signed out |

**“Remember me”** means less with single sign-on. A checkbox on your site only decides whether the site’s cookie outlives the browser, and the next silent sign-in renews it anyway. The provider’s session is what really remembers people, so on a shared computer they need a full sign-out.

Browsers add limits of their own. Chrome caps a cookie’s lifetime at 400 days, since Chrome 104. Safari deletes cookies created by JavaScript after 7 days without user interaction on your site, so set session cookies from your server with `Set-Cookie` and `HttpOnly`.

## How do you sign out of one site, or of all of them?

**Signing out of one site** means deleting that site’s session and its cookie. Watch for one trap: with a silent check in place, the next page load signs the person straight back in, because the central session is still there. After a sign-out, turn the check off for that browser with a long-lived cookie until the person chooses “Sign in” again.

**Signing out everywhere** takes two more steps:

1. Send the browser to the provider’s logout address, the `end_session_endpoint` in its discovery document. That ends the central session, so no site can renew from it.
2. End the site sessions on your other domains, since the site where the person clicked can’t touch their cookies. If your sites share a database, keep sessions on the server and delete every session for that person’s `sub`. With a few domains, you can also chain redirects through each one’s sign-out route. Otherwise, short site sessions end on their own, because silent renewal now returns `login_required`.

OpenID Connect also defines notifications from the provider. [Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) calls each site’s server directly. Front-Channel Logout loads each site’s logout page in an iframe, which third-party cookie blocking often breaks, as that spec itself warns.

## How do you show the right state on every site?

- **Render from your own session on the server.** Keep the person’s name and picture in the session, and the first HTML is already right.
- **Choose the first visit on purpose.** Check before rendering where the signed-in view matters. Elsewhere, render the signed-out page with a one-click sign-in.
- **Keep personal pages out of shared caches.** Send `Cache-Control: private` on pages that show who is signed in, so a CDN never serves them to someone else.
- **Tell your other tabs.** A `BroadcastChannel` message can make a site’s open tabs reload after a sign-in or sign-out. It only reaches the same origin, so your other domains update on their next page load.

## How loginos does it

loginos is the central login for all of your domains. They share one pool of people, and someone who has used your platform before comes straight back on any of them, without a form.

- **Silent checks** work as in the example above. `prompt=none` returns `login_required` when nobody is signed in to loginos. It returns `consent_required` when the person hasn’t approved your platform yet: show them your “Sign in” button, and they approve once. See the [authorization parameters](https://loginos.io/docs/oidc.md#get-oauth-authorize).
- **No redirect in Chrome and Edge.** The [“Continue as” SDK](https://loginos.io/docs/continue-as.md) shows the browser’s own prompt on page load. The browser can even sign returning members in by itself (`auto: true`), and your page learns nothing until a visitor continues. Other browsers get `null` from `prompt()`, so keep the redirect check for them.
- **Your session, your length.** ID tokens and access tokens last one hour, and there are no refresh tokens. Keep your own session, and renew it with a silent sign-in, which also returns the person’s current roles.
- **Sign-out.** Clear your session and call the SDK’s `signOut()`, so the browser doesn’t sign the person straight back in. Then send the browser to `https://loginos.io/oauth/logout` with your `client_id` and a `post_logout_redirect_uri` on one of your domains. loginos doesn’t notify your other domains, so end their sessions yourself.

The [quickstart](https://loginos.io/docs/quickstart.md) has complete sign-in and sign-out routes for Next.js, Express and PHP.

## Frequently asked questions

### Can a site check whether someone is logged in on my other domain without a redirect?

Not with cookies. The other domain’s cookie would be a third-party cookie on your page, which Safari blocks and Firefox partitions. Use a top-level redirect with `prompt=none`, a popup after a click, or FedCM, the browser API behind “Continue as” prompts in Chrome and Edge.

### Why does silent sign-in fail in Safari but work in Chrome?

Usually because it runs in a hidden iframe, where the provider’s session cookie is a third-party cookie. Safari has blocked all of those by default since Safari 13.1 in 2020. Chrome still allows them unless the person turns them off. A top-level redirect with `prompt=none` works in every browser.

### Should I use refresh tokens to keep users logged in?

Not for a website session. A refresh token lets your server keep calling an API for the person after the access token expires. To know who is signed in on your site, your own session cookie is enough, and a silent sign-in renews it. loginos doesn’t issue refresh tokens at all.

### Does signing out of one site sign people out of the others?

Not by itself. Signing out clears the session of the site you are on. To sign out everywhere, that site also ends the central session at the provider. Your other sites end theirs through a shared session store, or on their next silent renewal.

---

Updated 2026-10-14. Web page: https://loginos.io/answers/stay-logged-in-across-websites
