Integrating a Third-Party App

Guide for third-party applications that want to authenticate users via the Calimatic Auth platform using standard OAuth 2.0 / OpenID Connect.

This guide is for Category 3 apps: new third-party applications that want to let users sign in with their Calimatic account.


Prerequisites

  • A web application with an HTTPS-capable callback URL
  • An OAuth 2.0 / OIDC client library for your language/framework (any standard library works)
  • Contact with a Calimatic administrator (optional -- dynamic registration is available)

1. Register as a Third-Party OAuth Client

Dynamic Registration

Register your application by calling the registration endpoint:

curl -X POST https://auth.calimatic.com/api/v1/oidc/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Third-Party App",
    "redirect_uris": [
      "https://myapp.com/auth/callback",
      "http://localhost:5173/auth/callback"
    ],
    "grant_types": ["authorization_code", "refresh_token"],
    "scope": "openid profile email",
    "client_uri": "https://myapp.com",
    "logo_uri": "https://myapp.com/logo.png"
  }'

Response (201 Created):

{
  "client_id": "cca_aBcDeFgHiJkL...",
  "client_secret": "ccas_xYzAbCdEfGhI...",
  "client_secret_expires_at": 0,
  "client_name": "My Third-Party App",
  "redirect_uris": [
    "https://myapp.com/auth/callback",
    "http://localhost:5173/auth/callback"
  ],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "client_secret_post",
  "scope": "openid profile email"
}

Store the client_secret securely. It is returned only once.

Rate limit: 5 registrations per minute per IP address.

Public Clients (SPAs / Mobile Apps)

If your app runs entirely in the browser (no server-side secret storage), register as a public client:

curl -X POST https://auth.calimatic.com/api/v1/oidc/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My SPA",
    "redirect_uris": ["https://myapp.com/callback"],
    "grant_types": ["authorization_code"],
    "token_endpoint_auth_method": "none",
    "scope": "openid profile email"
  }'

Public clients do not receive a client_secret and must always use PKCE.


2. Standard OIDC Integration

Discovery URL

Point your OIDC library to the discovery document and it will auto-configure all endpoints:

https://auth.calimatic.com/.well-known/openid-configuration

This advertises:

EndpointURL
Authorizationhttps://auth.calimatic.com/api/v1/oidc/authorize
Tokenhttps://auth.calimatic.com/api/v1/oidc/token
UserInfohttps://auth.calimatic.com/api/v1/oidc/userinfo
JWKShttps://auth.calimatic.com/api/v1/oidc/jwks
End Sessionhttps://auth.calimatic.com/api/v1/oidc/end-session
Introspectionhttps://auth.calimatic.com/api/v1/oidc/introspect
Revocationhttps://auth.calimatic.com/api/v1/oidc/revoke

Scopes

Third-party apps receive the standard OIDC scopes:

ScopeClaims Granted
openidsub (required)
profilename, given_name, family_name, picture, updated_at
emailemail, email_verified
phonephone_number

Third-party apps cannot request organization or permissions scopes unless explicitly approved by a Calimatic administrator.

PKCE (Required)

PKCE (Proof Key for Code Exchange) is required for all clients. Use the S256 method:

code_verifier  = random 43-128 character string
code_challenge = BASE64URL(SHA256(code_verifier))

Authorization Flow

1. Your app redirects the user to /api/v1/oidc/authorize
   with client_id, redirect_uri, response_type=code,
   scope, state, code_challenge, code_challenge_method=S256

2. User authenticates on the Calimatic login page

3. User approves consent (first-time only)

4. Calimatic redirects to your callback:
   https://myapp.com/auth/callback?code=abc123&state=xyz

5. Your server exchanges the code for tokens:
   POST /api/v1/oidc/token
   with code, redirect_uri, client_id, client_secret, code_verifier

6. You receive access_token, id_token, refresh_token

7. Fetch user profile:
   GET /api/v1/oidc/userinfo
   Authorization: Bearer <access_token>

Example: Node.js / Express

import { randomBytes, createHash } from "crypto";
import express from "express";

const app = express();
const ISSUER = "https://auth.calimatic.com";
const CLIENT_ID = process.env.CALIMATIC_CLIENT_ID!;
const CLIENT_SECRET = process.env.CALIMATIC_CLIENT_SECRET!;
const REDIRECT_URI = "https://myapp.com/auth/callback";

