BetterIAM
Server API

assertions

Assertions are short-lived signed tokens that tell another service who is calling: the caller's identity, tenant, roles, groups, and whether they used MFA.

Assertions are short-lived signed tokens that tell another service who is calling: the caller's identity, tenant, roles, groups, and whether they used MFA. They solve the "internal service" problem: a reporting service or a worker behind your API needs to trust the caller, but should not share your session store, hold the deployment secret, or call Better IAM on every request. The downstream service verifies the token locally with a derived key. An assertion grants nothing inside Better IAM itself and cannot be used as a credential for it.

Verifying assertions downstream

An assertion is a compact JSON Web Token signed with HS256 under a key derived from the deployment secret. Give the downstream service that key, iam.assertionKey() (64 hex characters), never the secret itself, and verify each token with verifyAssertion:

import { verifyAssertion } from 'better-iam';

const claims = verifyAssertion(token, {
  key: process.env.IAM_ASSERTION_KEY!, // iam.assertionKey(), or iam.assertionKeys() during a secret rotation
  audience: 'reports',
  issuer: 'https://identity.example.com', // optional: the deployment's base URL origin
});
// claims.sub, claims.tid, claims.roles, claims.groups, claims.mfa

It checks the signature in constant time, requires aud to equal your audience (and iss your issuer, when given), and rejects tokens past exp or issued in the future, with 30 seconds of clock tolerance (toleranceSeconds). Any failure throws INVALID_ASSERTION (401). While you rotate the deployment secret, pass the array from iam.assertionKeys() so tokens signed under the previous secret keep verifying. In edge runtimes, verifyAssertionToken from @better-iam/next/edge applies the same rules with Web Crypto.

The key is symmetric: a service that holds it can verify assertions and could also create them. Share it only with services you trust. Assertions cannot be revoked, which is why they are short-lived: disabling a person stops new assertions, but tokens already issued stay valid until they expire.

Claims

ClaimMeaning
issThe deployment's base URL origin.
subThe caller's identity id.
audThe audience the token was issued for.
iat, expIssue and expiry times, in Unix seconds.
jtiA unique token id, for replay detection.
tidThe tenant the assertion was issued for.
kindThe caller's session kind: user, api-key, role, or session-token.
mfaWhether the session was verified with MFA.
methodHow the person signed in, when known.
impersonatorIdThe administrator behind a "view as" session; sub is then the member.
name, emailThe caller's display name, and email when set.
rolesSorted ids of the roles the caller holds right now, directly or through groups.
groupsSorted ids of the caller's current groups.
extThe extra claims you passed as claims.

roles counts only bindings that grant at this moment: started, not expired, inside their access window, and, for eligible bindings, activated.

Methods1
Serveriam.api.assertions
Clientclient.assertions
HTTPPOST /api/iam/assertions/*
MethodWhat it doesAccess
issueIssues a signed assertion about the caller for one audience, valid for five minutes by default.Credential

issue

Issues a signed assertion about the caller for one audience, valid for five minutes by default.

POST/api/iam/assertions/issue
client.assertions.issue()Credential

Used inOperations recipes

  • Permission: iam:assertions:create on iam/{audience}.
  • Audited as: iam:assertions:create, on the audience.
  • Errors: INVALID_INPUT when audience is not a URL-safe identifier, ttlSeconds is outside 10 to 3600, or claims exceeds 4 KiB of JSON or reuses a standard claim name; ACCESS_DENIED when the caller may not obtain assertions for that audience, or presents a session token (sts.getSessionToken) restricted by a session policy, including one inherited from a scoped API key (recorded as a denial).

Because the permission is checked on the audience, administrators decide which roles may obtain tokens for which services: allow iam:assertions:create on iam/reports for analysts, and on iam/billing-worker only for the billing role. The audience starts with a letter or digit and may contain letters, digits, ., _, :, /, and -, up to 256 characters. For an assumed-role session, roles holds only the assumed role and groups is empty. Restricted session tokens are refused because the roles claim would describe more access than the token allows.

// In your API: forward the caller to the reports service.
const { token, expiresAt } = await iam.api.assertions.issue(credential, {
  tenantId,
  audience: 'reports',
  ttlSeconds: 120,
  claims: { requestId },
});
await fetch('https://reports.internal/run', { headers: { authorization: `Bearer ${token}` } });
Input

Prop

Type

Returns

Prop

Type

Example HTTP request

Only the required fields are shown; replace each <placeholder>. The response is { "data": … } on success or { "error": { "code", "message" } }.

curl -X POST "$IAM_URL/api/iam/assertions/issue" \
  -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-Better-IAM: 1" \
  -d '{
  "tenantId": "<tenantId>",
  "audience": "<audience>"
}'
Signature
iam.api.assertions.issue(
  credential: CredentialInput,
  input: {
    tenantId: string;
    audience: string;
    ttlSeconds?: number;
    claims?: Record<string, Json>;
  },
): Promise<{ token: string; expiresAt: number; claims: AssertionClaims }>

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page