Sign-in and devices
Recipes for per-organization MFA and sign-in method rules, remembering trusted browsers after MFA, device lists, sign-out everywhere, and incident response.
These recipes cover how people sign in and what happens to their sessions afterwards: tightening sign-in for one
organization, sparing people a code on a browser they use every day, and ending sessions when something goes wrong.
credential is the caller's credential ({ token } or { headers }), and client is a browser client from
createIamClient.
Require MFA or restrict sign-in methods for one organization
The problem: one customer's security team requires MFA, or allows only and their own identity provider, while your other customers do not.
The solution: give that an authentication policy with
tenants.setAuthPolicy. The rules apply to that one organization, without changing the deployment.
await iam.api.tenants.setAuthPolicy(credential, {
tenantId,
authPolicy: {
requireMfa: true,
allowedMethods: ['passkey', 'federated'],
sessionIdleTimeoutMs: 30 * 60_000,
},
});A tenant policy can only tighten the deployment's configuration. Existing are re-checked on their next request, so requiring MFA locks out sessions without MFA immediately.
- Setting the policy needs
iam:tenants:updateand recent authentication.authPolicy: nullremoves it. allowedMethodsacceptspassword,passwordless-email,passwordless-sms,passkey, andfederated. Method restrictions are checked before any credential is examined, so a rejected method never reveals whether a password was right.requireMfaForOwnersrequires MFA from owners only. It protects the people who can change the policy first, before you require it from everyone.- The same policy holds
sessionLifetimeMs,maxSessions,allowedIpRanges,trustedDeviceDays,notifyNewSignIn,mfaEmailCodes,bindSessionsToIp, password rules, andallowImpersonation.
To apply a policy to every new organization, set tenantDefaults.authPolicy in the
configuration. See
Tenant policy.
Remember this device after MFA
The problem: asking for an MFA code on every sign-in from the same laptop trains people to resent MFA, but skipping MFA defeats its purpose.
The solution: let people remember a browser when they complete MFA, with rememberDevice: true. That browser
skips the second factor for a bounded time, while a stolen password alone is still not enough anywhere else.
const outcome = await client.auth.signIn({ tenantId, email, password });
if ('mfaRequired' in outcome) {
// The server sets the device cookie; API clients receive deviceToken in the body instead.
await client.auth.verifyMfa({
tenantId,
challenge: outcome.challenge,
code,
rememberDevice: true,
});
}
// Later sign-ins from this browser skip the code until the device expires or is forgotten.
const devices = await client.auth.listTrustedDevices(); // for an "Your devices" page
await client.auth.revokeTrustedDevice({ deviceId }); // forget one device
await client.auth.revokeTrustedDevices(); // forget all of them- Tenants set how long a device is remembered with
authPolicy.trustedDeviceDays(0 disables it). The deployment caps it withauthentication.trustedDeviceLifetimeMs(30 days by default, one year at most). - A password, email, or factor change forgets every device.
- Through the HTTP handler, the device token lives in its own HttpOnly
better-iam.devicecookie and is sent with later sign-ins automatically, so the client only passesrememberDevice: trueonce. - Root administrators' browsers are never remembered. When an administrator revokes a person's sessions, that person's remembered devices are forgotten too.
Devices, sign-out everywhere, and incident response
The problem: people want to see where they are signed in and end the session on a lost phone. During an incident, administrators need to cut off one account or a whole organization, and to unlock someone who tripped the rate limit.
The solution: each case has one call. People manage their own sessions through auth; administrators use
identities and tenants.
const sessions = await iam.api.auth.listSessions(credential); // each with client.userAgent / ip / label
await iam.api.auth.revokeOtherSessions(credential); // the caller keeps this session
await iam.api.identities.revokeSessions(adminCredential, { tenantId, identityId }); // one member
await iam.api.tenants.revokeSessions(adminCredential, { tenantId }); // everyone else in the tenant
await iam.api.identities.unlock(adminCredential, { tenantId, identityId }); // clear rate-limit lockoutsauth.listSessionsreturns the caller's sessions with their client details, for a device list.auth.revokeOtherSessionsends all the others and requires recent authentication.identities.revokeSessions(iam:identities:update) ends one member's sessions without disabling the account, and forgets their remembered devices.tenants.revokeSessions(iam:tenants:update) ends every session in the tenant, keeping the caller's unlessincludeSelfis set. Both require recent authentication and are audited.identities.unlockclears the rate-limit counters behind a person's sign-in, recovery, and MFA flows (audited asidentity:unlock). It never clears per-IP counters. Custom limiters support it by implementingreset.- Device lists show IP addresses only when the server knows them. Behind a proxy, set the
http.clientInfooption ofbetterIam(): a function that returns the request's realip,userAgent, and alabel. - For attacks from a known network, block it with
security.blockNetwork; see the security model.
Next steps
Better IAM is created by Sean Filimon
Last updated
Sharing and access questions
Recipes for sharing with relationships, answering who can do what, testing and rolling back policies, time-bound roles, access paths, and guardrails.
Support and privacy
Recipes for letting support staff view the product as a member, exporting everything stored about a person, and rendering the outbox's emails.