Integration Checklist
Every application that connects to the Calimatic Auth platform — whether via OIDC, the Headless Auth API, or the SDK — must handle the items below. Use this checklist before going live.
1. App Registration
- Register your app as an App Client at
/admin/app-clients(or viaPOST /api/v1/oidc/register) - Store credentials securely —
client_idandclient_secretin environment variables, never in source code or client-side bundles - Set Redirect URIs — Register all callback URLs for every environment (production, staging, localhost)
- Set Application URL — Your app's homepage (
clientUri) - Set Login Page URL — Your app's login page (
loginUrl). Required for headless/SDK apps so the platform can redirect users back to your login page (e.g., after email verification) - Select scopes — Choose only the scopes your app needs (
openid,profile,email,organization,permissions, etc.) - Assign app licenses to users — Users must be assigned your app in the admin panel (
/admin/users→ Manage Apps) before they can log in. The platform enforces this: users without a license will get anaccess_denied(OIDC) orAPP_NOT_LICENSED(headless) error at login time. Platform admins bypass this check.
License enforcement: The auth platform checks
user_app_licensesat both OIDC authorization and headless login. If the user doesn't have a license for the requesting app, login is blocked. The JWT access token also includes alicensesclaim listing all apps the user is licensed for.
2. Authentication Flow
Choose one approach and implement it fully:
Option A: OIDC / OAuth 2.0 (browser-based login)
- Configure OIDC — Point your app to
https://auth.calimatic.com/.well-known/openid-configuration - Implement Authorization Code + PKCE flow — Generate
code_verifierandcode_challengefor every login - Exchange code for tokens —
POST /api/v1/oidc/tokenwith the authorization code - Validate tokens — Verify the
id_tokensignature using JWKS from/api/v1/oidc/jwks - Store tokens server-side — Access tokens and refresh tokens must never be exposed to the browser
Option B: Headless Auth API / SDK (API-based login)
- Install the SDK —
npm install @calimatic/authand useHeadlessAuthClient - Implement login —
client.auth.login()orPOST /api/v1/auth/headless/login - Implement signup —
client.auth.signup()orPOST /api/v1/auth/headless/signup - Handle MFA challenges — Login may return
mfaRequired: true— implementclient.mfa.verify() - Send app client credentials — Every request must include
x-client-idandx-client-secretheaders
Option C: Hybrid — Headless + "Sign in with Calimatic" SSO Button
Apps using the Headless API (Option B) can also offer a "Sign in with Calimatic" button for SSO. This gives users two ways to log in: email/password via your form, or SSO via the Calimatic Auth platform (shared session across all Calimatic apps). Both use the same app client.
Prerequisites:
- Grant type — Ensure
authorization_codeis enabled on your app client (in addition to any headless usage) - Redirect URI — Add a callback URL for the OIDC flow (e.g.,
https://myapp.com/auth/callback)
Login page UI:
{/* Your existing headless login form */}
<form onSubmit={handleEmailPasswordLogin}>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Sign In</button>
</form>
<div className="divider">or</div>
{/* SSO button — redirects to Calimatic Auth */}
<a href="/api/auth/sso">
<button>Sign in with Calimatic</button>
</a>
SSO redirect route (server-side):
import { CalimaticAuth, generatePKCE } from '@calimatic/auth';
const auth = new CalimaticAuth({
baseUrl: 'https://auth.calimatic.com',
clientId: process.env.CALIMATIC_CLIENT_ID!,
clientSecret: process.env.CALIMATIC_CLIENT_SECRET!,
redirectUri: 'https://myapp.com/auth/callback',
});
export async function GET() {
const { codeVerifier, codeChallenge } = await generatePKCE();
// Store codeVerifier in a secure HTTP-only cookie or server session
const url = await auth.getAuthorizationUrl({
pkce: true,
codeChallenge,
scopes: ['openid', 'profile', 'email', 'organization'],
});
// Redirect user to url
}
Callback handler:
export async function GET(request: Request) {
const code = new URL(request.url).searchParams.get('code');
const codeVerifier = // retrieve from cookie/session
const tokens = await auth.exchangeCode(code, codeVerifier);
const userInfo = await auth.getUserInfo(tokens.accessToken);
// Match by email or userId to link to existing app account
// Create a session in your app
}
How it works with existing users:
- Users created via headless signup, admin invitation, or any other method already exist in Calimatic Auth
- When they click "Sign in with Calimatic", they log in with the same credentials on
auth.calimatic.com - Your callback handler receives their
userId,email, andorganizationIdfrom the token - Match the user by
emailoruserIdto their existing account in your app — no migration needed - If the user is already logged into another Calimatic app, they won't need to enter credentials again (true SSO)
Social Login: Google, Microsoft, and Apple
The Calimatic Auth platform supports Sign in with Google, Sign in with Microsoft, and Sign in with Apple out of the box via Keycloak identity providers.
If your app uses OIDC (Option A or the "Sign in with Calimatic" button in Option C):
Social login works automatically. When users are redirected to the Calimatic Auth login page, they see Google, Microsoft, and Apple buttons alongside the email/password form. No extra work needed in your app.
If your app wants social login buttons directly on your own login page:
You can add "Sign in with Google" and "Sign in with Microsoft" buttons that skip the Calimatic Auth login page and go directly to the social provider. This uses the same OIDC flow as Option C but with a kc_idp_hint parameter:
// Reuse the same CalimaticAuth instance from Option C
// "Sign in with Google" button handler
async function handleGoogleSignIn() {
const { codeVerifier, codeChallenge } = await generatePKCE();
// Store codeVerifier in session/cookie
const url = await auth.getAuthorizationUrl({
pkce: true,
codeChallenge,
scopes: ['openid', 'profile', 'email', 'organization'],
extraParams: { kc_idp_hint: 'google' },
});
// Redirect user to url — goes directly to Google, not the Calimatic login page
}
// "Sign in with Microsoft" button handler
async function handleMicrosoftSignIn() {
const { codeVerifier, codeChallenge } = await generatePKCE();
const url = await auth.getAuthorizationUrl({
pkce: true,
codeChallenge,
scopes: ['openid', 'profile', 'email', 'organization'],
extraParams: { kc_idp_hint: 'microsoft' },
});
// Redirect user to url — goes directly to Microsoft
}
The callback handler is the same as Option C — the tokens contain the user's email, userId, and profile regardless of which provider they used.
Login page UI with all options:
<form onSubmit={handleEmailPasswordLogin}>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Sign In</button>
</form>
<div className="divider">or</div>
<button onClick={handleGoogleSignIn}>
Sign in with Google
</button>
<button onClick={handleMicrosoftSignIn}>
Sign in with Microsoft
</button>
<button onClick={() => redirect('/api/auth/sso')}>
Sign in with Calimatic
</button>
Account linking: If a user signs up with email/password and later signs in with Google (same email), Keycloak automatically links the accounts. No duplicate users are created.
3. Email Verification
- Handle unverified emails — After signup, the user's email is unverified and their Keycloak account is disabled. They cannot log in until verified.
- Verification email is sent automatically — The platform sends a verification email on signup with a link to
https://auth.calimatic.com/verify-email?token=... - After verification, user is redirected — If your app has
loginUrlset, the user is sent to your login page. Otherwise they go toclientUri. - Resend verification — Implement a "Resend verification email" button using
client.email.sendVerification()orPOST /api/v1/auth/headless/email/verify-send - Handle
EMAIL_NOT_VERIFIEDerror — When a user tries to log in with an unverified email, show a helpful message with a resend option
4. Token Lifecycle
- Implement token refresh — Access tokens expire (default: 1 hour). Refresh before expiry using
client.auth.refresh()orPOST /api/v1/oidc/tokenwithgrant_type=refresh_token - Handle
TOKEN_EXPIREDerrors — If a refresh token also expires, redirect the user to log in again - Use auto-refresh (SDK) — Pass an
onRefreshcallback toHeadlessAuthClientto automatically refresh tokens
const client = new HeadlessAuthClient({
baseUrl: 'https://auth.calimatic.com',
clientId: process.env.CALIMATIC_CLIENT_ID!,
clientSecret: process.env.CALIMATIC_CLIENT_SECRET!,
onRefresh: (newTokens) => {
// Persist the new tokens in your session store
session.accessToken = newTokens.accessToken;
session.refreshToken = newTokens.refreshToken;
},
});
- Use the
licensesJWT claim — The access token includes alicensesarray (e.g.,["calimatic-pulse", "connect"]). Use this to check entitlements without calling the resolve API:
import { jwtDecode } from 'jwt-decode';
const decoded = jwtDecode(accessToken);
const licenses = decoded.licenses ?? [];
// Check if user has a specific app license
if (licenses.includes('my-feature-app')) {
// Show feature
}
Note: Since the platform enforces licenses at login, you don't need to call
/api/v1/users/resolvejust to checkhasLicenseanymore. The token is proof the user is licensed. Thelicensesclaim is useful for checking access to other apps or for feature gating.
5. Logout & Session Cleanup
- Implement logout — Clear your app's session AND call the auth platform to end the session:
- OIDC: Redirect to
GET /api/v1/oidc/end-session?id_token_hint=...&post_logout_redirect_uri=... - Headless:
client.auth.logout()orPOST /api/v1/auth/headless/logout
- OIDC: Redirect to
- Revoke tokens — Optionally revoke the refresh token via
POST /api/v1/oidc/revoke - Handle session revocation — If the platform revokes a session (admin action), your app should detect the invalid token and redirect to login
6. User Lifecycle Sync (Critical)
This is the most commonly missed integration step. Your app must stay in sync with the auth platform when users or organizations change.
Webhook subscriptions (recommended)
Register webhooks at /admin > Webhooks, or via POST /api/v1/admin/webhooks. Subscribe to these events:
| Event | What to do in your app |
|---|---|
user.created | Create the user in your local database |
user.updated | Update user profile (name, email, etc.) |
user.deleted | Delete the user and their data from your app |
user.suspended | Disable the user's access, revoke active sessions |
user.reactivated | Re-enable the user's access |
user.deactivated | Disable the user's access (permanent) |
organization.created | Create the org in your app if needed |
organization.updated | Update org details |
license.assigned | Pre-create the user's local account if it doesn't exist (optional — JIT provisioning handles this too) |
license.revoked | Revoke the user's active sessions immediately and remove access. Don't wait for next login — the user should lose access right away. |
Just-in-time (JIT) user provisioning
When a user is assigned an app in the admin panel and then logs in, they may not have a local account in your app yet. Your login/callback handler must handle this:
- Check if user exists locally after authentication — Look up by
emailorsub(userId) from the token claims - Create local account if missing — Use token claims (
email,given_name,family_name,sub,organization_id) to create the user in your app's database - Link to auth platform — Store the auth platform's
userId(subclaim) in your local user record for future lookups
// After receiving tokens (OIDC callback or headless login)
const userInfo = tokens.user ?? await auth.getUserInfo(tokens.accessToken);
let localUser = await db.users.findByEmail(userInfo.email);
if (!localUser) {
// User exists in auth but not in this app — just-in-time provision
localUser = await db.users.create({
email: userInfo.email,
firstName: userInfo.firstName ?? userInfo.given_name,
lastName: userInfo.lastName ?? userInfo.family_name,
authUserId: userInfo.userId ?? userInfo.sub,
organizationId: userInfo.organizationId ?? userInfo.organization_id,
});
}
// Create session for localUser
Why this matters: With license enforcement, admins can assign apps to users at any time. The user may already exist in the auth platform (invited to another app, signed up elsewhere) but not in your app. JIT provisioning ensures a seamless first login.
Syncing changes back to auth
When users or organizations are deleted in your app, you must also notify the auth platform:
- User deletion — Call
DELETE /api/v1/users/{userId}or the admin user deactivation endpoint to remove/deactivate the user on the platform - User profile updates — If your app allows profile edits, sync changes back via
PATCH /api/v1/users/{userId}or the admin API - Organization changes — If your app manages org settings, sync via the Organizations API
Webhook verification
- Verify HMAC signatures — Every webhook delivery includes an
x-webhook-signatureheader. Verify it with your webhook secret to prevent spoofing:
import { createHmac } from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === `sha256=${expected}`;
}
- Return 200 quickly — Process webhooks asynchronously. The platform retries on non-2xx responses with exponential backoff (max 5 retries over 24 hours).
- Handle idempotently — Webhooks may be delivered more than once. Use the event
idto deduplicate.
7. Password Management
- Implement forgot password — Use
client.password.resetSend()orPOST /api/v1/auth/headless/password/reset-send. The platform sends a reset email. - Implement password reset confirmation —
client.password.resetConfirm()orPOST /api/v1/auth/headless/password/reset-confirmwith the token from the email - Respect password policy — Fetch the policy via
client.security.passwordPolicy()and validate client-side before submission
8. MFA (if applicable)
- Handle MFA during login — When
login()returnsmfaRequired: true, prompt for the TOTP code and callclient.mfa.verify() - Allow MFA enrollment — Use
client.mfa.enroll()to get a QR code, thenclient.mfa.confirmEnrollment()to activate - Allow MFA disabling — Use
client.mfa.disable()with the user's current TOTP code - Backup codes — Show backup codes after enrollment. Users can regenerate via
client.mfa.backupCodes.regenerate()
9. Error Handling
- Catch
CalimaticAuthError— All SDK errors include acodeproperty fromAuthErrorCode - Handle common errors gracefully:
| Error Code | When it happens | What to show the user |
|---|---|---|
INVALID_CREDENTIALS | Wrong email/password | "Invalid email or password" |
EMAIL_NOT_VERIFIED | Unverified email login attempt | "Please verify your email" + resend button |
APP_NOT_LICENSED | User not assigned to this app | "You do not have access to this application. Please contact your administrator." |
ACCOUNT_LOCKED | Too many failed attempts | "Account locked, try again in X minutes" |
ACCOUNT_DISABLED | Admin disabled the account | "Your account has been disabled" |
MFA_REQUIRED | MFA needed | Show MFA input |
TOKEN_EXPIRED | Refresh token expired | Redirect to login |
RATE_LIMITED | Too many requests | "Please wait and try again" |
INVALID_CLIENT | Bad client credentials | Check your environment variables (don't show to user) |
access_denied (OIDC) | User not licensed for app (SSO flow) | Redirect error — show "You don't have access" page |
10. Security Checklist
- All API calls over HTTPS — Never call the auth API over plain HTTP
- Client secret server-side only — Never expose
client_secretin frontend code, mobile apps, or logs - Tokens server-side only — Store access/refresh tokens in server sessions, not localStorage or cookies accessible to JS
- PKCE for public clients — SPAs and mobile apps must use PKCE (Proof Key for Code Exchange)
- Validate redirect URIs — Only accept redirects to your registered URIs
- Don't log tokens — Ensure access tokens, refresh tokens, and client secrets are excluded from application logs
- Implement CSRF protection — Use the
stateparameter in OIDC flows
11. Testing
- Test signup → email verification → login — Full flow end-to-end
- Test with MFA enabled — If your app supports MFA
- Test token refresh — Let the access token expire and verify refresh works
- Test logout — Verify sessions are cleared on both your app and the platform
- Test webhook delivery — Use the webhook delivery log in the admin panel to verify events are received
- Test user deletion — Delete a user in your app and verify it syncs to auth. Delete in auth and verify your webhook handler removes them.
- Test error scenarios — Wrong password, locked account, expired token, rate limiting
- Test all environments — Verify redirect URIs work for localhost, staging, and production
Quick Reference: API Endpoints
| Purpose | Endpoint |
|---|---|
| OIDC Discovery | GET /.well-known/openid-configuration |
| Authorize | GET /api/v1/oidc/authorize |
| Token | POST /api/v1/oidc/token |
| UserInfo | GET /api/v1/oidc/userinfo |
| JWKS | GET /api/v1/oidc/jwks |
| Logout | GET /api/v1/oidc/end-session |
| Headless Login | POST /api/v1/auth/headless/login |
| Headless Signup | POST /api/v1/auth/headless/signup |
| Headless Refresh | POST /api/v1/auth/headless/refresh |
| Email Verify Send | POST /api/v1/auth/headless/email/verify-send |
| Email Verify Confirm | POST /api/v1/auth/headless/email/verify-confirm |
| Password Reset Send | POST /api/v1/auth/headless/password/reset-send |
| Password Reset Confirm | POST /api/v1/auth/headless/password/reset-confirm |
| Resolve User | GET /api/v1/users/resolve |
| Provision User | POST /api/v1/users/provision |
| Register Webhook | POST /api/v1/admin/webhooks |
Next Steps
- Headless Auth Quickstart — Get your first login working in 5 minutes
- Authentication Guide — Token lifecycle, auto-refresh, security
- Webhooks API Reference — Webhook management and event types
- Error Codes Reference — All error codes and troubleshooting
- User Management API — Provisioning, resolving, and managing users