Skip to content

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

TermDescription
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 TokenA 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 TokenA long-lived bearer token for the YouCan Store Admin API.
HMACA SHA-256 signature appended to the launch URL, proving it came from YouCan.
QantraYouCan'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:

Auto-injection via qantra.initialize()

Calling qantra.initialize() patches window.fetch so all same-origin requests automatically include:

http
Authorization: Bearer <live session JWT>
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": "<YOUR_CLIENT_ID>",
  "str": "my-store",
  "sid": "<stable-session-id>",
  "sub": "<seller-user-id>",
  "iat": 1709000000,
  "exp": 1709086400
}
FieldUse
strStore slug — identify the store
sidSession ID — use as your DB primary key
audMust match your Client ID
issMust be https://api.youcan.shop
expReject expired tokens

Full Flow Diagram


Persisting Sessions

sql
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
);

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)
    await db.session.update({ id: sessionId, accessToken: null, registeredWebhooks: null });
  } else {
    await db.session.create({ id: sessionId, store });
  }
}

Re-authentication & Token Recovery

ScenarioAction
Seller opens embedded appqantra.sessionToken() JWT injected → server looks up session → reuses stored token
No access token in DBJWT present → server does token_exchange → persists token
YouCan API returns 401Clear access_token in DB. Qantra retries with fresh JWT from qantra.sessionToken().
App uninstalledapp.uninstalled webhook → clear all access tokens for the store