Authentication Guide

This guide covers the headless authentication model, app client credentials, token lifecycle, auto-refresh, and security requirements for integrating with the Calimatic Auth API.


What Is Headless Auth?

The headless auth model lets your server-side application authenticate users via direct API calls — no browser redirects, no OIDC flows, no user-facing login pages on Calimatic's domain.

Your backend authenticates on behalf of users using:

  1. App client credentials (clientId + clientSecret) — identifies your application
  2. User credentials (email + password) — authenticates the end user

All requests go server-to-server. The Calimatic Auth API returns tokens that your application manages.


App Client Credentials

Every headless API request requires your app client credentials. These are issued when you register an application in the Calimatic Auth admin panel.

Required headers on every request:

HeaderValue
x-client-idYour app client ID
x-client-secretYour app client secret

Example .env file:

CALIMATIC_CLIENT_ID=cca_xxxxxxxxxxxx
CALIMATIC_CLIENT_SECRET=ccas_xxxxxxxxxxxx

The SDK reads these automatically when you instantiate HeadlessAuthClient:

const client = new HeadlessAuthClient({
  baseUrl: 'https://auth.calimatic.com',
  clientId: process.env.CALIMATIC_CLIENT_ID!,
  clientSecret: process.env.CALIMATIC_CLIENT_SECRET!,
});

DO NOT hardcode credentials in source code. If your clientSecret is committed to version control, rotate it immediately in the Calimatic Auth admin panel.

DO NOT use the headless SDK in client-side (browser) code. The SDK requires clientSecret, which must never be exposed to end users. See Server-Side Only Architecture below.


Token Lifecycle

A successful login returns three tokens and an expiry timestamp:

TokenLifetimePurpose
accessToken~15 minutesBearer token for authenticated API calls (mfa.status, etc.)
refreshToken~7 daysUsed to get a new token set when the access token expires
idToken~15 minutesJWT with user identity claims (userId, email, firstName, etc.)
expiresAtISO 8601 timestamp when the access token expires

Flow diagram:

Login
  |
  v
Get Tokens (accessToken, refreshToken, idToken)
  |
  v
Use accessToken for authenticated API calls
  |
  v
accessToken expires (check expiresAt)
  |
  v
Call auth.refresh({ refreshToken })
  |
  v
Get NEW token set (store new refreshToken — old one is invalidated)
  |
  v
Continue using new accessToken

Token rotation: After calling auth.refresh, the old refresh token is invalidated. Always store the new refreshToken from the refresh response.

let tokens = await client.auth.login({ email, password });

// Store tokens in your server-side session
let { accessToken, refreshToken } = tokens;

// When access token expires, refresh:
const newTokens = await client.auth.refresh({ refreshToken });
accessToken = newTokens.accessToken;
refreshToken = newTokens.refreshToken; // Always update — old one is now invalid

Auto-Refresh with the onRefresh Callback

Access-token-protected methods (such as mfa.status, mfa.enroll, mfa.backupCodes.count) automatically retry once on a 401 INVALID_TOKEN response. To enable this, provide an onRefresh callback that returns the current refresh token.

let currentRefreshToken = '';

const client = new HeadlessAuthClient(
  {
    baseUrl: 'https://auth.calimatic.com',
    clientId: process.env.CALIMATIC_CLIENT_ID!,
    clientSecret: process.env.CALIMATIC_CLIENT_SECRET!,
  },
  {
    onRefresh: () => ({ refreshToken: currentRefreshToken }),
  },
);

// Login and store tokens
const tokens = await client.auth.login({ email, password });
currentRefreshToken = tokens.refreshToken;

// This call auto-refreshes if the access token has expired:
const status = await client.mfa.status(tokens.accessToken);

How auto-refresh works:

  1. SDK calls the API with the current access token
  2. If the API returns 401 INVALID_TOKEN, the SDK calls your onRefresh callback
  3. The callback returns the stored refresh token
  4. The SDK calls auth.refresh to get a new token set
  5. The SDK retries the original request with the new access token

Concurrent refresh deduplication: Multiple concurrent calls that all receive a 401 will share a single refresh request via a promise-based mutex. Only one auth.refresh HTTP request fires regardless of how many calls need it simultaneously. This prevents invalid_grant race conditions.

If you do not provide onRefresh, a 401 INVALID_TOKEN throws immediately instead of retrying.


Security Warnings

DO NOT use the headless SDK in client-side (browser) code. The SDK requires clientSecret, which must never be exposed to end users. Client-side code is visible to anyone who opens DevTools.

DO NOT hardcode credentials in source code. Always use environment variables. Rotate credentials immediately if they are ever committed to version control.

DO NOT store access tokens in localStorage or cookies accessible to JavaScript. Use httpOnly cookies or server-side session storage. Access tokens are bearer credentials — anyone who can read them can impersonate the user.

DO NOT log tokens in production. Access tokens contain user identity claims. Token values must not appear in application logs, error tracking systems, or analytics.


Server-Side Only Architecture

The headless SDK is designed exclusively for server-to-server communication. Your backend calls Calimatic Auth; your frontend talks to your backend.

Browser / Mobile App
        |
        | (your API — session cookies, JWT, etc.)
        v
Your Backend Server
        |
        | (HeadlessAuthClient — clientId + clientSecret)
        v
Calimatic Auth API

Your backend owns the tokens. Your frontend never sees clientSecret or raw access tokens from Calimatic Auth.

If you need client-side authentication (e.g., SPAs, mobile apps), use the redirect-based OIDC flow instead. The OIDC flow does not require a server-side secret and is designed for public clients. See the OIDC Integration Guide for setup instructions.


Summary

WhatRule
clientSecret placementServer-side environment variable only
accessToken storagehttpOnly cookies or server-side session — never localStorage
Token rotationAlways save the new refreshToken after every auth.refresh call
Auto-refreshProvide onRefresh callback; concurrent refreshes are deduplicated
Client-side authUse OIDC redirect flow — not the headless SDK
Token loggingNever log token values in production