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.mfaIt 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
| Claim | Meaning |
|---|---|
iss | The deployment's base URL origin. |
sub | The caller's identity id. |
aud | The audience the token was issued for. |
iat, exp | Issue and expiry times, in Unix seconds. |
jti | A unique token id, for replay detection. |
tid | The tenant the assertion was issued for. |
kind | The caller's session kind: user, api-key, role, or session-token. |
mfa | Whether the session was verified with MFA. |
method | How the person signed in, when known. |
impersonatorId | The administrator behind a "view as" session; sub is then the member. |
name, email | The caller's display name, and email when set. |
roles | Sorted ids of the roles the caller holds right now, directly or through groups. |
groups | Sorted ids of the caller's current groups. |
ext | The 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.
| Method | What it does | Access |
|---|---|---|
issue | Issues 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.
Used inOperations recipes
- Permission:
iam:assertions:createoniam/{audience}. - Audited as:
iam:assertions:create, on the audience. - Errors:
INVALID_INPUTwhenaudienceis not a URL-safe identifier,ttlSecondsis outside 10 to 3600, orclaimsexceeds 4 KiB of JSON or reuses a standard claim name;ACCESS_DENIEDwhen 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}` } });Prop
Type
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>"
}'iam.api.assertions.issue(
credential: CredentialInput,
input: {
tenantId: string;
audience: string;
ttlSeconds?: number;
claims?: Record<string, Json>;
},
): Promise<{ token: string; expiresAt: number; claims: AssertionClaims }>Better IAM is created by Sean Filimon
Last updated
analysis
Access analysis scans a tenant's configuration for risky or stale access, such as administrators without MFA or dormant accounts that still hold roles.
audit
The audit log records who did what in a tenant; this group searches it, verifies its tamper-evident hash chain, and exports it for archiving.