--- url: https://docs.youcan.shop/apps/embedded_app/auth.md --- # Embedded App Authentication Embedded apps load inside the Seller Area as an iframe. There is no authorization code in the URL — the frontend calls `qantra.sessionToken()` at runtime to obtain a short-lived JWT, which your server exchanges for a store access token. ::: tip If you create your app using the [Nuxt Starter Template](https://github.com/youcan-shop/shop-app-template-nuxt) via the YouCan CLI, the entire auth flow (Qantra, JWT exchange, session persistence) is already fully implemented for you. ::: If you're building an external app that opens in its own browser tab, see [External App Authentication](/apps/external_app/auth) instead. ## Core Concepts | Term | Description | |---|---| | **Session ID** (`session`) | A stable, deterministic identifier for the (app × seller × store) combination. Passed in the launch URL as `?session=`. Used as your DB key — **not** an exchangeable credential. | | **Session Token** | A short-lived JWT signed by YouCan. Obtained at runtime via `qantra.sessionToken()` — it is **not** in the URL. This is what is exchanged for an access token. | | **Access Token** | A long-lived bearer token for the YouCan Store Admin API. | | **HMAC** | A SHA-256 signature appended to the launch URL, proving it came from YouCan. | | **Qantra** | YouCan's communication bridge between the Seller Area (parent window) and your app (iframe). | *** ## The Launch URL When a seller opens your embedded app, the Seller Area builds a signed URL: ``` https://your-app.com/?timestamp=...&session=SID&store=my-store&seller=ID&locale=en&embedded=1&hmac=SIG ``` The `session` param is only a DB lookup key. The actual session JWT is fetched at runtime via `qantra.sessionToken()`. *** ## Step 1 — HMAC Verification **Always verify the HMAC before doing anything else.** An invalid HMAC means the request didn't come from YouCan. 1. Take all query params from the URL **except** `hmac`. 2. Build a query string (in the same order as received). 3. Compute `HMAC-SHA256(query_string, CLIENT_SECRET)`. 4. Compare using **constant-time comparison**. ::: warning Never use `==` or `===` to compare signatures. Use constant-time comparison to prevent timing attacks. ::: ```js const crypto = require('crypto'); function verifyHmac(queryString, clientSecret) { const params = new URLSearchParams(queryString); const received = params.get('hmac'); params.delete('hmac'); const message = params.toString(); const expected = crypto.createHmac('sha256', clientSecret).update(message).digest('hex'); const a = Buffer.from(received ?? '', 'hex'); const b = Buffer.from(expected, 'hex'); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` *** ## Step 2 — Obtain a Session Token via Qantra ### How `qantra.sessionToken()` works Qantra uses `postMessage` to communicate with the Seller Area, which calls its own internal API to generate and return the JWT: ```mermaid sequenceDiagram participant App as Your App (iframe) participant SA as Seller Area (parent window) participant API as YouCan API App->>SA: qantra.sessionToken() SA->>API: GET /apps/session-token (internal) API-->>SA: { session_token: JWT } SA->>App: resolves with JWT Note over App: Token ready for exchange ``` ### Auto-injection via `qantra.initialize()` Calling `qantra.initialize()` patches `window.fetch` so all same-origin requests automatically include: ```http Authorization: Bearer X-Requested-With: XMLHttpRequest ``` Your server reads the `Authorization: Bearer` header on every request — no manual token wiring needed. If your server responds with `x-youcan-retry-invalid-session-request`, Qantra calls `qantra.sessionToken()` again and retries the request once automatically. *** ## Step 3 — Exchange the Session Token ### Server middleware (Node.js / Express) ```js const jwt = require('jsonwebtoken'); const { exchangeSessionToken, db } = require('./lib'); async function sessionMiddleware(req, res, next) { const authHeader = req.headers['authorization']; if (!authHeader) return next(); const token = authHeader.replace('Bearer ', ''); const payload = jwt.verify(token, process.env.YOUCAN_API_SECRET); let session = await db.session.findOne({ id: payload.sid }); if (!session) { session = await db.session.create({ id: payload.sid, store: payload.str }); } if (!session.accessToken) { const { access_token } = await exchangeSessionToken(token); session = await db.session.update({ id: payload.sid, accessToken: access_token }); } req.session = session; next(); } ``` ### Token exchange request ```js async function exchangeSessionToken(sessionToken) { const res = await fetch('https://api.youcan.shop/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'token_exchange', client_id: process.env.YOUCAN_API_KEY, client_secret: process.env.YOUCAN_API_SECRET, session_token: sessionToken, }), }); return res.json(); // { access_token, expires_in } } ``` ### JWT Payload ```json { "iss": "https://api.youcan.shop", "aud": "", "str": "my-store", "sid": "", "sub": "", "iat": 1709000000, "exp": 1709086400 } ``` | Field | Use | |---|---| | `str` | Store slug — identify the store | | `sid` | Session ID — use as your DB primary key | | `aud` | Must match your Client ID | | `iss` | Must be `https://api.youcan.shop` | | `exp` | Reject expired tokens | *** ## Full Flow Diagram ```mermaid sequenceDiagram actor seller participant SA as Seller Area participant App as Your App (iframe) participant Server as Your App Server participant API as YouCan API seller->>SA: Opens app SA->>SA: Build URL with session=SID, embedded=1, HMAC SA->>App: Load iframe at https://app.com/?session=SID&hmac=SIG App->>Server: GET / (signed URL) Server->>Server: Verify HMAC Server->>Server: Upsert session (SID), reset accessToken Server-->>App: Render page + load JS Note over App,SA: qantra.initialize() patches fetch, requests session token App->>SA: qantra.sessionToken() SA->>API: GET /apps/session-token API-->>SA: JWT SA->>App: resolves with JWT App->>Server: GET /api/data (Bearer JWT auto-injected by Qantra) Server->>Server: Verify JWT, look up session alt No access_token stored Server->>API: POST /oauth/token (token_exchange) API-->>Server: access_token Server->>Server: Persist access_token end Server->>API: GET /orders (Bearer access_token) API-->>Server: Data Server-->>App: Response ``` *** ## Persisting Sessions ### Recommended schema ```sql CREATE TABLE sessions ( id TEXT PRIMARY KEY, -- JWT `sid` store TEXT NOT NULL, -- store slug access_token TEXT -- null until first exchange ); ``` ### On re-install or re-open When the HMAC-verified launch URL is processed, always reset the access token: ```js async function handleSessionPost(sessionId, store) { const existing = await db.session.findOne({ id: sessionId }); if (existing) { // Reset to force fresh token exchange (handles reinstalls) } else { await db.session.create({ id: sessionId, store }); } } ``` *** ## Re-authentication & Token Recovery | Scenario | Action | |---|---| | Seller opens embedded app | `qantra.sessionToken()` JWT injected → server looks up session → reuses stored token | | No access token in DB | JWT present → server does `token_exchange` → persists token | | YouCan API returns `401` | Clear `access_token` in DB. Qantra retries with fresh JWT from `qantra.sessionToken()`. | | App uninstalled | `app.uninstalled` webhook → clear all access tokens for the store |