Skip to content

Sign in with Configure ​

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

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

When not to. Do not use it when you already have users. The onboarding trip does the same job on the profile you already key by your user id, so you keep one handle per user. Do not give one person two account systems.

Setup already created 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. You register it once at boot, in server.ts below.

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) => {
  const expected = req.cookies.configure_state;
  res.clearCookie("configure_state");   // one attempt per state, pass or fail
  if (!req.query.state || req.query.state !== expected) {
    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("/");
});

Every redirect sign-in needs these three things:

  • 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 the Back button replays the URL, and the second exchange fails. 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. An app that forwards to a URL from a query parameter is an open redirector.

Need a verified email? Use OpenID Connect with the email scope. The ID token then carries email and email_verified. required=gmail|outlook on the authorize request puts the mailbox page first. What the hosted flow asks for lists every screen and its order for each way in.

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

To do all of this in one call, use personalize() from the same package. It 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);

There are three separate registrations:

  • 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. You need it only when the browser cannot vouch for you.

The drop-in button ​

You wire the flow above yourself. If you would rather paste a button, Configure ships one. It uses 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 the button on load, so you do not call anything. To mount it yourself from JavaScript, use Configure.ssoButton({ el, clientId, redirectUri, label, connectedLabel, connected, size, width, theme, font, onEvent }). data-configure-sso on a plain element also mounts it. If you leave out clientId or redirectUri, 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, with the oc_ prefix. redirect-uri is a callback registered on that client with npx configure add origin <full callback URL>. allowSignInReturnTo plays no part here. Mixing up these two registrations is the most common reason this stops working.

The button opens Configure in a popup and returns an OAuth authorization code, not a token. It emits configure:sso-start, configure:sso-open, configure:sso-authenticated, configure:sso-error, and configure:sso-close. Your callback page reads the PKCE verifier the button stored. It posts the code to your own server to exchange it. Then it tells the opener the result:

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 stays on your server with your secret key, as in the redirect flow. The code is single use, and a browser must never hold the credential that mints tokens.

If the authorization request omits scope, Configure uses the scopes registered for that client. A client made with POST /v1/sso/client registers openid, email, and profile. If the account has no proven mailbox, its sign-in asks for one after the code. The OpenID Connect page covers scopes and claims.

Sign in without leaving the page ​

Configure.signInWithPopup() opens the same sign-in in a popup and passes a sign-in code back to your page. Your server exchanges it the same way /callback does. The user never leaves your page.

html
<script src="https://configure.dev/js/configure.js"></script>
<script>
  document.getElementById("sign-in").addEventListener("click", () => {
    const state = crypto.randomUUID();
    Configure.signInWithPopup({
      publishableKey: "pk_...",
      agent: "your-agent",
      returnTo: "https://yourapp.com/callback",   // registered with allowSignInReturnTo, as above
      state,
      onEvent: async (event) => {
        if (event.type === "configure:sign_in_code" && event.payload.state === state) {
          await fetch("/auth/configure/exchange", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ code: event.payload.code }),
          });
          location.reload();
        }
        if (event.type === "configure:error") showSignInFailed(event.payload.message);   // popup_blocked
      },
    });
  });
</script>

publishableKey, agent, and returnTo are required. If one is missing, the call throws. The page emits two events:

  • configure:sign_in_code with { code, state, returnTo, agent }.
  • configure:error when the browser blocks the popup.

Pass fallback: "redirect" to send the whole page to the sign-in when the popup is blocked. popupName and popupFeatures set the window.

ts
// POST /auth/configure/exchange: the same exchange as /callback, with your secret key.
app.post("/auth/configure/exchange", async (req, res) => {
  const { token, userId } = await configure.auth.exchangeSignInCode(String(req.body.code));
  await sessions.set(req, { userId, token });
  res.sendStatus(204);
});

The code is single use. The exchange stays on your server, as in the redirect flow, because a browser must never hold the credential that mints tokens.

Where this sits ​

Sign in with Configure replaces your account system. If you already have one and want Configure as a sign-in option inside it, use OpenID Connect. If you only want the user's context, you need neither page. Keep your own user ids and send the user on the onboarding trip, which reaches the same profile. The rest of the integration is the same either way. It is on the Quickstart.

Personalization infrastructure for agents