Session Tokens
Session Tokens
Session tokens authenticate embedded apps running inside the SeloraX dashboard iframe. They provide a secure way for the dashboard and embedded app to communicate without exposing long-lived credentials in the browser.
How It Works
- The merchant navigates to your app inside the SeloraX dashboard
- The dashboard requests embed parameters from the platform
- The platform generates an HMAC-signed iframe URL and a short-lived JWT session token
- The dashboard loads your app in an iframe with the signed URL
- Your app verifies the HMAC signature and uses the session token for API calls
Step 1: Get Embed Parameters
The dashboard calls this endpoint when loading an embedded app.
GET /api/apps/session/embed-params?app_id=X&store_id=Y
Requires [auth, admin] middleware (merchant must be logged in).
Response:
{
"iframe_url": "https://app.example.com?store_id=22&host=base64(admin.selorax.io)×tamp=1709251200&hmac=abc123...",
"session_token": "eyJ...",
"expires_in": 600,
"app_name": "SeloraX Messaging"
}The iframe_url includes query parameters that your app must verify before trusting the request.
HMAC URL Verification
When your app loads, it receives URL parameters including an hmac signature. You must verify this signature to confirm the request originated from the SeloraX platform.
How the HMAC is generated
- All query parameters except
hmacare collected - Parameters are sorted alphabetically by key
- Sorted key=value pairs are joined with
& - The resulting string is signed with your app's
session_signing_keyusing HMAC-SHA256
Message format:
host=base64url(admin.selorax.io)&store_id=22×tamp=1709251200
Verification on your app server
const crypto = require('crypto');
function verifyHmac(query, sessionSigningKey) {
const { hmac, ...params } = query;
// Sort parameters alphabetically and build the message
const message = Object.keys(params)
.sort()
.map((key) => `${key}=${params[key]}`)
.join('&');
// Compute expected HMAC
const expectedHmac = crypto
.createHmac('sha256', sessionSigningKey)
.update(message)
.digest('hex');
// Timing-safe comparison
if (hmac.length !== expectedHmac.length) return false;
return crypto.timingSafeEqual(
Buffer.from(hmac, 'hex'),
Buffer.from(expectedHmac, 'hex')
);
}
// Usage in an Express route
app.get('/', (req, res) => {
const isValid = verifyHmac(req.query, process.env.SESSION_SIGNING_KEY);
if (!isValid) {
return res.status(401).send('Invalid HMAC signature');
}
// Check timestamp is within 5 minutes
const timestamp = parseInt(req.query.timestamp, 10);
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return res.status(401).send('Request expired');
}
// Render your app
res.render('app');
});:::warning The timestamp window is 5 minutes. Requests with a timestamp older than 5 minutes must be rejected to prevent replay attacks. :::
JWT Session Token
Along with the HMAC-signed URL, the platform issues a JWT session token. This token is used for API calls from the embedded app.
Token payload (HS256, signed with session_signing_key)
{
"iss": "https://admin.selorax.io",
"dest": "https://app.example.com",
"aud": "sx_app_...",
"sub": "22",
"sid": "2",
"app_id": 1,
"jti": "uuid",
"iat": 1709251200,
"exp": 1709251800
}| Claim | Description |
|---|---|
iss | Issuer: the SeloraX dashboard URL |
dest | Destination: your app's URL |
aud | Audience: your app's client_id |
sub | Subject: the store_id |
sid | Session ID: the installation_id |
app_id | The app's numeric ID |
jti | Unique token identifier (UUID) |
iat | Issued at (Unix timestamp) |
exp | Expires at (Unix timestamp, 10 minutes after iat) |
Verifying the session token on your server
const jwt = require('jsonwebtoken');
function verifySessionToken(token, sessionSigningKey, expectedClientId) {
try {
const decoded = jwt.verify(token, sessionSigningKey, {
algorithms: ['HS256'],
audience: expectedClientId,
issuer: 'https://admin.selorax.io',
});
return {
valid: true,
store_id: decoded.sub,
installation_id: decoded.sid,
app_id: decoded.app_id,
};
} catch (err) {
return { valid: false, error: err.message };
}
}Refreshing Session Tokens
Session tokens expire after 10 minutes. There are two ways to get a new one.
Option 1: API call
POST /api/apps/session/session-token
Requires [auth, admin] middleware.
Request body:
{
"app_id": 1,
"store_id": 22
}Response:
{
"session_token": "eyJ...",
"expires_in": 600
}Option 2: postMessage (recommended for iframes)
Your embedded app can request a new token from the dashboard via the postMessage API without a full page reload.
App sends:
// From inside your iframe
window.parent.postMessage(
{ type: 'selorax:request-session-token' },
'https://admin.selorax.io'
);Dashboard responds:
// Your app listens for the response
window.addEventListener('message', (event) => {
if (event.origin !== 'https://admin.selorax.io') return;
if (event.data.type === 'selorax:session-token') {
const { session_token, expires_in } = event.data;
// Store the new token and schedule the next refresh
}
});:::tip Set up a timer to request a new token before the current one expires (e.g. refresh at the 8-minute mark) to ensure uninterrupted API access. :::
Server-Side Verification
If your app's backend needs to verify a session token without decoding the JWT locally, you can use the platform's verification endpoint.
POST /api/apps/session/verify
Rate limited to 300 requests per minute.
Request body:
{
"session_token": "eyJ...",
"client_id": "sx_app_...",
"client_secret": "sx_secret_..."
}Response:
{
"message": "Session token verified.",
"data": {
"store_id": 22,
"installation_id": 2,
"app_id": 1
},
"status": 200
}This is useful when you prefer not to handle JWT verification locally or need to confirm token validity against the platform in real time.
Full Example: Express App with Session Token Auth
const express = require('express');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const app = express();
const SESSION_SIGNING_KEY = process.env.SESSION_SIGNING_KEY;
const CLIENT_ID = process.env.CLIENT_ID;
// Verify HMAC on initial iframe load
app.get('/', (req, res) => {
const { hmac, ...params } = req.query;
const message = Object.keys(params)
.sort()
.map((key) => `${key}=${params[key]}`)
.join('&');
const expectedHmac = crypto
.createHmac('sha256', SESSION_SIGNING_KEY)
.update(message)
.digest('hex');
const isValid =
hmac.length === expectedHmac.length &&
crypto.timingSafeEqual(
Buffer.from(hmac, 'hex'),
Buffer.from(expectedHmac, 'hex')
);
if (!isValid) return res.status(401).send('Invalid signature');
const timestamp = parseInt(params.timestamp, 10);
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) {
return res.status(401).send('Request expired');
}
res.render('app', { storeId: params.store_id });
});
// Middleware to verify session tokens on API routes
function verifySession(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ message: 'Missing session token' });
try {
const decoded = jwt.verify(token, SESSION_SIGNING_KEY, {
algorithms: ['HS256'],
audience: CLIENT_ID,
});
req.session = {
store_id: decoded.sub,
installation_id: decoded.sid,
app_id: decoded.app_id,
};
next();
} catch (err) {
return res.status(401).json({ message: 'Invalid session token' });
}
}
app.get('/api/data', verifySession, (req, res) => {
res.json({ store_id: req.session.store_id });
});
app.listen(5003);