Connect
Visitor sign-in
Verify who the visitor is on your platform before the AI calls any secure custom tool. Required for “What’s my balance?”, “Show my order”, “Cancel my subscription” — anything tied to a specific user account. Public tools (“track this package by tracking number”) work without sign-in.
Visitor sign-in is the foundation; the actual gating of which tools require it lives on each Custom tool (separate feature). You configure the verification here; you mark individual tools as “requires sign-in” on the Custom tools page.
When you need this
Two example flows side-by-side:
| Scenario | Sign-in required? |
|---|---|
| Visitor asks “where is order #ABC123?” (anyone can pass an order number) | No |
| Visitor asks “what’s my account balance?” (must be the right person) | Yes |
| Visitor asks “cancel my latest subscription” | Yes |
| Visitor asks “what are your business hours?” | No (knowledge-base answer) |
How it works (architecture)
- Visitor signs in on your platform via your existing flow (email/password, SSO, Google, whatever).
- Your platform produces a signed token proving who the visitor is — either a standard JWT signed with public-key crypto (recommended) or a JWT signed with a shared HMAC secret.
- Your page calls
ChatWidget.identify({ token })after sign-in. The widget attaches the token to every chat request. - Our chat backend verifies the token against the keys you registered on the Visitor sign-in config page.
- Verified identity is attached to the request context. Any custom tool marked “requires sign-in” can now run and gets the verified user id automatically.
We never store your visitor passwords. We only verify a token your auth system already issued.
Mode 1 — JWT + JWKS (recommended)
Your platform issues JWTs and exposes a JWKS endpoint with the public keys. We pull the keys, cache them for 15 minutes, and verify token signatures locally on every chat request.
Auth0 example
- In our dashboard: Integrations → Visitor sign-in → JWT + JWKS.
- JWKS URL:
https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json - Issuer (optional):
https://YOUR_DOMAIN.auth0.com/ - Audience (optional): the API identifier you set up in Auth0.
- Save.
On your page, after Auth0 login completes:
const auth0 = await createAuth0Client({ ... });
const token = await auth0.getTokenSilently();
ChatWidget.identify({ token });
Amazon Cognito example
Use the User Pool ID’s discovery doc — Cognito serves a standards-compliant JWKS at:
https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/jwks.json
Issuer: https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}.
Clerk / Firebase Auth / generic OIDC
Same pattern. Any provider that serves a JWKS works. Common endpoints:
- Clerk:
https://your-app.clerk.accounts.dev/.well-known/jwks.json - Firebase:
https://www.googleapis.com/robot/v1/metadata/x509/[email protected](different format — we accept it) - Generic OIDC: append
/.well-known/jwks.jsonto your issuer URL.
Key rotation
Keys cache for 15 min. After you rotate keys, click the “Refresh JWKS + verify” button on the Test card to force an immediate refresh. Production traffic refreshes automatically on the next chat request after the cache expires.
Mode 2 — HMAC shared secret
Simpler if you don’t already have public-key infrastructure. Your backend signs a JWT with HS256 + a shared secret we store encrypted at rest. Less recommended than JWT/JWKS only because a leaked secret is a one-step path to forged identities — rotate it if you suspect compromise.
Setup
- In our dashboard: Integrations → Visitor sign-in → HMAC shared secret.
- Click Generate to create a random 32-byte secret. Copy it to your backend’s secrets manager.
- Save.
Node example (sign in your backend)
Use the same jose library we use for verification:
import { SignJWT } from "jose";
const secret = new TextEncoder().encode(process.env.QUINCER_VISITOR_SECRET);
// Call this after your user signs in.
async function tokenForUser(user) {
return new SignJWT({
email: user.email,
name: user.name,
plan: user.plan,
})
.setProtectedHeader({ alg: "HS256" })
.setSubject(user.id) // → ends up as the canonical user id
.setIssuedAt()
.setExpirationTime("1h") // short-lived; rotate on refresh
.sign(secret);
}
Pass the token to the widget
On the page where your user lands after signing in:
<script>
// Server-rendered: include the token in your HTML response.
ChatWidget.identify({ token: "{{ JWT_FROM_BACKEND }}" });
</script>
Or fetch the token from your own /api/me/quincer-token
endpoint after sign-in.
Popup sign-in flow (optional)
If you want the widget itself to prompt the visitor to sign in when a secure tool is needed, register a Sign-in URL in the config. The widget opens this URL in a new tab when an unauthenticated visitor asks for a secure tool; your sign-in page posts the token back when login succeeds.
Your sign-in page
// after login completes:
const token = await getQuincerTokenForCurrentUser();
window.opener.postMessage(
{ type: "quincer-identify", token },
// Match the origin where the widget is embedded.
"https://your-customer-site.com"
);
window.close();
The widget verifies the message origin matches the configured Sign-in URL before accepting the token, so a random tab can’t inject one.
Claim mapping
The model only sees what you tell it to see. The verifier extracts
standard JWT claims by default (everything except iss/sub/aud/exp/iat/nbf/jti)
and passes them to tools that need identity. To rename or restrict
that, fill in the Claim mapping JSON. Example:
{
"userId": "sub",
"email": "email",
"tier": "https://acme.example/tier",
"accountId": "https://acme.example/account_id"
}
Now a custom tool that lists requiredClaims: ["userId", "tier"]
works as expected, even though the JWT uses Auth0’s namespaced
custom-claim convention.
Testing your setup
The config page has a built-in Test a token card:
- Sign in on your platform and grab a real token (browser dev tools, your backend, your auth provider’s dashboard).
- Paste it into the Test card and click Verify.
- You’ll see either a green “Verified” result with the resolved identity (this is what the AI tools see), or a clear failure reason and message.
Use this to iterate on the JWKS URL, the issuer/audience values, and the claim mapping without bouncing through your full sign-in flow.
Common failure reasons
| Reason | What it means | Fix |
|---|---|---|
missing-token |
No Authorization: Bearer header on the request. |
Call ChatWidget.identify({ token }) before the visitor sends a message. |
config-incomplete |
You picked a mode but didn’t finish setup. | JWT mode needs a JWKS URL; HMAC needs a secret. |
expired |
The token’s exp is past. |
Refresh the token in your sign-in flow and re-call identify. |
bad-signature |
Token signed with a different key than we have. | Rotated keys? Click Refresh JWKS + verify. HMAC mismatch? Re-copy the secret to your backend. |
bad-claim |
iss / aud enforced and the token doesn’t match; or no sub claim. |
Check the issuer/audience values, or stop enforcing them. Every token must have sub — we use it as the stable user id. |
jwks-fetch-failed |
We couldn’t reach the JWKS URL. | Confirm the URL is public (no auth required) and serves JSON. |
Security notes
- HMAC secrets are encrypted at rest with the same key-store the rest of your API keys use. We never echo them back in dashboard GETs.
- JWKS responses are cached 15 min — key rotation propagates automatically; the dashboard’s refresh button forces an immediate re-pull.
- The widget’s postMessage listener verifies the origin against the configured Sign-in URL before accepting an
identifymessage. - Identity is verified per chat request — a forged token can’t survive a single round-trip.
- Logs of verification failures are written to Railway’s console (no token contents, just the reason).
Your visitor session expiration is yours to control. We honor whatever
exp your JWT carries. Don’t issue 1-year tokens
— a leaked one is valid for that whole window.