Tokens and JWTs
The bearer tokens Better IAM issues, their prefixes and lifetimes, short-lived session tokens from sts.getSessionToken, signed session JWTs with a JWKS endpoint, and web-identity federation for workloads.
Every credential Better IAM issues is a bearer token backed by a stored session row. A person's sign-in returns
one, an API key is one, and the security token service (sts) issues short-lived ones derived from a source
credential. The stored row is always authoritative: a token's prefix only routes and triages, and every use
re-validates the row, its identity, and its source.
| Credential | Session kind | Token prefix | Issued by | Lifetime | Cookie |
|---|---|---|---|---|---|
| User session | user | biam_ses_ | auth.signIn and every other sign-in flow | 7 days, 1 day idle (configurable) | Yes, better-iam.session |
| Impersonation session | user | biam_ses_ | identities.impersonate | At most 8 hours, never beyond the administrator's session | No |
| API key | api-key | biam_key_ | credentials.create, credentials.rotate | 90 days by default, at most a year | No |
| Session token | session-token | biam_sts_ | sts.getSessionToken | 3600 seconds by default, never beyond its source | No |
| Role session | role | biam_rol_ | roles.assume, sts.assumeRoleWithWebIdentity | 900 seconds by default, never beyond its source | No |
| Delegated agent session | delegated | biam_dlg_ | delegations.assume (AI agents) | 900 seconds by default (less when the agent or delegation sets a lower cap), never beyond the delegation or the agent's key | No |
Session tokens and role sessions are the temporary kinds. They can also be issued as session JWTs: signed tokens that other services verify offline against a JWKS endpoint, while Better IAM still checks the stored row on every use. Use this page when a script, a CLI, a CI job, or another service needs a credential that is shorter-lived or narrower than a person's session or an API key.
Setup
Opaque tokens need no setup: sign-in issues session tokens, credentials.create issues API keys, and
sts.getSessionToken works on every deployment. Session JWTs and web-identity federation are opt-in.
Generate a signing key
Session JWTs are signed with their own keys, separate from secret, so rotating the deployment secret never
affects them. Generate an Ed25519 private JWK and store it in your secret manager like secret.
import { generateKeyPairSync } from 'node:crypto';
const { privateKey } = generateKeyPairSync('ed25519');
const jwk = { ...privateKey.export({ format: 'jwk' }), kid: '2026-11', alg: 'EdDSA', use: 'sig' };
console.log(JSON.stringify(jwk)); // store as SESSION_JWT_KEYKeys are Ed25519 (kty: 'OKP', crv: 'Ed25519', alg: 'EdDSA') or P-256 (kty: 'EC', crv: 'P-256',
alg: 'ES256'), each with a unique kid of up to 64 letters, digits, dots, underscores, or hyphens.
Configure the server
sts.jwt turns on format: 'jwt' and the JWKS route. sts.webIdentity.enabled turns on the OIDC token exchange
for workloads. Invalid values throw INVALID_CONFIG naming the field.
import { betterIam } from 'better-iam';
import { sqliteAdapter } from 'better-iam/adapter-sqlite';
export const iam = betterIam({
database: sqliteAdapter({ filename: './iam.db' }),
secret: process.env.BETTER_IAM_SECRET!,
baseURL: 'https://iam.example.com',
sts: {
maxSessionTokenSeconds: 43_200, // the default ceiling for session tokens
jwt: {
signingKeys: [JSON.parse(process.env.SESSION_JWT_KEY!)],
audiences: ['https://billing.example.com'], // services that verify these tokens
maxLifetimeSeconds: 900, // short tokens limit how long a revoked one still verifies offline
},
webIdentity: {
enabled: true,
allowedIssuers: ['https://token.actions.githubusercontent.com'],
},
},
});Migrate the database
Sessions, OIDC providers, trusts, and web-identity replay records live in collections that migrate creates with
the rest of the schema.
npx better-iam migrate --config better-iam.config.mjsInstall the verifier in other services
Services that verify session JWTs offline need only the verifier subpath, which imports nothing but jose and runs
in Node, Bun, Deno, workers, and edge runtimes.
npm i better-iamImport it as better-iam/session-tokens, or @better-iam/server/session-tokens when you install the scoped
package.
Usage
Server code passes a credential as { token } or { headers: request.headers }. Over HTTP every token travels as
Authorization: Bearer <token> (the scheme is case-insensitive). Issuers return temporary tokens once, in the
response body, and never set a cookie. Routes are relative to the handler's basePath (/api/iam by default).
Use a sign-in token as a bearer
auth.signIn and the other sign-in flows return { token, session }. Through the HTTP handler the token is also
set as the better-iam.session cookie (HttpOnly, __Host- prefixed over HTTPS). Mobile apps, CLIs, and servers
send it explicitly instead:
import { createIamClient } from 'better-iam/client';
import type { iam } from './iam';
const client = createIamClient<typeof iam>({
baseURL: 'https://iam.example.com',
token: () => process.env.BETTER_IAM_TOKEN, // a biam_ses_ token from signIn
});
const { identity, session, limits } = await client.auth.getSession();A bearer credential takes precedence over the cookie and never touches cookies. The client's token option accepts
the two bearer shapes the server can take: an opaque token (16 to 512 characters of letters, digits, _, and -)
or a session JWT (three base64url segments, at most 4096 characters). Any other value is never sent.
Session tokens
sts.getSessionToken trades a signed-in session or an API key for a short-lived token for your own identity,
optionally narrowed by a policy. Hand it to a script or a CI step instead of the long-lived credential: if it leaks,
it expires by itself.
import { iam } from './iam';
const { token, expiresAt, session } = await iam.api.sts.getSessionToken(
{ token: process.env.IAM_API_KEY },
{
durationSeconds: 900,
sessionName: 'deploy-118',
policy: {
version: 1,
statements: [{ effect: 'allow', actions: ['deployments:create'], resources: ['*'] }],
},
},
);Route: POST /sts/getSessionToken
- Requires
iam:session-tokens:createon your own identity (iam/{identityId}). Owners hold it through the Owner role; anyone else needs an explicit grant, and a deny stops, for example, CI keys from minting tokens. - The token acts with the identity's grants, bounded by
policy, by the source's own policy (an API key's scopes), and by the source's grant authority. durationSecondsdefaults to 3600 and may not exceedsts.maxSessionTokenSeconds(43200 by default) or, for a JWT,sts.jwt.maxLifetimeSeconds. A value outside the range isINVALID_INPUT, never silently shortened. The token never outlives its source and ends when the source ends.sessionNameis 2 to 64 letters, digits, or+=,.@_-, and reaches policies asprincipal.sessionName.- Tokens cannot chain: a role session or another session token cannot mint one (
CREDENTIAL_CHAINING_DISABLED), and a "view as" session cannot either (IMPERSONATION_RESTRICTED). - Each identity holds at most
sts.maxSessionTokensPerIdentitylive tokens (50 by default,LIMIT_EXCEEDED). - Using a token never refreshes its source's idle timer, so long-running automation should mint tokens from an API key rather than from a person's session.
- Audited as
iam:session-tokens:createandsession-token:issued.
auth.listSessions shows a person's own session tokens, and auth.revokeSession ends one.
Step up with MFA, then assume a role
mfaCode makes the token carry mfa: true and a fresh principal.mfaTime. Roles never take a code themselves, so
this is how a command-line tool assumes a role whose trust requires MFA: verify once, then assume with the token.
import { iam } from './iam';
const step = await iam.api.sts.getSessionToken(
{ token: userSessionToken },
{ mfaCode: '123456', sessionName: 'ops-cli' },
);
const role = await iam.api.roles.assume(
{ token: step.token },
{ tenantId: customerTenantId, trustId, sessionName: 'ticket-4711' },
);
// role.token is a biam_rol_ token for the customer tenant until role.expiresAt.Route: POST /sts/getSessionToken, then POST /roles/assume
mfaCodeis accepted only from a person's session whose identity has an authenticator. It is counted against the sensitive rate limit first, then checked with the same single-use rules as sign-in (INVALID_MFA). Emailed and recovery codes never count. From an API key, or without an authenticator, it fails withMFA_NOT_ENROLLED.- The step-up is audited as
auth:mfa:step-up. - Role sessions, trusts, and their options are covered in role sessions.
Session JWTs
With sts.jwt configured, pass format: 'jwt' to sts.getSessionToken, roles.assume, or
sts.assumeRoleWithWebIdentity to receive a signed JWT instead of an opaque token. User sessions and API keys are
never JWTs.
import { iam } from './iam';
const credential = await iam.api.sts.getSessionToken(
{ token: process.env.IAM_API_KEY },
{ format: 'jwt', audience: ['https://billing.example.com'], durationSeconds: 600 },
);
// credential.format === 'jwt'; credential.audience lists the audiences the token names.
await fetch('https://billing.example.com/api/invoices', {
headers: { authorization: `Bearer ${credential.token}` },
});Route: POST /sts/getSessionToken
- Without
sts.jwt,format: 'jwt'fails withFEATURE_DISABLED. audiencetakes 1 to 5 entries fromsts.jwt.audiences; the issuer is always allowed and is the default. Every audience other than the issuer must also be allowed asiam:assertions:createoniam/{audience}for the credential being issued (ACCESS_DENIEDotherwise), so a scoped-down credential cannot name a service its scope forbids.- Inside Better IAM a JWT is accepted only when its signature, type, algorithm, issuer, audience (which must include the IAM issuer), and times verify and the stored row matches it. A token issued only for a downstream audience is refused by Better IAM itself.
Publish the JWKS
With sts.jwt configured, the HTTP handler serves the public signing and verification keys as
application/jwk-set+json with Cache-Control: public, max-age=300. Without it the route answers 404.
import { iam } from './iam';
// The same key set in process; iam.sessionTokens is undefined without sts.jwt.
const jwks = iam.sessionTokens?.jwks(); // { keys: [{ kty, crv, x, y?, kid, alg, use: 'sig' }] }
const issuer = iam.sessionTokens?.issuer; // `${baseURL.origin}${basePath}` unless sts.jwt.issuer overrides itRoute: GET /.well-known/jwks.json
Verify session JWTs in other services
createSessionTokenVerifier from better-iam/session-tokens enforces the same type, algorithm allowlist, issuer,
audience, lifetime, kind, and claim checks as Better IAM. verify(token) and verifyRequest({ headers }) return
the claims or throw SessionTokenError (code: 'INVALID_SESSION_TOKEN', status: 401, and a reason for logs).
import { createSessionTokenVerifier, SessionTokenError } from 'better-iam/session-tokens';
export const verifier = createSessionTokenVerifier({
issuer: 'https://iam.example.com/api/iam',
audience: 'https://billing.example.com', // must be in sts.jwt.audiences and requested at issuance
jwks: 'https://iam.example.com/api/iam/.well-known/jwks.json',
});
export { SessionTokenError };import { createServer } from 'node:http';
import { SessionTokenError, verifier } from './session-tokens';
createServer(async (request, response) => {
try {
const claims = await verifier.verifyRequest({
headers: { authorization: request.headers.authorization ?? '' },
});
response.end(`hello ${claims.sub} in ${claims.tid}`);
} catch (error) {
response.writeHead(error instanceof SessionTokenError ? 401 : 500).end();
}
}).listen(8080);jwksis a key set (iam.sessionTokens.jwks()) or the JWKS URL, which must be https except on localhost. A remote set is cached (cacheMaxAgeSeconds, default 600) and refetched on an unknownkidat most once percooldownSeconds(default 30).reasonis one ofmalformed,too-large,type,algorithm,unknown-key,signature,expired,not-yet-valid,issuer,audience,claims,lifetime,kind,jwks, orrevoked. Show clients only the 401.- Services that run inside the IAM application need no verifier:
iam.authenticate,iam.authorize, and everyiam.apicall accept session JWTs and opaque temporary tokens as bearers.
Check a token online
An offline verifier learns of revocation only at exp. When a service must honour revocation immediately, check
the token with Better IAM, instead of or after the offline check.
const response = await fetch('https://iam.example.com/api/iam/sts/getCallerIdentity', {
method: 'POST',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/json',
'x-better-iam': '1',
},
body: '{}',
});
if (!response.ok) throw new Error('revoked or invalid');
const { data: caller } = await response.json(); // identityId, tenantId, sessionKind, expiresAt, ...Route: POST /sts/getCallerIdentity
sts.getCallerIdentity works for every credential kind, needs no permission, writes no audit event, and returns
identityId, identityTenantId, identityKind, tenantId, sessionId, sessionKind, format, mfa,
authenticatedAt, issuedAt, expiresAt, and, when set, method, roleId, trustId, sourceTenantId,
sessionName, sourceIdentity, sessionTags, audience, webIdentity, impersonatorId, agentId, and
delegationId. The CLI's
whoami prints it for BETTER_IAM_TOKEN.
| Event | Inside Better IAM | Offline verifier |
|---|---|---|
Source sign-out, rotation, revokeSessions, trust.revoke, identity revoked | Refused on the next request | Accepted until exp |
| Watermark moved, provider or service account disabled, authority revoked | Refused on the next request | Accepted until exp |
kid removed from both signingKeys and verificationKeys | Refused on the next request | Refused once its JWKS cache refreshes |
exp passed | Refused | Refused (5 seconds of clock tolerance) |
Web-identity federation
CI pipelines and cloud workloads already receive short-lived OpenID Connect tokens from their platform: GitHub
Actions, GitLab CI, Kubernetes service accounts, and cloud workload identity all issue them.
sts.assumeRoleWithWebIdentity exchanges such a token for a role session acting as a service account, so no IAM
secret is stored in CI. It needs sts.webIdentity.enabled: true.
Register the provider
import { iam } from './iam';
const provider = await iam.api.oidcProviders.create(adminCredential, {
tenantId,
name: 'GitHub Actions',
issuer: 'https://token.actions.githubusercontent.com',
audiences: ['https://iam.example.com'],
});Route: POST /oidcProviders/create
The issuer must be https, must not be Better IAM itself, and must be in sts.webIdentity.allowedIssuers when that
is set. Without jwks or jwksUri, keys come from OIDC discovery.
Create a web-identity trust
The trust names the role, the service account sessions act as, and conditions on the token's claims, written as
token.<claim> keys in the policy condition grammar. The conditions must pin token.sub
(WEAK_TRUST_CONDITIONS otherwise), so a trust can never admit every job on the platform.
import { iam } from './iam';
const trust = await iam.api.trust.create(adminCredential, {
tenantId,
kind: 'web-identity',
providerId: provider.id,
serviceAccountId: deployBot.id,
roleId: deployerRole.id,
conditions: {
StringEquals: {
'token.sub': 'repo:acme/app:ref:refs/heads/main',
'token.repository_owner': 'acme',
},
},
tagClaims: { repository: 'token.repository' },
maxSessionSeconds: 1800,
});Route: POST /trust/create
Exchange the token in the job
The exchange is public: the external token is the only credential. Send JSON with X-Better-IAM: 1 and use the
returned token as Authorization: Bearer.
permissions:
id-token: write # lets the job request an OIDC token
contents: read
steps:
- name: Get an IAM role session
run: |
ID_TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://iam.example.com" | jq -r .value)
IAM_TOKEN=$(curl -sf https://iam.example.com/api/iam/sts/assumeRoleWithWebIdentity \
-H 'Content-Type: application/json' -H 'X-Better-IAM: 1' \
-d "{\"tenantId\":\"$TENANT_ID\",\"trustId\":\"$TRUST_ID\",\"webIdentityToken\":\"$ID_TOKEN\",\"sessionName\":\"gha-$GITHUB_RUN_ID\"}" \
| jq -r .data.token)
echo "::add-mask::$IAM_TOKEN"
echo "BETTER_IAM_TOKEN=$IAM_TOKEN" >> "$GITHUB_ENV"Route: POST /sts/assumeRoleWithWebIdentity
- The input is
{ tenantId, trustId, webIdentityToken, sessionName, durationSeconds?, policy?, format?, audience? }.sessionNameis required; the token is at most 8192 characters. - The session is kind
roleacting as the service account, withsession.webIdentity = { providerId, issuer, subject }andmfa: false. It lasts 900 seconds by default, up to the trust's maximum, and is not bounded by the external token'sexp. - Until a verified token has matched a live trust, every refusal that depends on the token or on stored state
answers the same
WEB_IDENTITY_REJECTED(403), so a caller cannot probe which trusts exist. After a match, specific errors surface (rate limits, network blocks, the per-trust session cap, duration, policy, format, and audience). Administrators see the real reason withtrust.evaluateWebIdentity, which checks a token against a trust without issuing anything. - By default each token is redeemed once per provider (
replayProtection: 'single-use'). Set'off'for Kubernetes projected tokens, which are reused until the kubelet rotates them. - Disabling the provider, revoking the trust, disabling the service account, or revoking either grant authority
ends live sessions at their next use. Audited as
role:assumed-with-web-identity.
Temporary access shows the same flow from the authorization side, and the trust and oidcProviders references list every field.
Revoke temporary credentials
| Call | Ends |
|---|---|
roles.revokeSessions({ tenantId, roleId, before? }) | Every role session of the role, classic and web identity. |
trust.revokeSessions({ tenantId, trustId, before? }) | The trust's sessions. |
oidcProviders.revokeSessions({ tenantId, providerId, before? }) | Web-identity sessions of every trust that uses the provider. |
identities.revokeSessions({ tenantId, identityId, keepApiKeys? }) | Every session of the identity, including its session tokens. |
auth.revokeSession({ sessionId }) | One of the caller's own sessions, including a session token. |
Every revokeSessions call needs recent authentication. The role, trust, and provider calls also move a watermark
that only ever moves forward, delete the matching rows, and return { sessionsRevokedBefore, revoked } along with
the role, trust, or provider id. Session tokens end with their source.
Rotate signing keys
Signing keys are independent of secret, so secret rotation never touches them.
- Add the new private key to
sts.jwt.signingKeysand deploy. It is published in the JWKS but not used yet. - Wait at least the JWKS
max-ageplus your verifiers' cache time: 15 minutes with the defaults (300 seconds plus a verifier's 600-second cache). - Set
activeKeyIdto the newkidand deploy. - Move the old key's public part to
verificationKeys(private members there areINVALID_CONFIG). - Remove it once
maxLifetimeSecondshas passed.
Removing a key early is the emergency revoke for every token it signed, legitimate ones included.
Token formats
Opaque credentials are biam_<type>_<random><check>: type is ses, key, rol, sts, or dlg; random is
43 base64url characters (32 random bytes); check is 6 base64url characters of the CRC-32 of everything before it.
The whole token is 58 characters of [A-Za-z0-9_-], and only its SHA-256 is stored.
- A prefixed value whose shape or checksum is wrong is refused with 401 before any storage read. The checksum filters typos and garbage; it authenticates nothing.
- The stored kind must equal the prefix's kind (
sestouser,keytoapi-key,roltorole,ststosession-token,dlgtodelegated), or the token is refused. - Legacy unprefixed 43-character tokens keep working until they expire.
credentialTokenScanPattern,parseCredentialToken,newCredentialToken, andcredentialTokenKindsfrombetter-iam/authlet secret scanners and tools recognize the format.
import { credentialTokenScanPattern, parseCredentialToken } from 'better-iam/auth';
const found = text.match(new RegExp(credentialTokenScanPattern, 'g')) ?? [];
const typed = found.map((token) => parseCredentialToken(token)?.type); // 'ses' | 'key' | 'rol' | 'sts' | 'dlg'Session JWTs are never prefixed. Their header is { alg: 'EdDSA' | 'ES256', kid, typ: 'biam-session+jwt' }, they
are at most 4096 characters, and they carry these claims:
| Claim | Meaning |
|---|---|
iss | The IAM issuer. |
aud | One audience as a string, several as an array. |
sub | The identity the session acts as. |
tid | The tenant the session belongs to. |
sid, jti | The session id (equal). |
iat, nbf, exp | Issue, not-before (equal to iat), and expiry times, in epoch seconds. |
auth_time | When the credential behind the session was authenticated. |
kind | role or session-token. |
mfa | Whether the session passed MFA. |
role, trust, src_tid | Role sessions: the role, the trust, and the source tenant. |
session_name, source_identity | When set at issuance. |
idp, idp_sub | Web-identity sessions: the OIDC provider id and the verified external subject. |
Tags, policies, grant authority ids, the source session id, and hashes are never included. Read tags online
through sts.getCallerIdentity.
Options
sts in the server options controls temporary credentials. See
temporary credentials for the full reference.
| Option | Type | Default | Description |
|---|---|---|---|
sts.maxRoleSessionSeconds | number | 3600 | Ceiling for role sessions, 900 to 43200. Trusts can only lower it. |
sts.maxSessionTokenSeconds | number | 43200 | Ceiling for session tokens, 900 to 129600. |
sts.maxSessionTokensPerIdentity | number | 50 | Live session tokens per identity, 1 to 1000. |
sts.jwt.signingKeys | JWK[] | none (JWTs off) | 1 to 10 private Ed25519/EdDSA or P-256/ES256 JWKs with unique kids. |
sts.jwt.activeKeyId | string | the first signing key | The kid new tokens are signed with. |
sts.jwt.verificationKeys | JWK[] | none | 0 to 10 public-only retired keys that still verify. |
sts.jwt.issuer | string | ${baseURL.origin}${basePath} | The iss claim. |
sts.jwt.audiences | string[] | the issuer | Audiences tokens may name; the issuer is always included. |
sts.jwt.maxLifetimeSeconds | number | 3600 | Longest JWT, 300 to 43200. |
sts.webIdentity.enabled | boolean | false | Turns web-identity federation on. |
sts.webIdentity.allowedIssuers | string[] | any https issuer | Issuers tenants may register, at most 100. |
sts.webIdentity.maxExchangesPerWindow | number | 600 | Exchanges per trust per rate-limit window. |
sts.webIdentity.maxSessionsPerTrust | number | 1000 | Live sessions per web-identity trust. |
sts.webIdentity.jwksCacheSeconds | number | 600 | Provider key cache, 60 to 3600. |
sts.webIdentity.fetchTimeoutMs | number | 5000 | Discovery and JWKS fetch timeout, 500 to 10000. |
sts.webIdentity.maxJwksBytes | number | 65536 | Largest discovery or JWKS response. |
sts.webIdentity.fetchJson | (url: URL) => Promise<unknown> | built-in guarded fetch | Replaces the fetch; bypasses the SSRF guard. |
createSessionTokenVerifier accepts:
| Option | Type | Default | Description |
|---|---|---|---|
issuer | string | required | The IAM issuer, exactly. |
audience | string | string[] | required | Audiences this service accepts; a token must name one. |
jwks | { keys } | URL | string | required | A key set or the JWKS URL (https except on localhost). |
algorithms | ('EdDSA' | 'ES256')[] | both | Accepted signature algorithms. |
kinds | ('role' | 'session-token')[] | both | Accepted session kinds. |
clockToleranceSeconds | number | 5 | Skew tolerated for exp and nbf, 0 to 60. |
maxLifetimeSeconds | number | 43200 | Longest accepted exp - iat. |
cacheMaxAgeSeconds | number | 600 | Remote JWKS cache lifetime. |
cooldownSeconds | number | 30 | Minimum time between refetches on an unknown kid. |
timeoutMs | number | 5000 | Remote JWKS fetch timeout. |
now | () => number | Date.now | The clock, in epoch milliseconds. |
Schema
| Collection | Key fields | Purpose |
|---|---|---|
sessions | kind, tokenHash, identityId, tenantId, expiresAt, mfa, sourceSessionId, policy, sourcePolicy, sessionName, format, audience, roleId, trustId, webIdentity | Every credential, opaque or JWT. A JWT row stores the hash of the JWT like any token. |
trusts | kind, roleId, sourceIdentityId (for a web-identity trust, the serviceAccountId it was created with), providerId, conditions, tagClaims, maxSessionSeconds, revoked, authorityId, sessionsRevokedBefore | Identity and web-identity trusts. |
oidcProviders | issuer, audiences, jwks or jwksUri, replayProtection, enabled, sessionsRevokedBefore | External token issuers a tenant accepts. |
webIdentityReplays | keyed by the token's jti or content hash, per provider | Single-use redemption records, swept after the token's expiry. |
Errors
| Code | When |
|---|---|
CREDENTIAL_CHAINING_DISABLED | A role session or session token tried to mint a session token. |
ROLE_CHAINING_DISABLED | A role session tried to assume a role. |
IMPERSONATION_RESTRICTED | A "view as" session tried to mint a token or assume a role. |
MFA_NOT_ENROLLED | mfaCode from an API key, or from a person without an authenticator. |
INVALID_MFA | A wrong or replayed mfaCode. |
FEATURE_DISABLED | format: 'jwt' without sts.jwt, or a web-identity exchange with the feature off. |
LIMIT_EXCEEDED | Too many live session tokens for the identity, or live sessions for a web trust. |
INVALID_INPUT | A duration, session name, format, or audience out of bounds. |
ACCESS_DENIED | Missing iam:session-tokens:create, or an audience the credential may not obtain. |
WEB_IDENTITY_REJECTED | Any web-identity refusal, before the token matched a live trust, that depends on the token or on stored state. |
WEAK_TRUST_CONDITIONS | A web-identity trust whose conditions do not pin token.sub. |
RECENT_AUTH_REQUIRED | A temporary credential used for an operation that needs recent authentication. |
UNAUTHENTICATED | An invalid, expired, revoked, or wrongly prefixed token. |
INVALID_CONFIG | At startup: an invalid sts setting or signing key. |
SessionTokenError from better-iam/session-tokens is not an API error: it is thrown by the offline verifier with
code: 'INVALID_SESSION_TOKEN' and status 401.
Next steps
Better IAM is created by Sean Filimon
Last updated
Sessions
Database sessions with absolute and idle lifetimes, device metadata, recent authentication, sign-out everywhere, session caps, and sign-in records.
API keys and service accounts
Service accounts and their API keys in Better IAM, covering issuance with credentials.create, scopes, expiry, rotation, revocation, unused-key hygiene, and how keys are presented.