SeloraXDEVELOPERS

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

  1. The merchant navigates to your app inside the SeloraX dashboard
  2. The dashboard requests embed parameters from the platform
  3. The platform generates an HMAC-signed iframe URL and a short-lived JWT session token
  4. The dashboard loads your app in an iframe with the signed URL
  5. 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)&timestamp=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

  1. All query parameters except hmac are collected
  2. Parameters are sorted alphabetically by key
  3. Sorted key=value pairs are joined with &
  4. The resulting string is signed with your app's session_signing_key using HMAC-SHA256

Message format:

host=base64url(admin.selorax.io)&store_id=22&timestamp=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
}
ClaimDescription
issIssuer: the SeloraX dashboard URL
destDestination: your app's URL
audAudience: your app's client_id
subSubject: the store_id
sidSession ID: the installation_id
app_idThe app's numeric ID
jtiUnique token identifier (UUID)
iatIssued at (Unix timestamp)
expExpires 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
}

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