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 authenticator is enrolled yet
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.
In this section
Sign-in methods
Passwords and password screening, magic links, email and SMS codes, federation, API keys, and assumed roles.
Multi-factor authentication
TOTP, recovery codes, emailed codes, remembered devices, and step-up.
Passkeys
Passkeys for sign-in and as a second factor, discoverable sign-in, and naming.
Sessions
Lifetimes, idle timeouts, device metadata, sign-out everywhere, and sign-in records.
Verification and recovery
Email verification, password reset and change, email changes, and lockouts.
Tenant policy
Per-organization MFA, methods, password rules, session limits, and network restrictions.
Impersonation
Audited "view as" sessions for support.
HTTP and configuration
Cookies, CSRF and Origin checks, rate limits, request IDs, deployment options, and email templates.
Was this page helpful?
Last updated on
Data and consistency
How Better IAM stores its records, why every write is serialized, how side effects leave through the outbox, and how plugins and protocols compose.
Sign-in methods
Passwords and password screening, self-registration, magic links, email and SMS codes, federated sign-in, API keys, and assumed roles.