Authentication
How people and services prove who they are in Better IAM, how a sign-in flows from first factor to session, and where each method is configured.
Before your application can decide what someone may do, it has to know who they are. Authentication covers how people and services prove their identity, what the resulting session carries, and which knobs the deployment and each organization control.
Every sign-in happens in a tenant, an organization's isolated account: the same email in two tenants is two separate identities with separate credentials, factors, and sessions. The security model explains the guarantees; these pages explain the flows.
Methods at a glance
Different people and situations call for different proofs: a password for most people, a passkey for phishing-resistant sign-in, a company identity provider for enterprise customers, and API keys for machines. You enable the methods your product needs, and each organization can narrow the list.
| Method | Calls | Session method | Notes |
|---|---|---|---|
| Password | auth.signIn({ tenantId, email, password }) | password | Argon2id; tenant password rules apply on creation, reset, and change. |
| Magic link or emailed code | auth.startPasswordless, then auth.finishPasswordless | passwordless-email | Needs passwordlessEmail and sendEmail. Never links accounts by matching an address. |
| SMS code | auth.startPasswordless, then auth.finishPasswordless | passwordless-sms | Needs passwordlessSms and sendSms, and a verified phone number. |
| Passkey | auth.beginPasskeyAuthentication, then auth.finishPasskeyAuthentication | passkey | Discoverable passkeys sign in without an email (browser autofill). Satisfies MFA. |
| Federated (OAuth/OIDC, SAML) | The protocol packages complete the ceremony and call protocolHost.completeAuthentication | federated | mapAttributes stores provider attributes on the identity. |
| API key (service accounts) | Authorization: Bearer <key> | none | Issued with credentials.create; expiring, rotatable, labeled, with lastUsedAt. |
| Assumed role | roles.assume({ tenantId, trustId }) | none | Platform-controlled trust; sessions are short and cannot chain. |
| Impersonation ("view as") | identities.impersonate | impersonation | An administrator's session as a member; opt-in per tenant, restricted, and fully attributed. |
The session's method reaches policies as principal.authMethod, so a policy can, for example, require a passkey
for a sensitive action. Sign-in methods covers each method in
detail.
How a sign-in works
Whatever the method, a sign-in follows the same shape, so your login UI only has to handle two outcomes. The first factor (a password, a code, a passkey, or a federated assertion) ends either with a session, or with a challenge that asks for a second factor.
Every sign-in returns one of two shapes:
type SignInResult =
| { token: string; session: SafeSession }
| {
mfaRequired: true;
challenge: string; // single-use, valid for five minutes
enrollmentRequired: boolean; // true when no second factor (authenticator or passkey) is set up yet
authenticatorEnrolled?: boolean; // an authenticator or recovery code can satisfy the challenge
emailCodeAvailable?: boolean; // requestMfaCode may email a one-time code
passkeyAvailable?: boolean; // a registered passkey may satisfy the challenge
};A typical login page first finds the tenant with tenants.lookup (from an organization alias), then calls
auth.signIn, which checks the password and returns one of those two shapes. Through the HTTP handler, a returned
session is also set as a cookie, so the page only needs to navigate on.
import { createIamClient, IamClientError } from 'better-iam/client';
import type { iam } from './iam';
const client = createIamClient<typeof iam>();
const { tenantId } = await client.tenants.lookup({ slug: 'acme' });
try {
const result = await client.auth.signIn({ tenantId, email, password });
if ('mfaRequired' in result) {
// Show the second-factor step; see the MFA guide.
return goToSecondFactor(tenantId, result);
}
// Signed in: the handler set the session cookie.
} catch (error) {
if (error instanceof IamClientError && error.code === 'RATE_LIMITED') {
showRetryIn(error.retryAfterMs);
} else throw error;
}What every sign-in checks
In order, before any session is issued:
- Network blocks refuse a blocked client address with
IP_BLOCKED, before rate limits or credentials are examined. - Rate limits count the attempt against the client IP (when
ipAttemptsis set) and the account, and refuse withRATE_LIMITED. - The tenant and every ancestor must be active (
TENANT_UNAVAILABLE). - The method must be in the tenant's
allowedMethods(METHOD_NOT_ALLOWED). This happens before any credential is examined, so a rejected method never reveals whether a password was right. - The credential is verified. Unknown addresses perform the same password-hash work as real ones and return
the same
INVALID_CREDENTIALS, so responses do not reveal which accounts exist. - Email verification is required when
requireEmailVerificationis on (EMAIL_UNVERIFIED), and an expired password is refused withPASSWORD_EXPIREDonly after it was verified. - MFA, when required, returns a challenge instead of a session.
- Session issuance re-checks the tenant's IP allowlist (
IP_NOT_ALLOWED) and blocks, applies the tenant's session lifetime, ends the oldest session beyondmaxSessions, records the client, and may queue a new-sign-in notice.
After issuance, the session is re-validated on every use. See sessions.
When MFA is required
A password alone falls to phishing, reuse, and guessing. Multi-factor authentication (MFA) adds a second proof, such as an authenticator code or a passkey. MFA is required:
- for root administrators, always;
- for anyone with an authenticator enrolled;
- when the tenant policy sets
requireMfa(orrequireMfaForOwners, for owners); - when the deployment's
authentication.requireMfa(tenant, identity)callback returns true.
The requirement also applies to federated sign-in and to sessions that already exist: requiring MFA in a tenant locks out sessions that did not complete it on their next use. See multi-factor authentication.
Recent authentication
A session can live for days, and a laptop left unlocked for a minute should not let someone change the password or
remove the second factor. So sensitive operations (password, email, factor, device, session, and ownership changes,
impersonation, and policy changes) require recent authentication: the session must have been established within
recentAuthenticationMs (five minutes by default). Otherwise they fail with RECENT_AUTH_REQUIRED, and the person
confirms their password with auth.reauthenticate({ password }), which issues a fresh session.
See sessions.
Which page do I need?
| You want to | Read |
|---|---|
| Compare every way to sign in and choose the ones to offer | Sign-in methods |
| Let people sign up and sign in with a password | Email and password |
| Send magic links or one-time codes by email or SMS | Magic links and codes |
| Offer phishing-resistant sign-in with WebAuthn | Passkeys |
| Require a second factor, or remember trusted browsers | Multi-factor authentication |
| Control session lifetimes, sign-out everywhere, and recent authentication | Sessions |
| Issue short-lived tokens or signed session JWTs to other services | Tokens and JWTs |
| Authenticate servers, jobs, and integrations | API keys and service accounts |
| Verify email addresses, reset passwords, and clear lockouts | Verification and recovery |
| Let each organization set its own MFA, method, password, and network rules | Tenant policy |
| Let support staff see what a member sees | Impersonation |
| Understand cookies, CSRF checks, rate limits, and every option | HTTP and configuration |
In this section
Every method at a glance, how to enable each, and how to restrict them per tenant.
Email and passwordSign-up, sign-in, password screening, and Argon2id hashing.
Magic links and codesPasswordless sign-in by email link, emailed code, or SMS code.
PasskeysPasskeys for sign-in and as a second factor, discoverable sign-in, and naming.
Multi-factor authenticationTOTP, recovery codes, emailed codes, remembered devices, and step-up.
SessionsLifetimes, idle timeouts, device metadata, sign-out everywhere, and sign-in records.
Tokens and JWTsToken prefixes, short-lived session tokens, signed session JWTs, and web identity.
API keys and service accountsIssue, scope, rotate, and revoke keys for machines.
Verification and recoveryEmail verification, password reset and change, email changes, and lockouts.
Tenant policyPer-organization MFA, methods, password rules, session limits, and network restrictions.
ImpersonationAudited "view as" sessions for support.
HTTP and configurationCookies, CSRF and Origin checks, rate limits, request IDs, deployment options, and email templates.
Better IAM is created by Sean Filimon
Last updated
Error handling
How Better IAM reports failures with IamError and its stable codes, how codes map to HTTP statuses, how to handle them on the server and in the client, and which ones are safe to retry.
Sign-in methods
An overview of every way to sign in to Better IAM (passwords, magic links and codes, passkeys, federation, API keys, temporary tokens, and assumed roles) and how to enable and restrict them.