External App Authentication
External apps authenticate via an authorization code flow. When a seller opens your app, YouCan redirects them to your URL with a one-time ?code= parameter, which your server exchanges for a store access token.
If you're building an embedded app that runs inside the Seller Area iframe, see Embedded App Authentication instead.
Core Concepts
| Term | Description |
|---|---|
Authorization Code (code) | A one-time code in the launch URL. Directly exchangeable for an access token. Short-lived — exchange it immediately. |
| Access Token | A long-lived bearer token for the YouCan Store Admin API. |
| HMAC | A SHA-256 signature on the launch URL, proving it came from YouCan. |
The Launch URL
When a seller opens your external app, the Seller Area redirects them to:
https://your-app.com/?timestamp=...&code=AUTH_CODE&state=STATE&store=my-store&seller=ID&locale=en&embedded=0&hmac=SIGStep 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);
}Step 2 — Exchange the Code
Exchange the ?code= for an access token server-side, before rendering the page. The code is one-time use and short-lived.
// Express route handler
app.get('/', async (req, res) => {
const { code, store, hmac } = req.query;
if (!verifyHmac(req.url.split('?')[1], process.env.YOUCAN_API_SECRET)) {
return res.status(403).send('Forbidden');
}
const tokenRes = await fetch('https://api.youcan.shop/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.YOUCAN_API_KEY,
client_secret: process.env.YOUCAN_API_SECRET,
code,
}),
});
const { access_token } = await tokenRes.json();
await db.session.upsert({ store, accessToken: access_token });
res.redirect('/dashboard');
});WARNING
The code is one-time use and short-lived. Exchange it immediately — never store it.
The exchange returns:
{ "access_token": "...", "expires_in": 86400 }