Skip to content

Sign in with Configure

What it is. Configure as your account system. The user signs in on Configure's hosted flow, which the card also opens, and comes back to your app with a user token instead of an externalId. You never store a password and you never verify a phone or an email yourself; Configure did. The button carries your app's name, or a line like "Connect your context", not the word Configure.

When to use it. When your app has no accounts of its own and Configure's sign-in is the sign-in. Then the token is how you reach the profile, and everything else on the Quickstart is unchanged.

When not to. When you already have users. Then the onboarding trip does the same job on the profile you already key by your user id, and there is only ever one handle per user to keep. Two account systems for one person is the failure mode this section exists to prevent.

Setup already wrote what this needs: CONFIGURE_PUBLISHABLE_KEY (pk_ prefix, safe in the browser) and your secret key. Configure only sends a signed-in user back to a callback you registered for your agent, so register it once at boot:

The client is the same one the rest of the integration uses:

ts
import { Configure } from "configure";

export const configure = new Configure({
  apiKey: process.env.CONFIGURE_API_KEY,   // sk_..., server only
  agent: process.env.CONFIGURE_AGENT,      // your agent handle
});

export const profileFor = (userId: string) => configure.profile({ externalId: userId });
ts
import { randomUUID } from "node:crypto";

const RETURN_TO = "https://yourapp.com/callback";   // a constant
await configure.auth.allowSignInReturnTo(RETURN_TO); // once at boot; idempotent

// GET /login
app.get("/login", (req, res) => {
  const state = randomUUID();
  res.cookie("configure_state", state, { httpOnly: true, sameSite: "lax", maxAge: 600_000 });
  res.redirect(
    configure.auth.signInUrl({
      publishableKey: process.env.CONFIGURE_PUBLISHABLE_KEY,
      returnTo: RETURN_TO,
      state,
      displayName: "Your App",
    }),
  );
});

// GET /callback
app.get("/callback", async (req, res) => {
  res.clearCookie("configure_state");
  if (!req.query.state || req.query.state !== req.cookies.configure_state) {
    return res.status(400).send("Invalid sign-in state.");
  }
  const { token, userId } = await configure.auth.exchangeSignInCode(String(req.query.code));
  await sessions.set(req, { userId, token });   // this user's key to their profile
  res.redirect("/");
});

Three things keep this correct, and they are the same three every redirect sign-in needs:

  • state is a one-time value bound to the browser. Mint it per attempt, keep it in an httpOnly cookie, compare on return, and clear it. That is the CSRF protection for the callback.
  • The code is single use. Exchange it once, server-side, with your secret key. A refresh or a Back button replays the URL; the second exchange fails, so treat that failure as "already signed in", not as an error to retry.
  • returnTo is a constant. Never build it from the request. Configure refuses a destination you did not register, and an app that forwards to a URL from a query parameter is an open redirector.

The token is an agent credential for this user, valid for 24 hours. Keep it in the session, use it for the daily refresh, the first-turn injection, the commit, and the tools with configure.profile({ token }) in place of configure.profile({ externalId }), and send the user through /login again when it expires.

Prefer one call? personalize() from the same package wires /login, /callback, and the boot registration for you:

ts
import { personalize } from "configure";

personalize({
  apiKey: process.env.CONFIGURE_API_KEY,
  publishableKey: process.env.CONFIGURE_PUBLISHABLE_KEY,
  agent: process.env.CONFIGURE_AGENT,
  baseUrl: "https://yourapp.com",          // callback becomes baseUrl + "/callback"
  onSignedIn: ({ userId, token, profile }) => sessions.create({ userId, token }),
}).listen(3000);

Three registrations exist and they are not the same thing. allowSignInReturnTo registers where a sign-in may return. npx configure add origin registers your OAuth client's callback. allowed_return_hosts on your developer account is the only one the connect return consults, and you need it only when the browser cannot vouch for you.

The drop-in button

The flow above is yours to wire. If you would rather paste a button, Configure ships one, and it is the same grant underneath.

html
<script src="https://configure.dev/js/configure.js"></script>

<configure-sso-button
  client-id="oc_your_client_id"
  redirect-uri="https://yourapp.com/auth/configure/callback"
  agent-name="Your App"
  scopes="profile.read profile.search profile.remember profile.commit"
  width="100%"
></configure-sso-button>

The script mounts it on load, so there is nothing to call. Configure.ssoButton({ el, clientId, redirectUri, label, connectedLabel, connected, size, width, theme, font, onEvent }) is the same button from JavaScript when you need to mount it yourself, and data-configure-sso on a plain element mounts it too. Leave out clientId or redirectUri and the button renders a setup notice instead of a broken control.

This button uses a different registration from the redirect flow above. client-id is your OAuth client, oc_ prefix, and redirect-uri is a callback registered on it with npx configure add origin <full callback URL>. allowSignInReturnTo plays no part here. Getting these two crossed is the most common way this stops working.

Sign in without leaving the page

Configure.signInWithPopup() opens the same flow in a popup and hands the result back to the opener, so a user who is already signed in to Configure never leaves your page.

html
<script src="https://configure.dev/js/configure.js"></script>
<script>
  Configure.signInWithPopup({
    onEvent: (event) => {
      if (event.type === "configure:sso-authenticated") exchangeOnYourServer(event.payload);
      if (event.type === "configure:sso-error") showSignInFailed(event.payload.message);
    },
  });
</script>

It also accepts popupName, popupFeatures, and fallback for the case where the browser blocks the popup. The surface announces configure:sso-start, configure:sso-open, configure:sso-authenticated, configure:sso-error and configure:sso-close.

The popup returns an authorization code, not a token. Your callback page reads the PKCE verifier that the button stored, posts the code to your own server to exchange it, and tells the opener how it went:

html
<script src="https://configure.dev/js/configure.js"></script>
<script type="module">
  const params = new URLSearchParams(location.search);
  const state = params.get("state");
  const pkce = Configure.getSsoPkce(state);          // stored by the button, per state

  if (params.get("error") || !pkce?.codeVerifier) {
    Configure.completeSso({ error: params.get("error") || "pkce_missing", payload: {} });
  } else {
    const res = await fetch("/auth/configure/exchange", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ code: params.get("code"), codeVerifier: pkce.codeVerifier, state }),
    });
    Configure.completeSso(res.ok ? { state, payload: await res.json() } : { error: "exchange_failed", payload: {} });
  }
</script>

completeSso() posts the result to the opener, clears the stored verifier, and closes the window. The exchange itself stays on your server with your secret key, for the same reason the redirect flow exchanges there: the code is single use and a browser must never hold the token-minting credential.

Where this sits

Sign in with Configure replaces your account system. If you already have one, you do not need this page: keep your own user ids and send the user on the onboarding trip instead, which reaches the same profile. The rest of an integration is the same either way, and it is on the Quickstart.

Personalization infrastructure for agents