// Generate PKCE pair
function generatePKCE() {
  const verifier = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  return { verifier, challenge };
}

// Login route
app.get("/login", (req, res) => {
  const { verifier, challenge } = generatePKCE();
  const state = randomBytes(16).toString("hex");

  // Store in session for callback verification
  req.session.oauthState = state;
  req.session.codeVerifier = verifier;

  const url = new URL(`${ISSUER}/api/v1/oidc/authorize`);
  url.searchParams.set("client_id", CLIENT_ID);
  url.searchParams.set("redirect_uri", REDIRECT_URI);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "openid profile email");
  url.searchParams.set("state", state);
  url.searchParams.set("code_challenge", challenge);
  url.searchParams.set("code_challenge_method", "S256");

  res.redirect(url.toString());
});

// Callback route
app.get("/auth/callback", async (req, res) => {
  const { code, state } = req.query;

  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(400).send("Invalid state");
  }

  // Exchange code for tokens
  const tokenRes = await fetch(`${ISSUER}/api/v1/oidc/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: code as string,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      code_verifier: req.session.codeVerifier,
    }),
  });

  const tokens = await tokenRes.json();

  // Fetch user info
  const userRes = await fetch(`${ISSUER}/api/v1/oidc/userinfo`, {
    headers: { Authorization: `Bearer ${tokens.access_token}` },
  });
  const user = await userRes.json();

  // Create session in your app
  req.session.user = user;
  req.session.accessToken = tokens.access_token;

  res.redirect("/dashboard");
});

Example: Python / Flask

import os, hashlib, base64, secrets
from flask import Flask, redirect, request, session

app = Flask(__name__)
ISSUER = "https://auth.calimatic.com"
CLIENT_ID = os.environ["CALIMATIC_CLIENT_ID"]
CLIENT_SECRET = os.environ["CALIMATIC_CLIENT_SECRET"]
REDIRECT_URI = "https://myapp.com/auth/callback"

def generate_pkce():
    verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b"=").decode()
    challenge = base64.urlsafe_b64encode(
        hashlib.sha256(verifier.encode()).digest()
    ).rstrip(b"=").decode()
    return verifier, challenge

@app.route("/login")
def login():
    verifier, challenge = generate_pkce()
    state = secrets.token_hex(16)
    session["oauth_state"] = state
    session["code_verifier"] = verifier

    url = (
        f"{ISSUER}/api/v1/oidc/authorize"
        f"?client_id={CLIENT_ID}"
        f"&redirect_uri={REDIRECT_URI}"
        f"&response_type=code"
        f"&scope=openid+profile+email"
        f"&state={state}"
        f"&code_challenge={challenge}"
        f"&code_challenge_method=S256"
    )
    return redirect(url)

@app.route("/auth/callback")
def callback():
    if request.args.get("state") != session.get("oauth_state"):
        return "Invalid state", 400

    import requests
    tokens = requests.post(f"{ISSUER}/api/v1/oidc/token", data={
        "grant_type": "authorization_code",
        "code": request.args["code"],
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "code_verifier": session["code_verifier"],
    }).json()

    user = requests.get(f"{ISSUER}/api/v1/oidc/userinfo", headers={
        "Authorization": f"Bearer {tokens['access_token']}"
    }).json()

    session["user"] = user
    return redirect("/dashboard")

3. What Data Third-Party Apps Receive

With the standard openid profile email scopes, your app receives:

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Jane Smith",
  "given_name": "Jane",
  "family_name": "Smith",
  "email": "jane@school.com",
  "email_verified": true,
  "picture": "https://lh3.googleusercontent.com/photo.jpg",
  "updated_at": 1700000000
}

What You Do NOT Receive

  • Organization details (organization_id, organization_memberships)
  • Platform permissions or roles
  • Phone number (unless phone scope is requested)
  • Internal user type

To request extended scopes, contact a Calimatic administrator to have your client approved for organization or permissions scopes.


4. User Provisioning via API

If your app needs to create users within a Calimatic organization (e.g., onboarding users from your platform), use the Provisioning API:

curl -X POST https://auth.calimatic.com/api/v1/users/provision \
  -H "Content-Type: application/json" \
  -H "x-client-id: cca_your_client_id" \
  -H "x-client-secret: ccas_your_client_secret" \
  -d '{
    "email": "newuser@example.com",
    "firstName": "New",
    "lastName": "User",
    "organizationId": "org-uuid-here",
    "role": "member",
    "sendInviteEmail": true
  }'
const response = await fetch("https://auth.calimatic.com/api/v1/users/provision", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-client-id": CLIENT_ID,
    "x-client-secret": CLIENT_SECRET,
  },
  body: JSON.stringify({
    email: "newuser@example.com",
    firstName: "New",
    lastName: "User",
    organizationId: orgId,
    role: "member",
    sendInviteEmail: true,
  }),
});

When your app client provisions a user, it automatically receives a license for your application.

For the full API reference including bulk provisioning, see User Management API.


5. Set Up Webhooks for User Events

Subscribe to user lifecycle events so your app stays in sync with Calimatic Auth:

curl -X POST https://auth.calimatic.com/api/v1/admin/webhooks \
  -H "Content-Type: application/json" \
  -H "x-client-id: cca_your_client_id" \
  -H "x-client-secret: ccas_your_client_secret" \
  -d '{
    "url": "https://myapp.com/webhooks/calimatic",
    "events": [
      "user.created",
      "user.updated",
      "user.deleted",
      "user.suspended",
      "user.reactivated"
    ],
    "secret": "whsec_your_webhook_secret_here",
    "description": "Sync user changes to MyApp"
  }'

Available Event Types

EventTriggered When
user.createdA new user is provisioned
user.updatedUser profile is updated
user.deletedUser is deleted
user.suspendedUser account is suspended
user.reactivatedSuspended user is reactivated
user.deactivatedUser account is deactivated
user.password_resetPassword reset is triggered
user.invitedUser receives an invitation
organization.createdNew organization is created
organization.updatedOrganization details are updated
license.assignedApp license is assigned to a user
license.revokedApp license is revoked from a user

Webhook Payload Format

{
  "event": "user.created",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "userId": "uuid-...",
    "email": "jane@school.edu",
    "firstName": "Jane",
    "lastName": "Smith",
    "organizationId": "org-uuid-...",
    "isNewUser": true,
    "createdAt": "2025-01-15T10:30:00.000Z"
  }
}

Verifying Webhook Signatures

If you provided a secret, each delivery includes an X-Webhook-Signature header. Verify it using HMAC-SHA256:

import { createHmac } from "crypto";

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return signature === expected;
}

// In your webhook handler
app.post("/webhooks/calimatic", (req, res) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const isValid = verifyWebhookSignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET!
  );

  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  // Process the event
  const { event, data } = req.body;
  // ...

  res.status(200).send("OK");
});

For the full webhook API reference, see Webhooks API.


6. SCIM Integration for Enterprise Provisioning

For enterprise customers that need automated user provisioning and deprovisioning, Calimatic Auth supports SCIM 2.0.

SCIM (System for Cross-domain Identity Management) allows identity providers like Azure AD, Okta, and OneLogin to automatically sync users with your application via Calimatic Auth.

For SCIM endpoints, schema mapping, and configuration details, see the SCIM API Reference.


7. Rate Limits and Best Practices

Rate Limits

EndpointLimitWindow
Token endpoint20 requests1 minute per client
Authorization endpoint30 requests1 minute per IP
UserInfo endpoint60 requests1 minute per token
Introspection endpoint60 requests1 minute per client
Registration endpoint5 requests1 minute per IP

When rate limited, you receive a 429 response with Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers.

Best Practices

Token management:

  • Cache access tokens and reuse them until they expire (default: 1 hour)
  • Refresh tokens proactively 5 minutes before expiry
  • Never store refresh tokens in localStorage for browser apps

Security:

  • Always use PKCE (S256), even for confidential clients
  • Always validate the state parameter on callback to prevent CSRF
  • Verify JWTs locally using the JWKS endpoint instead of calling introspection for every request
  • Store client_secret in environment variables, never in source code

Error handling:

  • Handle access_denied gracefully (user may deny consent)
  • On invalid_token, attempt a token refresh before prompting re-login
  • Implement exponential backoff for 429 responses

User data:

  • Cache user profile data in your own database to reduce calls to userinfo
  • Use webhooks to stay synchronized instead of polling

Redirect URIs:

  • Register exact redirect URIs (no wildcards)
  • Always use HTTPS in production
  • Register separate URIs for each environment (production, staging, development)

Related Documentation