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.

💡
This page covers frontend-specific security. For the full authentication model including backend JWT signing and HMAC request signing, see the Authentication guide.

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.

CredentialCreated byPasses through browser?LifetimeNotes
Partner KeyFrugaYes - it is publicStaticYour NEXT_PUBLIC_PARTNER_ID. Safe in JavaScript, HTML, or any client-side context
Assertion JWTYour backendNo - 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 tokenFrugaYes - your endpoint returns it, and the SDK passes it to the widget15 minutesShort-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, or ISSUER in client-side code, including bundled environment variables. Only NEXT_PUBLIC_PARTNER_ID belongs in the browser.
  • Let errors in onTokenRequired throw. 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;
DirectiveDomainWhy it is needed
frame-srchttps://widget.fruga.comAllows the widget iframe to load
connect-srchttps://api.fruga.comAllows the SDK to make API calls to Fruga (from within the iframe context)
⚠️
If you have a strict CSP and the widget is not loading, open your browser console - a blocked resource will appear as a CSP violation error and will identify exactly which directive needs to be updated.

Summary

PracticeRequired?
Keep PARTNER_SECRET server-side onlyYes - never expose in frontend code
Include kid (PARTNER_KEY_ID) in every assertion headerYes - the exchange fails with a 401 without it
Return the access token, never the assertion, to the browserYes
Sign a fresh assertion per token requestYes - never cache or reuse
Do not log the assertionStrongly recommended
Update CSP to allow Fruga domainsYes, if your site uses CSP
Handle SESSION_EXPIRED event for long sessionsYes, if users may be active for >15 minutes