Authenticate Users in an Embedded AI Chat Widget (JWT)
To authenticate users in an embedded AI chat widget, mint a short-lived JWT on your server that carries the user's id, email, and roles, hand only the signed token to the widget, and let the agent scope every answer and action to that user. Never put the signing key in the browser. Keep tokens short-lived and refresh them on demand.
Quick answer
To authenticate users in an embedded AI chat widget, mint a short-lived JWT on your server that carries the user's id, email, and roles, hand only the signed token to the widget, and let the agent scope every answer and action to that user. Never put the signing key in the browser. Keep tokens short-lived and refresh them on demand.
To authenticate users in an embedded AI chat widget, mint a short-lived JSON Web Token (JWT) on your backend that carries the user's id, email, and roles, then hand only the signed token to the widget. The agent reads that token, confirms who it is talking to, and scopes every answer and action to that person's data and permissions. The signing key stays on your server and never touches the browser.
This matters more for an AI agent than for a plain chatbot, because the agent does not just answer questions. It reads from your CRM, files a ticket in your help desk, updates a record, or posts a result to Slack. If the agent cannot trust the identity in front of it, every one of those actions becomes a security hole. A forged or missing identity means an agent acting as the wrong user, on the wrong data, with the wrong permissions.
This guide covers why JWT is the right approach, where to sign the token, how long it should live, how identity flows from your login session into the agent's tool calls, and the mistakes that quietly leak secrets. It is written for the engineer dropping the widget on a page who has to get auth right the first time.
Why a JWT, not a user ID hash
An older pattern is HMAC user-hashing: you take a user_id, hash it with a shared secret, and send the pair so the widget can confirm the id was not tampered with. It works, but it verifies exactly one field. The widget learns that this user_id is genuine and nothing else. Roles, email, plan tier, account membership, expiry: none of it travels with the proof.
A signed JWT carries a full payload and a built-in expiry. You can put the user's id, email, display name, roles, organization, and any custom attribute the agent needs to make access decisions. Because the token is signed, the agent can trust every claim inside it without a round-trip back to your database. Because it expires, a leaked token stops working on its own.
For an AI agent that takes action, the difference is concrete. With a hash, the agent knows the user is real. With a JWT, the agent knows the user is real and that they are a billing-admin in the Acme org on the Pro plan, which is exactly what it needs to decide whether to let them cancel a subscription or only read invoices. That is why JWT is the modern recommended approach: it is richer, self-expiring, and carries the context that least-privilege access control depends on.
| Capability | HMAC user hash | Short-lived JWT |
|---|---|---|
| Proves user identity | Yes (one field) | Yes (full payload) |
| Carries roles and attributes | No | Yes |
| Built-in expiry | No (manual) | Yes (exp claim) |
| Signing key stays server-side | Yes | Yes |
| Drives least-privilege scoping | Limited | Yes |
| Standard, library support | Custom | Broad (RFC 7519) |
| Refresh mid-session | Re-issue hash | Re-mint token via callback |
How identity flows from login to agent action
The whole point of authenticating the widget is so identity survives the trip from your login session all the way into the agent's tool calls. Here is the chain, end to end, with no secret ever leaving your server.
- 1
User logs in
Your existing auth establishes a session for the user in your product.
- 2
Backend mints a JWT
On widget load, your server signs a short-lived token with the user's id, email, and roles.
- 3
Widget receives the token
The browser gets only the signed token, never the signing key.
- 4
Agent verifies the claims
Onpilot validates the signature and expiry and reads the identity payload.
- 5
Agent scopes the request
RBAC limits which tools and data the agent may touch for this user.
- 6
Action runs with audit trail
Reads and writes execute as the right user, logged for review and approval.
Each step happens server-side except the widget render. The signing key never reaches the browser.
Notice what is missing from that flow: at no point does the browser hold the key that signs tokens, and at no point does the agent guess who the user is. Identity is asserted once, server-side, and carried as signed claims everywhere it is needed. That is the property you are buying with a JWT.
It also means the agent's permissions are never broader than the user's. A support rep's token cannot trigger an admin-only action, because the role claim that would authorize it simply is not in the token.
Sign on the server, always
The signing key or shared secret must never appear in client JavaScript. Not in the bundle, not in an environment variable that gets inlined at build time, not in a config endpoint the browser can fetch. Your backend mints a token per user on request, and the widget receives only the already-signed token.
Picture the failure mode. If the key is in the browser, anyone who opens the network tab or reads the JS bundle can copy it, sign their own token claiming to be any user, paste it into the widget, and the agent will believe them. From there they can read another customer's data or trigger actions as that customer. There is no clever client-side trick that makes this safe; the trust boundary is the server, and the key lives behind it.
Practically, this means you expose a small server endpoint, something like GET /chat-token, that checks the current session, builds the claims, signs the token, and returns it. The widget calls that endpoint on load and passes the result in. Five or six lines of server code in Node, Python, Ruby, Go, or PHP. The discipline is not the volume of code, it is keeping the secret on the right side of the wire.
“If your signing key can be viewed in the browser's network tab or JS bundle, your authentication is already broken. Mint tokens server-side, every time, behind your session check.”
Keep the token short-lived
Give the token a short expiry, typically a few minutes, and refresh on demand. The exp claim is your blast-radius control: if a token is somehow captured, a five-minute window is a far smaller problem than a token that lives for a day. Short-lived tokens are the same pattern you already use for access tokens elsewhere in your stack, and the reasoning is identical.
A short expiry needs a refresh path so long sessions do not break. The widget supports a callback that fires when the token is near expiry or rejected, and your callback simply calls the same server endpoint again to get a fresh token. The user notices nothing; the agent keeps acting as the right person. Pair the short exp with a few standard claims, an iss (issuer) you control, an aud (audience) for the widget, and an iat (issued-at) so you can reason about token age.
Resist the urge to make tokens long-lived to avoid writing the refresh callback. It is a few lines, and it converts a leaked-token incident from a breach into a non-event.
A worked scenario: support widget in a multi-tenant SaaS
Make it concrete. You run a B2B analytics product. Acme and Globex are both customers on the same deployment. You embed an Onpilot agent in the in-app help panel so users can ask 'why did my report fail last night?' and the agent can actually look, not just guess.
Dana, a billing-admin at Acme, opens the help panel. Your server checks her session and mints a JWT with id: dana@acme.com, org: acme, roles: [billing_admin, member], and an exp five minutes out. The widget receives only that signed token. Dana asks the agent to pull her last three invoices and flag any failed charges.
The agent verifies the signature, reads org: acme, and scopes the data query to Acme's tenant only. Dana could not see Globex data even if she asked, because the org claim, not her prompt, decides scope. Because she carries billing_admin, the agent is allowed to read invoices. When she then asks it to retry a failed charge, that is a write, so a human-in-the-loop approval card appears before anything executes, and the whole sequence lands in the audit log with her identity attached.
Now flip it. Sam at Globex, a regular member, asks the same questions. His token says org: globex, roles: [member]. He sees only Globex data, and the retry-charge action is simply not offered, because the role that would authorize it is not in his token. Same widget, same agent, two completely different and correctly bounded experiences, all driven by the claims your server signed. No prompt and no clever phrasing can cross those lines, because the boundary is the token, not the conversation.
Common pitfalls (and how to avoid them)
Most embedded-auth incidents come from a small set of repeatable mistakes. Watch for these.
- Shipping the signing key to the client: the single most damaging error. Inlining the secret at build time or exposing it via a config route lets anyone forge tokens. Keep the key in server-only secret storage and sign behind a session check.
- Trusting claims the user can edit: never read the user's identity from a query string, localStorage, or a header the browser controls. The signed JWT is the only source of truth for who the user is.
- Long-lived or never-expiring tokens: skipping the exp claim, or setting it to hours, turns one leaked token into a long-running impersonation. Keep exp at minutes and wire the refresh callback.
- No signature or expiry verification on the receiving side: a token that is not actually checked is decoration. Onpilot validates the signature and exp, so do not work around it by passing identity out-of-band.
- Scoping by prompt instead of by claim: telling the agent 'only show Acme data' in the prompt is not access control. Scope must come from the org and role claims and be enforced by RBAC, so no clever phrasing can widen access.
- Reusing one token across users or tabs: mint per user, per session. A shared token erases the audit trail and breaks the one-identity guarantee.
- Forgetting iss and aud: without an issuer and audience, a token minted for one app can be replayed against another. Set and verify both.
How much security each control adds
Not every control carries the same weight. The chart below ranks the practices that matter most for a production embedded widget, scored by how much each one reduces real impersonation and data-exposure risk. Server-side signing and short-lived tokens do the heavy lifting; the rest harden the edges.
Illustrative scores to show relative weight, not measured benchmarks. Combine controls; none is sufficient alone.
One identity across domains and devices
Use a stable external user ID inside the token payload. The widget treats matching IDs as the same identity, so conversation history and permissions follow the user across domains, browsers, and devices, without you re-implementing session handling per surface. A user who starts a thread on your marketing site and continues inside the app keeps the same agent context, because both surfaces sign tokens with the same external ID.
Pick an ID that is stable and unique and that you control: your internal primary key or a dedicated external_id, not an email that might change and not a value the browser can rewrite. If you operate across multiple sub-brands or domains, the external ID is what stitches them into one coherent identity for the agent.
Onpilot is built around this flow. The identity-tokens reference and the server-side recipes give copy-paste examples in Node, Python, Ruby, Go, and PHP, and the same token that proves who the user is also drives RBAC scoping and the audit log, so identity, permission, and accountability all come from one signed source.
A decision framework: choosing your auth approach
Use this to pick the right level of auth for your embed rather than defaulting to the heaviest option.
- Is the widget on a public, anonymous page with no user data? You may not need identity at all. Run the agent unauthenticated and let it answer only from public knowledge with no tool access to private systems.
- Is the widget behind a login but only answering, never acting? Mint a short-lived JWT so the agent can personalize and scope reads. Identity still matters because reads can expose private data.
- Does the agent take actions (update records, file tickets, post messages)? A JWT is mandatory, plus RBAC scoping by role claim and human-in-the-loop approval on writes. Identity now authorizes side effects, so it must be airtight.
- Are you multi-tenant? The org or tenant claim in the token is non-negotiable, and every data query must be scoped by it server-side, never by prompt. One mis-scoped query is a cross-tenant leak.
- Do you have compliance obligations (SOC 2, audit requirements)? Pair the JWT with an audit log that records the signed identity on every action, so you can answer 'who did what, as whom, when' with evidence.
- Still hashing user IDs from an older integration? Migrate to JWT. The hash cannot carry roles or expiry, both of which you will need the moment the agent does more than answer questions.
“Rule of thumb: if the agent can change anything or see anything private, you need a signed JWT plus RBAC plus an audit log. Anonymous, read-only, public-knowledge widgets are the only case that can skip identity.”
Putting it together
Authenticating an embedded AI chat widget comes down to a few durable rules. Sign the token on your server and keep the key there. Carry id, email, and roles in the payload so the agent can make least-privilege decisions. Keep the token short-lived and refresh it through a callback. Use a stable external ID so identity follows the user everywhere. And let the same signed claims drive RBAC and the audit log, so reads are scoped, writes are gated, and every action is attributable.
Get those right and the hard part is done. The agent knows who it is talking to, acts only within that user's permissions, and leaves a clean record behind. For copy-paste server code and the widget integration, start with the identity-tokens reference and the quickstart.
Frequently asked questions
Why use a JWT to authenticate an embedded AI chat widget?
+
A signed JWT lets your backend vouch for the logged-in user so the agent knows exactly who it is talking to. It prevents impersonation and lets the agent safely scope data and tool access to that user, all without exposing any secret in the browser. For an agent that takes action, that signed identity is what authorizes or blocks each step.
Should the token be generated client-side or server-side?
+
Always server-side. The signing key or shared secret must never appear in client JavaScript, including the bundle or any config endpoint the browser can read. Your backend mints a token per user behind a session check, and the widget receives only the already-signed token.
How long should the token live?
+
Keep it short, typically a few minutes, and refresh on demand. A short expiry limits the blast radius if a token leaks, and the widget can request a fresh token mid-session via a callback. Pair the exp claim with iss, aud, and iat so you can reason about each token's origin and age.
What is the difference between HMAC user-hashing and JWT?
+
HMAC hashing just proves a user_id was not tampered with, verifying a single field. A JWT carries a full signed payload (id, email, roles, custom attributes) and a built-in expiry. JWT is the modern recommended approach because it is richer and self-expiring, and it carries the role and org context that least-privilege scoping depends on.
How do I authenticate the same user across multiple domains or devices?
+
Use a stable external user ID inside the token payload. The widget treats matching IDs as one identity, so conversation history and permissions follow the user across domains, browsers, and devices. Pick an ID you control, such as your internal primary key, rather than an email that might change.
How does the agent enforce permissions from the token?
+
The role and org claims in the JWT feed least-privilege RBAC, so the agent only exposes tools and data the user is allowed to touch. Scope is decided by the signed claims, not by anything the user types in the chat, so no clever phrasing can widen access. Writes can also require human-in-the-loop approval before they run.
What happens if a JWT leaks or is stolen?
+
Because the token is short-lived, a leaked token stops working within minutes when its exp passes. The damage is bounded to that small window, and the next request fails until a fresh token is minted server-side. This is exactly why you keep exp at minutes rather than hours and wire a refresh callback instead of issuing long-lived tokens.
Do I need a JWT if the widget is on a public, anonymous page?
+
Not necessarily. If the agent only answers from public knowledge and has no access to private systems, you can run it without identity. The moment the widget sits behind a login or the agent can read private data or take actions, you need a signed JWT plus RBAC scoping.
How does authenticating the widget help with SOC 2 or audit requirements?
+
The signed identity in the JWT is recorded with every action the agent takes, so the audit log can answer who did what, as whom, and when, with evidence. Combined with RBAC scoping and human approval on writes, that gives you the traceability auditors expect for agent activity.
Related
Embed a secure, authenticated AI agent.
Mint a short-lived identity token on your server and drop the widget on the page. The agent acts as the right user, scoped by RBAC and recorded in the audit log.
Read the developer docs