# Service Account Token Flow for Frontend Apps This guide shows a safe token handoff pattern for frontend apps: 1. The frontend asks your backend for a temporary consumer token. 2. Your backend authenticates to the Merchants backend with a service account (private key based). 3. Your backend requests a short-lived site access token. 4. The backend returns that temporary token to the frontend. 5. The frontend uses the token as a Bearer token when calling Ingrid APIs. ## Why this pattern - Keeps service account private keys on the server only. - Issues short-lived tokens to browsers/mobile apps. - Reduces blast radius if a consumer token leaks. - Lets you add your own authorization checks before minting a token. ## Architecture ```text Frontend app -> POST /api/frontend-token (your backend) -> Authenticate service account with private key (Merchants backend) -> POST /v1/sites/{siteId}/access-tokens (Merchants backend) <- { accessToken, tokenType, expiresAt, expiresIn } Frontend app -> Ingrid APIs with Authorization: Bearer ``` ## Backend example (Node.js) The exact service-account auth endpoint can differ by environment/version. In this example it is represented as `MERCHANTS_AUTH_URL`. ```ts import crypto from "node:crypto"; const MERCHANTS_API_BASE = process.env.MERCHANTS_API_BASE!; const MERCHANTS_AUTH_URL = process.env.MERCHANTS_AUTH_URL!; const SERVICE_ACCOUNT_ID = process.env.SERVICE_ACCOUNT_ID!; const SERVICE_ACCOUNT_PRIVATE_KEY = process.env.SERVICE_ACCOUNT_PRIVATE_KEY!; const SITE_ID = process.env.SITE_ID!; function createSignedAssertion() { const now = Math.floor(Date.now() / 1000); const payload = { sub: SERVICE_ACCOUNT_ID, iat: now, exp: now + 60, aud: MERCHANTS_AUTH_URL, }; const header = { alg: "RS256", typ: "JWT" }; const encode = (obj: object) => Buffer.from(JSON.stringify(obj)).toString("base64url"); const input = `${encode(header)}.${encode(payload)}`; const signature = crypto .createSign("RSA-SHA256") .update(input) .end() .sign(SERVICE_ACCOUNT_PRIVATE_KEY, "base64url"); return `${input}.${signature}`; } async function getServiceAccountAccessToken() { const assertion = createSignedAssertion(); const response = await fetch(MERCHANTS_AUTH_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ grantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion, }), }); if (!response.ok) { throw new Error(`Service account auth failed: ${response.status}`); } const data = await response.json(); return data.accessToken as string; } export async function issueFrontendToken() { const serviceToken = await getServiceAccountAccessToken(); const response = await fetch( `${MERCHANTS_API_BASE}/v1/sites/${SITE_ID}/access-tokens`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${serviceToken}`, }, body: JSON.stringify({ scopes: ["view"], ttlSeconds: 300, externalRef: "frontend-session", }), }, ); if (!response.ok) { throw new Error(`Access token mint failed: ${response.status}`); } return response.json(); } ``` ## Frontend example ```ts export async function fetchFrontendToken() { const res = await fetch("/api/frontend-token", { method: "POST", credentials: "include", }); if (!res.ok) { throw new Error("Could not get frontend token"); } return (await res.json()) as { accessToken: string; tokenType: "Bearer"; expiresAt: string; expiresIn: number; }; } ``` Use the returned token in API calls: ```ts const { accessToken } = await fetchFrontendToken(); await fetch("https://api.ingrid.com/v1/delivery/options", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(payload), }); ``` ## Security checklist - Never expose service account private keys to the client. - Keep temporary token TTL short (for example 5 minutes). - Scope token permissions narrowly (for example `view` only). - Authenticate your own user/session before issuing tokens. - Log token issuance with `externalRef` for traceability. - Rotate service account keys regularly. ## Operational notes - Refresh on expiry: when frontend receives `401`, fetch a fresh temporary token and retry once. - Cache server-side service-account auth token briefly to reduce auth calls. - Add rate limits on `/api/frontend-token` to protect against abuse.