Authentication
Securely authenticate your users with the Fruga platform.
How the model works
When a user opens your platform and the Relay widget loads, three things happen in sequence — two of which are invisible to the user entirely.
The key insight is that Fruga never manages a login flow for your users. You remain the authority on who is authenticated in your platform. Fruga simply trusts your signed assertion and acts on it.
Your credentials
Fruga provides two credentials for each environment (sandbox and production). You will receive these from the Fruga integrations team before you begin your integration.
| Credential | Format | Where it is used | Safe in frontend? |
|---|---|---|---|
| Partner Key | pk_test_... / pk_live_... | Passed to Fruga.init() in your frontend code to identify your partner environment | Yes — it is a public identifier |
| Signing Secret | sk_test_... / sk_live_... | Used on your backend to sign assertion JWTs. Never passed to the widget or browser. | No — keep this server-side only |
The assertion JWT
An assertion is a standard JWT that your backend generates fresh for each widget session. It has a very short lifespan — 60 seconds is recommended, 120 seconds is the maximum — because it is a one-time proof of identity, not a session token. Once exchanged for a Fruga access token, the assertion cannot be reused.
Required claims
| Claim | Type | Required | Description |
|---|---|---|---|
iss | string | Yes | Your partner identifier, provided by Fruga — e.g. partner:p_123 |
aud | string | Yes | Must be exactly fruga:external_token_exchange |
iat | unix timestamp | Yes | Time the token was issued, in seconds |
exp | unix timestamp | Yes | Expiry time — must be no more than 120 seconds after iat |
jti | string (UUID) | Yes | A unique identifier for this token — prevents the same assertion being used twice |
userRef | string | Yes | Your internal identifier for the current user — can be any stable, unique string in your system |
Example payload
{
"iss": "partner:p_123",
"aud": "fruga:external_token_exchange",
"iat": 1739819000,
"exp": 1739819060, // 60 seconds after iat
"jti": "0c3f5c3a-8d2e-4d4f-9e7a-62b2c0b7d8d1",
"userRef": "user_123"
}
JWT header
Set the algorithm to HS256. No other fields are required in the header for V1.
{
"alg": "HS256",
"typ": "JWT"
}
Signing the assertion
Sign the JWT using HMAC-SHA256 with your Signing Secret. The following examples show how to do this in common backend languages.
Node.js
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
const generateAssertion = (userId) => {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
iss: 'partner:p_123',
aud: 'fruga:external_token_exchange',
iat: now,
exp: now + 60,
jti: uuidv4(),
userRef: userId,
},
process.env.FRUGA_SIGNING_SECRET,
{ algorithm: 'HS256' }
);
};
Python
import jwt
import uuid
import time
import os
def generate_assertion(user_id):
now = int(time.time())
payload = {
"iss": "partner:p_123",
"aud": "fruga:external_token_exchange",
"iat": now,
"exp": now + 60,
"jti": str(uuid.uuid4()),
"userRef": user_id,
}
return jwt.encode(
payload,
os.environ["FRUGA_SIGNING_SECRET"],
algorithm="HS256"
)
What Fruga checks when it receives your assertion
Understanding the server-side verification steps helps you debug failures quickly. When your assertion reaches Fruga’s POST /auth/external/token endpoint, the following checks run in order:
| Check | What Fruga verifies |
|---|---|
| Partner lookup | The partnerKey in the request body maps to a known, active partner environment |
| Signature | The JWT signature is valid against the Signing Secret associated with that partner environment |
| Audience | aud is exactly fruga:external_token_exchange |
| Issuer | iss matches the partner identifier on record |
| Expiry | exp has not passed — the token has not expired |
| Replay protection | The jti has not been used before within its TTL window |
| User upsert | A user record is created or confirmed for the userRef within this partner’s scope |
All checks must pass. If any fail, the exchange returns an error and no access token is issued.
The Fruga access token
On a successful exchange, Fruga issues its own short-lived access token to the widget. This is separate from your assertion — it is Fruga’s internal session token and is handled entirely by the widget SDK. You do not need to store, refresh, or interact with it directly.
The access token has a 15-minute lifetime. The widget manages its own refresh cycle — if a user’s session extends beyond 15 minutes, the widget will silently request a new assertion from your backend and exchange it for a fresh access token. Make sure your assertion endpoint remains available for the duration of a user’s session.
Troubleshooting
The following table covers the most common authentication failures and their causes.
| Error | Likely cause | Fix |
|---|---|---|
401 Unauthorized | Invalid signature — the assertion could not be verified | Confirm you are using the correct Signing Secret for the environment (sandbox vs production). Check that the secret has not been accidentally truncated or URL-encoded. |
401 — token expired | The assertion has already passed its exp time | Ensure your server clock is accurate (NTP-synced). Generate assertions as close to widget initialisation as possible — do not cache them. |
401 — invalid audience | The aud claim does not match what Fruga expects | Set aud to exactly fruga:external_token_exchange — no trailing slashes or additional values. |
409 Conflict | Replay detected — the jti has already been used | Generate a fresh UUID for jti on every assertion. Never reuse or cache assertion tokens. |
| Widget loads but shows no user data | The userRef does not match the value used when the cashback claim was submitted | Ensure the userRef in the assertion is the same stable identifier you pass as userRef in cashback claim calls. It must be consistent across both. |
Security considerations
The assertion pattern is designed so that sensitive credentials never touch the browser. Here is a summary of what is safe where:
| Item | Frontend safe? | Notes |
|---|---|---|
Partner Key (pk_...) | Yes | Public identifier — safe in JavaScript, HTML, or any client-side context |
Signing Secret (sk_...) | No | Store in your secrets manager. Rotate immediately if exposed. |
| Assertion JWT | Yes — briefly | It is passed through the browser, but it expires in 60 seconds and is single-use. Its exposure window is negligible. |
| Fruga access token | Yes | Managed entirely by the widget SDK inside an iframe — your host page never has access to it |
The widget runs inside an iframe, which means your host page is isolated from the Fruga access token by browser same-origin policy. Even if a user inspects your page, they cannot extract the Fruga session.