# assertions (/docs/reference/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 [#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`:

```ts
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()`](/docs/reference/api#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 [#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`](#issue) | Issues a signed assertion about the caller for one audience, valid for five minutes by default. | Credential |

## issue [#issue]

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

**HTTP:** `POST /api/iam/assertions/issue` (requires a credential) · **Browser client:** `client.assertions.issue()`

* **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.

```ts
// 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}` } });
```

```ts title="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 }>
```
