Visitor authentication
Prove who a visitor is with a signed token, and let your agent call your own APIs on their behalf.
Visitor authentication
By default, when your page tells Lyro "this visitor is [email protected]", that is a claim from a browser and nothing more. Anyone can open devtools and say the same thing about someone else. That is fine for personalising a greeting and not fine for showing an order history.
This page covers the two mechanisms that fix it, and how they differ.
| What it answers | Who checks it | |
|---|---|---|
Signed identity (user_jwt) | Who the visitor is | Lyro |
Auth tokens (setAuthTokens) | A credential to call your backend as that visitor | Your backend |
API keys (krns_pk_) | Who your application is | Lyro - see API and developers |
They are independent. You can use either, both, or neither.
Signed identity
A signed token says who the visitor is, and Lyro verifies the signature before believing it. A verified identity is what makes an email trustworthy enough to look up an account against.
Available on the two distributions with a frontend of your own - the chat widget and the API (krns_pk_). Other channels already carry a provable identity from the provider.
1. Choose how visitors are verified
Open your widget or API distribution and go to the Security tab. Two options:
| Shared secret (HS256) | Login provider (RS256) | |
|---|---|---|
| Who signs | Your backend | Your login provider - Cognito, Auth0, Okta, Entra, Keycloak, Firebase, Clerk |
| What you configure | One shared secret | The provider's issuer URL and client ID |
| Lyro stores | The secret | Nothing - keys are fetched from the provider |
| Backend work | A signing endpoint | None: the page forwards the token your provider already issued |
| Rotation | Instant, from the dashboard | Your provider rotates; Lyro follows |
Shared secret is the default and the simplest when you do not run a login provider. Both sides hold the same value, which means the secret can sign as well as verify - so treat it like a password. Lyro can generate one, shown exactly once, or you can supply your own from your KMS.
Login provider is the right choice when your users already sign in through an OpenID Connect provider. Enter the issuer and the client ID from the provider's console, press Connect, and Lyro confirms it can reach the provider before saving. From then on the ID token your provider issues is the visitor's identity: Lyro checks its signature against the provider's published keys and accepts it only for that issuer and that client. Because there is no key to leak on either side, connecting a provider also switches Require a signed identity on and keeps it on.
Some providers do not fit. Supabase Auth signs with a shared secret by default, so it stays on the shared-secret path. Providers that issue only opaque tokens cannot be connected.
2a. Login provider: pass the provider's token
Nothing to sign. Hand the widget the ID token, as a getter so it is re-read before every request - provider tokens are short-lived and your site refreshes them:
// The token lives in a cookie your page can read:
lyro('boot', {
user_jwt: () => document.cookie.match(/(?:^|;\s*)id_token=([^;]+)/)?.[1],
});
// Or your login SDK holds it:
lyro('boot', { user_jwt: () => auth.getIdToken() });On the API distribution, send the ID token as user_jwt on POST /api/chat.
Lyro takes the visitor's id from the token's sub claim and the email only when the provider marks it email_verified. Other claims are ignored unless you list them under Import claims; listed ones become verified properties with any punctuation in their names replaced by underscores, so custom:plan is {{user.claims.custom_plan}}.
A token that has expired makes the visitor anonymous for that request - the chat keeps working without account data. The widget skips a token it can see is expired and fires a lyro('on', 'identity_expired', cb) event once, in case your page wants to refresh. If a different account logs in on your site, the widget starts a fresh session for the new person rather than mixing the two.
2b. Shared secret: sign a token
The claims:
// BACKEND ONLY - never expose the key to the browser.
import jwt from "jsonwebtoken";
const user_jwt = jwt.sign(
{
user_id: user.id, // required
email: user.email, // optional
plan: user.plan, // any extra key becomes a verified property
},
process.env.LYRO_IDENTITY_SECRET,
{ algorithm: "HS256", expiresIn: "10m" },
);user_idis required. Use your own stable user id - it is the anchor Lyro resolves the contact against.emailis optional and, when present, is recorded as a verified email identity.- Any other claim becomes a verified property, usable in agent instructions and custom tools.
- Choose an expiry. Lyro accepts a token without one, but a token that never expires is a permanent assertion sitting in your frontend. Match it to your session length.
Sign on your server, never in the browser. The dashboard's Security tab has copy-paste samples for Node, PHP and Python.
3. Pass it to Lyro
On the widget:
lyro('boot', { user_jwt: '<signed token>' });
lyro('update', { user_jwt: '<refreshed token>' }); // after you refresh the sessionOn the API distribution, send it as the user_jwt field on POST /api/chat.
Re-supply it whenever you refresh the session, or pass a getter (user_jwt: () => …) and the widget re-reads it before every request. The token is held in memory and Lyro does not renew it. If it expires mid-conversation the visitor is quietly treated as unverified - the chat keeps working, but verified properties stop resolving, which reads as a quality problem rather than an error.
4. Require it (optional)
The Require a signed identity toggle rejects unsigned identity claims, so your page can no longer assert an email without proof. Anonymous visitors can still chat in either setting - they simply have no identity attached.
A valid signed token is honoured whether the toggle is on or off. The toggle governs only what happens to unsigned claims. With a login provider connected it is always on: an identity your site claims without the provider's token would be exactly the impersonation the provider prevents.
Checking a token
Check a token on the Security tab verifies a token against your installed key and shows what Lyro extracts from it. Use it before wiring anything up - it turns a silent "properties are empty" into a specific reason.
| Result | Usually means |
|---|---|
| Verified | Working. The panel lists the identity and claims. |
| Invalid signature | The token was signed with a different key than the one installed, or by a key the provider no longer publishes. |
| Wrong algorithm | The token's algorithm does not match the option selected on this tab. |
| Expired | Working, but the token has aged out - check how long yours live. |
| Missing user_id | The required claim (user_id, or sub for a provider) is absent or empty. |
| Wrong issuer / audience | A provider token from a different pool or app client than the one connected. Check the values in the provider's console. |
| Wrong token type | An access token was sent where the provider's ID token is expected. |
| Unknown key | The provider is rotating keys; Lyro refetches them and the next attempt succeeds. |
Verified properties
Claims from a verified token are available to your agent and to custom tools as {USER.ID}, {USER.EMAIL} and {{user.claims.<name>}}.
They resolve only when the current turn is trusted. An anonymous or unsigned visitor gets nothing, so a forged identity can never be interpolated into a call to your backend. {USER.VERIFIED} is always answerable and returns true or false, so a tool can branch on it without failing.
To keep a contact property trustworthy after it is stored, mark it Protected in Settings > Properties. A protected property can only be written by a verified source, so an unsigned browser claim can no longer overwrite it. See People.
Auth tokens for your own APIs
Signed identity tells Lyro who someone is. It does not let a custom tool call your API as that person - for that, pass a credential your backend already understands:
lyro('setAuthTokens', { my_api: '<the visitor\'s access token>' });It becomes {{auth_tokens.my_api}} in any custom tool, most often as an Authorization header. Lyro forwards it verbatim and never parses, stores or logs it.
Because Lyro does not read the token, it cannot tell when it expires - call setAuthTokens again whenever you refresh. Full details in Chat Widget.
Where to go next
- Chat Widget - the JavaScript API these calls belong to.
- People - how identities resolve into one contact, and protected properties.
- API and developers - API keys and the REST surface.
- Tools and integrations - custom tools that consume these values.
Updated 22 days ago
