loginos

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

loginos teamUpdated 7 min read

Short answer

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:

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 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 and keep its app secret on your server. The page with the prompt must be on one of your platform’s domains. While you build, add localhost:3001 as a domain.

2. Load the SDK on your page

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.

your-app.example
Sign in

Sign in to your-app.example with loginos.io

J

Jane Doe

jane@example.com

Continue as Jane
1/4Someone who is signed in to loginos opens a page on your site.

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:

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 show the same route in Next.js.

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:

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:

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:

BrowserOn page load (prompt())From your button (signIn())
Chrome and EdgeThe browser’s prompt, for people signed in to the providerThe browser’s prompt, after the provider’s sign-in window if needed
Safari and FirefoxNothing: it resolves nullA 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 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.

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