# How do I add a one-tap “Continue as” login to my site?

> Use FedCM, the browser API behind “Continue as” prompts, with an identity provider that supports it: your page asks on load or from a button, Chrome and Edge show the prompt, and your server checks what comes back. Safari and Firefox need a popup instead. The loginos SDK does both and gives your page a one-time code for your server to exchange.

You have seen it on other sites: a page opens, and the browser itself asks “Continue as Jane?”. One tap, and you are signed in without leaving the page. That prompt comes from FedCM (Federated Credential Management), a browser API, and you can show it on your own site.

You need three things: an identity provider that supports FedCM, a few lines on your page, and a server route that turns the result into your own session. Safari and Firefox don’t support FedCM, so they need a fallback, usually a small popup window.

## How does a “Continue as” prompt work?

A “Continue as” prompt is drawn by the browser, not by your page. When your page asks for one, five things happen:

1. Your page calls `navigator.credentials.get()` with the identity provider’s config URL and your client ID.
2. The browser asks the provider which accounts are signed in, without telling it which site is asking.
3. The browser shows “Continue as Jane” over your page. Your page sees nothing yet.
4. When Jane continues, the browser asks the provider for a token for your site and hands it to your page.
5. Your server checks the token and starts its own session.

You can ask in two ways. A call on page load (passive mode) shows the prompt only to people already signed in to the provider. A call from a click (active mode) also works for people who aren’t, because the browser can open the provider’s sign-in window first. Chrome and Edge support both. In Safari and Firefox your button opens a popup window instead, which hands the result back to your page.

## What would you build with raw FedCM?

With raw FedCM you build both sides. If you run your own identity provider, it has to serve:

- a `/.well-known/web-identity` file at the root of its registrable domain, pointing to its config file;
- a config file that lists the other endpoints and a sign-in page;
- an accounts endpoint that reads the provider’s session cookie (`SameSite=None`) and lists the signed-in accounts;
- an ID assertion endpoint that checks the page’s `Origin` against the client ID and returns a token with CORS headers;
- a `Set-Login: logged-in` or `logged-out` header whenever people sign in or out.

Your page then makes the call, and handles browsers without FedCM:

```js title="Raw FedCM"
signInButton.onclick = async () => {
  if (!("IdentityCredential" in window)) return openPopupSignIn(); // Safari, Firefox
  const credential = await navigator.credentials
    .get({
      identity: {
        mode: "active", // from a click; leave it out to prompt on page load
        providers: [{
          configURL: "https://idp.example/fedcm/config.json",
          clientId: "YOUR_CLIENT_ID",
          params: { nonce }, // a random value your server made for this attempt
        }],
      },
      mediation: "required",
    })
    .catch(() => null); // closed, or no account
  if (credential) await sendToServer(credential.token); // your server validates it
};
```

