Sign-in methods
Passwords and password screening, self-registration, magic links, email and SMS codes, federated sign-in, API keys, and assumed roles.
Better IAM supports several ways to sign in, because no single method suits everyone: passwords are familiar, emailed links avoid passwords altogether, passkeys resist phishing, enterprises want their own identity provider, and machines need keys. You choose which methods your deployment offers, and each organization (a ) can narrow that set with its tenant policy.
Whatever the method, the result is the same: a , or an MFA challenge when a second factor is required (see how a sign-in works).
Choose the methods you offer
Methods are switched on under authentication in the server options. Features that send messages need a delivery
callback: sendEmail hands a queued email to your mail provider, and sendSms does the same for text messages.
Construction fails with INVALID_CONFIG when an enabled email feature has no sendEmail, or SMS codes have no
sendSms, so a misconfiguration is caught at startup rather than when someone tries to sign in.
import { betterIam } from 'better-iam';
import { pwnedPasswords } from 'better-iam/auth';
export const iam = betterIam({
// database, secret, baseURL ...
authentication: {
sendEmail: async (message) => mailer.send(message), // deduplicate retries by message.id
sendSms: async (message) => sms.send(message),
emailPassword: true, // the default; false turns password sign-in off
passwordlessEmail: true, // magic links and emailed codes
passwordlessSms: true, // SMS codes
signUpEnabled: false, // the default
passkeys: { rpID: 'example.com', rpName: 'Acme Cloud' },
passwordPolicy: { isBreached: pwnedPasswords() },
},
});Passwords
Passwords remain the default for most products, so Better IAM makes them as safe as it can: strong hashing, screening against weak and breached passwords, and responses that never reveal whether an account exists.
Call auth.signIn({ tenantId, email, password, deviceToken? }) from your login form. It checks the password and
returns a session or an MFA challenge. deviceToken comes from
"remember this device"; through the HTTP handler it is sent
automatically from a cookie.
const result = await client.auth.signIn({ tenantId, email: 'alice@example.com', password });- Passwords are hashed with Argon2id and must be 12 to 1,024 characters long.
- A wrong password, an unknown address, and a disabled account all fail with the same
INVALID_CREDENTIALS. Unknown and passwordless-only accounts perform the same password-hash work, so response times do not reveal which accounts exist. - A wrong password for a real, active account is recorded as a failed attempt (
auth:signin:fail) and counted in the person's sign-in record. emailPassword: falsedisables password sign-in, reset, and self-registration (FEATURE_DISABLED).
Password screening
Length alone does not make a password safe: password1234 is twelve characters, and millions of longer passwords
circulate in breach dumps that attackers try first. Screening refuses those at the moment a password is chosen.
Every password rule applies wherever a password is set: identity creation, invitations, bulk onboarding, reset,
and change. The deployment sets a baseline with authentication.passwordPolicy:
Prop
Type
pwnedPasswords() from better-iam/auth is a Have I Been Pwned client that uses k-anonymity: it sends only the
first five characters of the password's SHA-1 hash and matches the returned suffixes locally. It times out after
three seconds and fails open unless you ask it to fail closed:
passwordPolicy: {
isBreached: pwnedPasswords({
threshold: 1, // minimum breach count that rejects a password
failClosed: true, // refuse with PASSWORD_CHECK_UNAVAILABLE when the service cannot be reached
timeoutMs: 3_000,
}),
check: (password) => (/acme/i.test(password) ? 'Do not use the company name' : undefined),
},Organizations tighten this further with their own rules: minimum length, character classes, no personal
information, password history (PASSWORD_REUSED), and a maximum age (PASSWORD_EXPIRED). See
password rules.
Self-registration
Most business products add people by invitation, so self-registration is disabled by default. Turn it on with
authentication.signUpEnabled: true when anyone should be able to join a tenant on their own, for example a
community or a free tier. auth.signUp then creates the visitor's account in an existing tenant:
const { identity, verificationRequired } = await client.auth.signUp({
tenantId,
email: 'bob@example.com',
name: 'Bob',
password,
});signUp never creates an identity in the root tenant (FORBIDDEN) and does not sign the person in.
requireEmailVerification defaults to the value of signUpEnabled: while it is on, sign-up queues a
verify-email message (valid for 24 hours) and unverified people cannot sign in (EMAIL_UNVERIFIED). See
email verification. Tenant plan limits apply
(LIMIT_EXCEEDED).
Magic links and one-time codes
Many people reuse passwords or forget them. Passwordless sign-in avoids passwords altogether: it proves the person controls an email address or phone number by sending a single-use secret there. It is a two-step flow:
auth.startPasswordlesssends a magic link or a six-digit code to the address the person typed.auth.finishPasswordlesschecks the token from the link, or the code the person typed, and signs them in.
await client.auth.startPasswordless({
tenantId,
destination: 'alice@example.com',
channel: 'email',
kind: 'magic-link',
});
// On the page the link opens (your links.magicLink builder decides its URL):
const result = await client.auth.finishPasswordless({
tenantId,
destination: 'alice@example.com',
token, // from the link
});startPasswordlessalways answers{ success: true }, whether or not the address belongs to anyone, so it cannot be used to enumerate accounts. It sends only when an active person in the tenant has that email, or has verified that phone number.- Links and codes are single-use and expire after five minutes. Codes are six digits. The outbox uses the
magic-linktemplate for links andcodefor codes. - SMS supports codes only (
INVALID_INPUTfor an SMS magic link), and only to a phone number the person has verified from their account first (see phone numbers). - Finishing an email flow marks the address verified. A flow fails with
INVALID_CHALLENGEif the person's email or phone changed after the code was sent. - Passwordless sign-in never links accounts by matching an address; it signs in the identity that owns the address in that tenant.
- Both steps use the sensitive rate-limit tier. The tenant's
allowedMethodsmust includepasswordless-emailorpasswordless-sms. finishPasswordlessaccepts adeviceTokenlikesignIn, and returns an MFA challenge when a second factor is required.
Passkeys
sign people in with a device-bound or synced WebAuthn credential instead of a password. They cannot be phished, and they satisfy MFA on their own. With discoverable passkeys, the browser's autofill picks the account and no email is needed. See passkeys.
Federated sign-in
Enterprise customers want their people to sign in with the company's own identity provider, so that joining, leaving, and MFA are managed in one place. Federated sign-in delegates the proof to that provider through (OAuth/OIDC, including Google and GitHub) or .
The protocol packages handle the ceremony. When it succeeds, they call protocolHost.completeAuthentication, which
finishes like any other sign-in with the method federated: the tenant must allow the method, and root, tenant,
and deployment MFA requirements apply.
- Federation maps
(tenant, provider, issuer, subject)to an identity. A new subject with a verified email provisions an account (VERIFIED_EMAIL_REQUIREDwithout one). - An email that already belongs to an identity fails with
ACCOUNT_LINK_REQUIRED. The person signs in to the existing account and links the provider explicitly; an email collision is never treated as proof of ownership. mapAttributesstores provider attributes on the identity, validated againstpermissions.identityAttributes, and replaces them on every sign-in.
See OAuth sign-in and SAML.
Service accounts and API keys
Scripts, integrations, and other servers cannot type a password or answer an MFA prompt. They sign in with API keys
issued to . A key is an opaque bearer token sent as
Authorization: Bearer <key>; only its hash is stored, so a database leak does not reveal usable keys.
credentials.create issues a key for a service account and returns the token once. Store it in your secret
manager right away; it cannot be shown again.
const { token, credentialId, expiresAt } = await iam.api.credentials.create(adminCredential, {
tenantId,
identityId: serviceAccountId,
name: 'nightly-export',
scopes: ['reports:export'], // or a session `policy`; not both
expiresInSeconds: 30 * 86_400, // 30 days; 90 days when omitted
});- Keys expire (90 days by default).
credentials.rotatereplaces a key with a new one and invalidates the old key in the same transaction, for scheduled rotation or after a leak.credentials.revokeends a key at once, andcredentials.updatechanges its label, description, or expiry. lastUsedAtis updated at most once a minute.credentials.list({ unusedForMs })finds keys nobody has used for that long, so you can revoke them.- A key keeps the ceiling of the that issued it, and stops working when
that authority is revoked or the service account is disabled or past its
expiresAt. - API keys are judged against the address presenting them: a key used from a blocked network is refused.
See the credentials reference.
Assumed roles
Sometimes an identity in one tenant must act inside another: your support team working in a customer's organization, or a central automation managing many tenants. Rather than creating accounts everywhere, gives a short-lived session with one specific role in the target tenant.
It rests on a trust: an exact source-identity to target-role relationship that a root administrator creates with
trust.create. The source identity then calls roles.assume to receive a role session:
const { token, session } = await iam.api.roles.assume(credential, {
tenantId: targetTenantId,
trustId,
durationSeconds: 900, // 15 minutes, the default; at least 60
});roles.assume returns the role session's bearer token, which the caller sends as Authorization: Bearer to act
in the target tenant. The HTTP handler never sets it as a cookie, so the caller's own session stays intact.
- Assumption also requires the source identity's
iam:roles:assumepermission oniam/{roleId}. A trust may require MFA (the default) and an external ID. - Role sessions last 15 minutes by default and at most an hour, unless the trust's
maxSessionSecondsand the deployment'ssts.maxRoleSessionSecondsallow longer. They never outlive the source credential. - Role sessions cannot chain (
ROLE_CHAINING_DISABLED), cannot be started from an impersonation session, and are never recently authenticated, so they cannot perform sensitive operations. - Revoking the trust, the source session, the source identity, or a relevant grant authority ends the role session at its next use.
Restricting methods per tenant
Not every organization wants every method you offer. A company that runs its own identity provider may insist on
federated sign-in only. An organization's allowedMethods lists the sign-in methods it accepts: password,
passwordless-email, passwordless-sms, passkey, and federated. Unset means every method the deployment
enables. A method outside the list is refused with METHOD_NOT_ALLOWED before any credential is examined. See
tenant policy.
Failed attempts and lockouts
Attackers guess passwords and codes at scale, so sign-in, recovery, and MFA flows share persisted rate limits per
account, and optionally per client IP. A refused attempt fails with RATE_LIMITED and a retryAfterMs that tells
the client when to try again. When a real person locks themselves out, an administrator clears their counters with
identities.unlock; when one address keeps attacking, an incident responder refuses it with
security.blockNetwork. See
rate limits and
network restrictions.
Better IAM is created by Sean Filimon
Last updated
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.
Multi-factor authentication
When a second factor is required, how people complete it with TOTP, recovery codes, emailed codes, or passkeys, and how remembered devices and step-up work.