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 via POST /api/v1/oidc/register)
  • Store credentials securelyclient_id and client_secret in 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 an access_denied (OIDC) or APP_NOT_LICENSED (headless) error at login time. Platform admins bypass this check.

License enforcement: The auth platform checks user_app_licenses at 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 a licenses claim 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_verifier and code_challenge for every login
  • Exchange code for tokensPOST /api/v1/oidc/token with the authorization code
  • Validate tokens — Verify the id_token signature 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 SDKnpm install @calimatic/auth and use HeadlessAuthClient
  • Implement loginclient.auth.login() or POST /api/v1/auth/headless/login
  • Implement signupclient.auth.signup() or POST /api/v1/auth/headless/signup
  • Handle MFA challenges — Login may return mfaRequired: true — implement client.mfa.verify()
  • Send app client credentials — Every request must include x-client-id and x-client-secret headers

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_code is 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, and organizationId from the token
  • Match the user by email or userId to 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 loginUrl set, the user is sent to your login page. Otherwise they go to clientUri.
  • Resend verification — Implement a "Resend verification email" button using client.email.sendVerification() or POST /api/v1/auth/headless/email/verify-send
  • Handle EMAIL_NOT_VERIFIED error — 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() or POST /api/v1/oidc/token with grant_type=refresh_token
  • Handle TOKEN_EXPIRED errors — If a refresh token also expires, redirect the user to log in again
  • Use auto-refresh (SDK) — Pass an onRefresh callback to HeadlessAuthClient to 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 licenses JWT claim — The access token includes a licenses array (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/resolve just to check hasLicense anymore. The token is proof the user is licensed. The licenses claim 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() or POST /api/v1/auth/headless/logout
  • 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:

EventWhat to do in your app
user.createdCreate the user in your local database
user.updatedUpdate user profile (name, email, etc.)
user.deletedDelete the user and their data from your app
user.suspendedDisable the user's access, revoke active sessions
user.reactivatedRe-enable the user's access
user.deactivatedDisable the user's access (permanent)
organization.createdCreate the org in your app if needed
organization.updatedUpdate org details
license.assignedPre-create the user's local account if it doesn't exist (optional — JIT provisioning handles this too)
license.revokedRevoke 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 email or sub (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 (sub claim) 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-signature header. 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 id to deduplicate.

7. Password Management

  • Implement forgot password — Use client.password.resetSend() or POST /api/v1/auth/headless/password/reset-send. The platform sends a reset email.
  • Implement password reset confirmationclient.password.resetConfirm() or POST /api/v1/auth/headless/password/reset-confirm with 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() returns mfaRequired: true, prompt for the TOTP code and call client.mfa.verify()
  • Allow MFA enrollment — Use client.mfa.enroll() to get a QR code, then client.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 a code property from AuthErrorCode
  • Handle common errors gracefully:
Error CodeWhen it happensWhat to show the user
INVALID_CREDENTIALSWrong email/password"Invalid email or password"
EMAIL_NOT_VERIFIEDUnverified email login attempt"Please verify your email" + resend button
APP_NOT_LICENSEDUser not assigned to this app"You do not have access to this application. Please contact your administrator."
ACCOUNT_LOCKEDToo many failed attempts"Account locked, try again in X minutes"
ACCOUNT_DISABLEDAdmin disabled the account"Your account has been disabled"
MFA_REQUIREDMFA neededShow MFA input
TOKEN_EXPIREDRefresh token expiredRedirect to login
RATE_LIMITEDToo many requests"Please wait and try again"
INVALID_CLIENTBad client credentialsCheck 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_secret in 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 state parameter 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

PurposeEndpoint
OIDC DiscoveryGET /.well-known/openid-configuration
AuthorizeGET /api/v1/oidc/authorize
TokenPOST /api/v1/oidc/token
UserInfoGET /api/v1/oidc/userinfo
JWKSGET /api/v1/oidc/jwks
LogoutGET /api/v1/oidc/end-session
Headless LoginPOST /api/v1/auth/headless/login
Headless SignupPOST /api/v1/auth/headless/signup
Headless RefreshPOST /api/v1/auth/headless/refresh
Email Verify SendPOST /api/v1/auth/headless/email/verify-send
Email Verify ConfirmPOST /api/v1/auth/headless/email/verify-confirm
Password Reset SendPOST /api/v1/auth/headless/password/reset-send
Password Reset ConfirmPOST /api/v1/auth/headless/password/reset-confirm
Resolve UserGET /api/v1/users/resolve
Provision UserPOST /api/v1/users/provision
Register WebhookPOST /api/v1/admin/webhooks

Next Steps