Add `navigator.credentials.preventSilentAccess()` on sign-out, token checks on your server, and a popup flow with `postMessage` for the other browsers. That is most of what an SDK and a hosted provider save you. [How does FedCM work](https://loginos.io/answers/how-fedcm-works.md) covers each endpoint in detail.

## How loginos does it

loginos is a hosted identity provider that supports FedCM, and its browser SDK is the page side. In Chrome and Edge it uses the browser’s prompt; elsewhere it opens a small loginos window that closes itself after sign-in. Either way, your page gets a one-time code, the same kind as the redirect flow, and your server exchanges it.

### 1. Create a platform and add your domains

Create a platform in the [developer console](https://loginos.io/developer) and keep its app secret on your server. The page with the prompt must be on one of your platform’s [domains](https://loginos.io/docs/domains.md). While you build, add `localhost:3001` as a domain.

### 2. Load the SDK on your page

```html title="HTML"
<button id="sign-in">Sign in with loginos</button>

<script src="https://loginos.io/sdk/v1.js"></script>
<script>
  const loginos = Loginos.init({ appKey: "YOUR_APP_KEY" });

  // Page load: Chrome and Edge ask "Continue as …?" when the visitor is signed in to loginos.
  // Resolves null in other browsers, or when the visitor closes the prompt.
  loginos.prompt().then(finish);

  // A click: the browser's prompt where there is one, a small loginos window elsewhere.
  document.querySelector("#sign-in").onclick = () => loginos.signIn().then(finish);

  async function finish(result) {
    if (!result) return; // dismissed, or not signed in to loginos
    const res = await fetch("/api/auth/login/single/token", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(result),
    });
    if (res.ok) location.reload();
  }
</script>
```

Call `signIn()` directly in the click handler, not after an `await`. The window needs the click’s user gesture, or the popup blocker stops it. Show your button without waiting for `prompt()`, too: when nobody is signed in to loginos, browsers may delay the `null` on purpose, so your page can’t tell.

### 3. Exchange the code on your server

The exchange is the same as for a callback code, without `redirect_uri` and without `code_verifier`. Then check that the ID token carries the nonce your page got:

```js title="Express"
app.post("/api/auth/login/single/token", express.json(), async (req, res) => {
  const { code, nonce } = req.body ?? {};
  if (typeof code !== "string" || typeof nonce !== "string") return res.status(400).send("Bad request.");

  const r = await fetch("https://loginos.io/oauth/token", {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code, // SDK codes: no redirect_uri, no code_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, id_token } = await r.json();

  // The token came straight from loginos over TLS; the nonce ties it to this page's request.
  const claims = JSON.parse(Buffer.from(id_token.split(".")[1], "base64url").toString());
  if (claims.nonce !== nonce) return res.status(401).send("Sign-in failed.");

  req.session.user = user; // your own session: find your user by user.sub
  res.sendStatus(204);
});
```

Codes work once and expire after five minutes. The `user` object carries `sub`, `preferred_username`, `roles` and `permissions`, as after a redirect. The [Continue as docs](https://loginos.io/docs/continue-as.md) show the same route in Next.js.

> [!TIP]
> To bind the sign-in to the visitor’s browser, create the nonce on your server. Keep it in an HttpOnly cookie, put it in the page, pass it as `Loginos.init({ appKey, nonce })`, and compare the ID token’s `nonce` with the cookie instead of the request body. A code obtained in another browser then fails the check.

### 4. Sign people out

Call `signOut()` before you clear your own session. Otherwise Chrome and Edge may sign a returning member straight back in on the next page load:

```js title="Sign out"
document.querySelector("#sign-out").onclick = async () => {
  await loginos.signOut(); // the browser won't sign them in by itself next time
  await fetch("/api/auth/logout", { method: "POST" }); // your own session
  location.reload();
};
```

To ask every time, even for returning members, pass `auto: false`, as in `Loginos.init({ appKey: "YOUR_APP_KEY", auto: false })`. `disconnect()` goes further: in browsers with the prompt, it removes your site’s access from the visitor’s loginos account.

## Which headers break the sign-in window?

The popup fallback needs a link between your page and the window it opens, because the window hands the code back with `postMessage`. The `Cross-Origin-Opener-Policy: same-origin` header cuts that link. The browser opens the window in a separate browsing context group, the window can’t reach your page, and the sign-in never finishes. Send `same-origin-allow-popups` instead, or no header at all. Google documents the same rule for its sign-in popups.

You only see this problem in Safari and Firefox, where the window replaces the prompt. Check your framework and middleware. Helmet, the Express security middleware, sends `same-origin` by default, and its default Content Security Policy blocks the SDK script. FedCM also checks the config URL against `connect-src`, so allow the loginos origin there:

```js title="Express with Helmet"
app.use(
  helmet({
    crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" },
    contentSecurityPolicy: {
      directives: {
        "script-src": ["'self'", "https://loginos.io"], // the SDK
        "connect-src": ["'self'", "https://loginos.io"], // FedCM's config URL
      },
    },
  }),
);
```

Helmet’s policy also blocks inline scripts, so move the sign-in code into a script file of your own.

## How do you test “Continue as” in Chrome, Safari and Firefox?

Test both paths, because each browser family takes a different one:

| Browser | On page load (`prompt()`) | From your button (`signIn()`) |
| --- | --- | --- |
| Chrome and Edge | The browser’s prompt, for people signed in to the provider | The browser’s prompt, after the provider’s sign-in window if needed |
| Safari and Firefox | Nothing: it resolves `null` | A small sign-in window |

In Chrome and Edge:

- Sign in to the identity provider in the same browser profile first. The page-load prompt only appears while that session exists.
- After you continue once, the browser may sign you in by itself on later visits (`auto: true` in the result). Chrome does this at most once every 10 minutes. Sign out with `signOut()` to see the prompt again.
- If you close the prompt, Chrome holds it back on that site: for two hours, then a day, a week and four weeks. Reset it under **Third-party sign-in** in the site settings next to the address bar.
- Browsers treat `localhost` as a secure context, so FedCM works on `http://localhost:3001` once that domain is on your platform.

In Safari and Firefox:

- `canPrompt` is `false`, and `prompt()` resolves `null` at once.
- Your button opens the sign-in window. If the window doesn’t open, `signIn()` probably runs after an `await`. If it opens but your page never gets the code, look for `Cross-Origin-Opener-Policy: same-origin`.

## Frequently asked questions

### Why doesn’t the prompt appear when my page loads?

The page-load prompt only appears in Chrome and Edge, and only for people signed in to the identity provider in that browser. It also stays hidden for a while after someone closes it, and people can turn off third-party sign-in in Chrome’s settings. If the prompt appears but continuing fails, check that the page’s host is one of your platform’s domains.

### Does a “Continue as” login need third-party cookies?

No. FedCM was built for browsers that block third-party cookies: the browser sends the provider’s cookie only to the provider’s own FedCM endpoints. The popup fallback is a normal window on the provider’s site, so its cookies are first-party there.

### Does my page learn who the visitor is before they continue?

No. The browser shows the prompt and keeps the account list to itself. Your page gets a result only after the visitor continues, and `null` otherwise. Browsers may even delay that `null`, so a page can’t tell whether the visitor has an account at all.

### Can I change how the prompt looks?

Only a little, because the browser draws it. The `context` option changes the wording to `signin` (the default), `signup`, `use` or `continue`, and `loginHint` preselects an account by email or username. Your own button can look however you like.

### Do I still need the redirect flow?

For most visitors, no: every browser gets either the prompt or the window. Keep the [redirect flow](https://loginos.io/docs/quickstart.md) as a backup where popups can’t open, or when a sign-in starts from a link. Both kinds of code are exchanged at the same endpoint.

---

Updated 2026-10-14. Web page: https://loginos.io/answers/continue-as-one-tap-login
