JWT & Token Security
A summary of how tokens move through the browser, what the host page can and cannot access, and what you need to do - and not do - to keep the integration secure on the frontend.
What passes through the browser
The Relay authentication flow involves three credentials. Understanding what each one is and where it lives is the foundation of the frontend security model.
| Credential | Created by | Passes through browser? | Lifetime | Notes |
|---|---|---|---|---|
| Partner Key | Fruga | Yes - it is public | Static | Your NEXT_PUBLIC_PARTNER_ID. Safe in JavaScript, HTML, or any client-side context |
| Assertion JWT | Your backend | No - signed and exchanged entirely server-side | ~16 seconds (120 max) | Never returned to the browser. If your frontend can see an assertion, the flow is wired incorrectly |
| Fruga access token | Fruga | Yes - your endpoint returns it, and the SDK passes it to the widget | 15 minutes | Short-lived and scoped to a single user |
This is the key difference from a naive integration: the sensitive part of the exchange happens between your server and Fruga’s, so the browser only ever holds a public identifier and a short-lived, user-scoped access token.
iframe isolation
The Relay widget runs inside a cross-origin iframe served from Fruga’s domain. This means your host page and the widget are subject to the browser’s same-origin policy - they cannot read each other’s DOM, cookies, local storage, or JavaScript context.
In practice, this has two important implications:
Your host page cannot read the widget’s session state. The access token is handed to the widget once and then lives inside the iframe. Your page cannot inspect the widget’s storage or JavaScript context.
The widget cannot access anything on your host page. The widget cannot read your cookies, inspect your local storage, or execute JavaScript in your page context. The iframe boundary is a strict security boundary in both directions.
Keeping the assertion secure
The assertion never reaches the browser, so these are backend practices - but they are the ones that keep the frontend safe.
Never return the assertion to the browser
Your token endpoint must return the access token from the exchange, not the assertion that produced it. Returning the assertion hands the browser a credential it has no reason to hold.
Never cache the assertion
Sign a fresh assertion on every token request. Do not store assertions anywhere - not in a server-side cache, and certainly not in the browser. A reused assertion fails the replay check and returns a 409.
Never log the assertion
Avoid logging the assertion string to your server logs or sending it to any error monitoring service. Although it expires quickly, treat it as you would any short-lived credential. Decoding it locally at jwt.io during setup is fine; shipping it to a log aggregator is not.
Sign and exchange in the same request
The ~16 second window begins at signing, not at first use. Signing an assertion ahead of time and exchanging it later is the most common cause of spurious expiry errors.
What the frontend must do
The browser’s only job in this flow is to call your endpoint and hand the result to the widget.
- Never hardcode a token. Always supply
onTokenRequired, so the widget can request a fresh one whenever it needs it. - Never put
PARTNER_SECRET,PARTNER_KEY_ID, orISSUERin client-side code, including bundled environment variables. OnlyNEXT_PUBLIC_PARTNER_IDbelongs in the browser. - Let errors in
onTokenRequiredthrow. Catching a failure and returning''mounts the widget with an empty token, which surfaces as a blank widget instead of a diagnosable error.
Content Security Policy
If your site uses a Content Security Policy, you will need to allow the Fruga widget iframe and its associated resources. Add the following directives to your existing CSP header.
Content-Security-Policy:
frame-src https://widget.fruga.com;
connect-src https://api.fruga.com;
| Directive | Domain | Why it is needed |
|---|---|---|
frame-src | https://widget.fruga.com | Allows the widget iframe to load |
connect-src | https://api.fruga.com | Allows the SDK to make API calls to Fruga (from within the iframe context) |
Summary
| Practice | Required? |
|---|---|
Keep PARTNER_SECRET server-side only | Yes - never expose in frontend code |
Include kid (PARTNER_KEY_ID) in every assertion header | Yes - the exchange fails with a 401 without it |
| Return the access token, never the assertion, to the browser | Yes |
| Sign a fresh assertion per token request | Yes - never cache or reuse |
| Do not log the assertion | Strongly recommended |
| Update CSP to allow Fruga domains | Yes, if your site uses CSP |
Handle SESSION_EXPIRED event for long sessions | Yes, if users may be active for >15 minutes |