BetterIAM

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.

@better-iam/auth@better-iam/server@better-iam/clientauthentication.mdsecurity.mdbase.tstypes.ts

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.

MethodCallsSession methodNotes
Passwordauth.signIn({ tenantId, email, password })passwordArgon2id; tenant password rules apply on creation, reset, and change.
Magic link or emailed codeauth.startPasswordless, then auth.finishPasswordlesspasswordless-emailNeeds passwordlessEmail and sendEmail. Never links accounts by matching an address.
SMS codeauth.startPasswordless, then auth.finishPasswordlesspasswordless-smsNeeds passwordlessSms and sendSms, and a verified phone number.
Passkeyauth.beginPasskeyAuthentication, then auth.finishPasskeyAuthenticationpasskeyDiscoverable passkeys sign in without an email (browser autofill). Satisfies MFA.
Federated (OAuth/OIDC, SAML)The protocol packages complete the ceremony and call protocolHost.completeAuthenticationfederatedmapAttributes stores provider attributes on the identity.
API key (service accounts)Authorization: Bearer <key>noneIssued with credentials.create; expiring, rotatable, labeled, with lastUsedAt.
Assumed roleroles.assume({ tenantId, trustId })nonePlatform-controlled trust; sessions are short and cannot chain.
Impersonation ("view as")identities.impersonateimpersonationAn 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:

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

login.ts
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:

  1. Network blocks refuse a blocked client address with IP_BLOCKED, before rate limits or credentials are examined.
  2. Rate limits count the attempt against the client IP (when ipAttempts is set) and the account, and refuse with RATE_LIMITED.
  3. The tenant and every ancestor must be active (TENANT_UNAVAILABLE).
  4. 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.
  5. 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.
  6. Email verification is required when requireEmailVerification is on (EMAIL_UNVERIFIED), and an expired password is refused with PASSWORD_EXPIRED only after it was verified.
  7. MFA, when required, returns a challenge instead of a session.
  8. Session issuance re-checks the tenant's IP allowlist (IP_NOT_ALLOWED) and blocks, applies the tenant's session lifetime, ends the oldest session beyond maxSessions, 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 (or requireMfaForOwners, 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 toRead
Compare every way to sign in and choose the ones to offerSign-in methods
Let people sign up and sign in with a passwordEmail and password
Send magic links or one-time codes by email or SMSMagic links and codes
Offer phishing-resistant sign-in with WebAuthnPasskeys
Require a second factor, or remember trusted browsersMulti-factor authentication
Control session lifetimes, sign-out everywhere, and recent authenticationSessions
Issue short-lived tokens or signed session JWTs to other servicesTokens and JWTs
Authenticate servers, jobs, and integrationsAPI keys and service accounts
Verify email addresses, reset passwords, and clear lockoutsVerification and recovery
Let each organization set its own MFA, method, password, and network rulesTenant policy
Let support staff see what a member seesImpersonation
Understand cookies, CSRF checks, rate limits, and every optionHTTP and configuration

In this section

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page