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 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 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=SIGThe 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.
- Take all query params from the URL except
hmac. - Build a query string (in the same order as received).
- Compute
HMAC-SHA256(query_string, CLIENT_SECRET). - Compare using constant-time comparison.
WARNING
Never use == or === to compare signatures. Use constant-time comparison to prevent timing attacks.
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);
}2
3
4
5
6
7
8
9
10
11
12
13
14
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:
Auto-injection via qantra.initialize()
Calling qantra.initialize() patches window.fetch so all same-origin requests automatically include:
Authorization: Bearer <live session JWT>
X-Requested-With: XMLHttpRequest2
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)
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();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Token exchange request
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 }
}2
3
4
5
6
7
8
9
10
11
12
13
14
JWT Payload
{
"iss": "https://api.youcan.shop",
"aud": "<YOUR_CLIENT_ID>",
"str": "my-store",
"sid": "<stable-session-id>",
"sub": "<seller-user-id>",
"iat": 1709000000,
"exp": 1709086400
}2
3
4
5
6
7
8
9
| 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
Persisting Sessions
Recommended schema
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- JWT `sid`
store TEXT NOT NULL, -- store slug
access_token TEXT, -- null until first exchange
registered_webhooks TEXT -- for webhook state tracking
);2
3
4
5
6
On re-install or re-open
When the HMAC-verified launch URL is processed, always reset the access token:
async function handleSessionPost(sessionId, store) {
const existing = await db.session.findOne({ id: sessionId });
if (existing) {
// Reset to force fresh token exchange (handles reinstalls)
await db.session.update({ id: sessionId, accessToken: null, registeredWebhooks: null });
} else {
await db.session.create({ id: sessionId, store });
}
}2
3
4
5
6
7
8
9
10
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 |