---
title: "Sign-in, MFA and passkeys"
---

# Sign-in, MFA and passkeys

Password sign-in and password policy, TOTP second factors and recovery codes, passkeys, sessions and token lifetimes, and where Govern asks for a fresh second factor.

This page covers the sign-in methods Govern runs itself. Sign-in through your own identity provider is on [Single sign-on with SAML](https://neuralinverse.com/docs/govern/sso-saml/) and [Single sign-on with OIDC](https://neuralinverse.com/docs/govern/sso-oidc/).

## Sign-in methods

| Method | Status | Notes |
|---|---|---|
| Email and password | Working | Hosted sign-in at `/oauth2/authorize`; API sign-in at `POST /api/v1/auth/login`. Passwords are hashed with bcrypt. |
| TOTP second factor | Working | Authenticator apps, with one-time recovery codes. |
| Passkeys (WebAuthn) | Working | Registration, passwordless sign-in and use as a second factor. |
| Single sign-on | Working | SAML 2.0 and OpenID Connect connections in Identity Center. |
| Social sign-in | Working | Google, GitHub and Microsoft, per organisation. |
| Device authorisation grant (RFC 8628) | Working | For the Neural Inverse apps and CLI on machines without a browser: the app shows a code and the person approves it on the console's device page. |
| Magic link | Partial | The emailed link does not issue a session. Do not rely on it. |

Failed sign-ins answer the same way whether or not the account exists. Brute-force protection runs inside password sign-in, passkey sign-in, the hosted sign-in page, the email routes and root step-up.

## Password policy

Each organisation has a password policy, read with `GET /api/v1/organizations/{id}/password-policy` (`organizations:read`) and changed with `PUT` on the same path (`organizations:update`).

| Field | Meaning |
|---|---|
| `min_length`, `max_length` | Length bounds |
| `require_uppercase`, `require_lowercase`, `require_digit`, `require_special` | Character classes |
| `history_count` | How many previous passwords may not be reused |
| `max_age_days` | Maximum password age |
| `lockout_threshold` | Failed attempts before lockout |
| `lockout_duration_minutes` | Lockout length |

`POST /api/v1/auth/password/validate` checks a candidate password against the policy without changing anything. People change their own password with `POST /api/v1/auth/password/change` (the current password is required).

## TOTP and recovery codes

A person enrols an authenticator app from their own security page in the console, or through the API:

| Route | Purpose |
|---|---|
| `POST /api/v1/users/{id}/totp/enroll` | Replace any pending factor and return a one-time secret and an `otpauth://` URI |
| `POST /api/v1/users/{id}/totp/verify` | Verify a six-digit code and turn the factor on |
| `GET /api/v1/users/{id}/totp` | Enrolment status |
| `DELETE /api/v1/users/{id}/totp` | Turn the factor off and remove it |
| `POST /api/v1/users/{id}/recovery-codes` | Generate recovery codes |
| `GET /api/v1/users/{id}/recovery-codes/count` | Remaining recovery codes |

TOTP secrets are encrypted with AES-256-GCM under the deployment's `NI_AUTH_TOTP_ENCRYPTION_KEY`. Codes accept one 30-second step of clock skew, and a code that has been used once cannot be replayed.

## Passkeys

| Route | Purpose |
|---|---|
| `POST /api/v1/passkeys/register/begin`, `/complete` | Register a passkey for the signed-in person; the attestation is verified |
| `GET /api/v1/users/{id}/passkeys` | List a person's passkeys |
| `PATCH /api/v1/users/{id}/passkeys/{credentialId}` | Rename one |
| `DELETE /api/v1/users/{id}/passkeys/{credentialId}` | Delete one |
| `POST /api/v1/auth/passkeys/authenticate/begin`, `/complete` | Passwordless sign-in, email first |

A person manages their own passkeys without any extra permission. An administrator needs `users:read` to list another person's passkeys, `users:update` to rename one and `users:delete` to delete one, and the person must belong to the administrator's organisation (and to no other organisation for a rename or delete). Anyone else gets `404`. Administrator actions and refusals are audited (`auth.passkey.admin_listed`, `auth.passkey.renamed`, `auth.passkey.deleted`, `auth.passkey.access_denied`).

## Sessions and tokens

The operator sets session limits with environment variables:

| Variable | Default | Meaning |
|---|---|---|
| `NI_AUTH_ACCESS_TOKEN_TTL` | `15m` | Access token lifetime |
| `NI_AUTH_REFRESH_TOKEN_TTL` | `720h` | Refresh token lifetime (30 days) |
| `NI_AUTH_SESSION_IDLE_TIMEOUT` | `168h` | A session nobody used for this long ends |
| `NI_AUTH_CONSOLE_SESSION_IDLE_TIMEOUT` | `1h` | Idle timeout of console sessions |
| `NI_AUTH_CONSOLE_SESSION_MAX_LIFETIME` | `12h` | Absolute lifetime of console sessions, at most `12h` |

Console limits apply to the OAuth client named by `NI_AUTH_CONSOLE_CLIENT_ID`. Identity Center connections can set their own, shorter limits; see [Single sign-on with SAML](https://neuralinverse.com/docs/govern/sso-saml/#session-policy).

| Route | Purpose |
|---|---|
| `POST /api/v1/auth/logout` | End the current session |
| `POST /api/v1/auth/token/refresh` | Refresh an access token |
| `POST /api/v1/auth/token/revoke` | Revoke a token (RFC 7009) |
| `POST /api/v1/auth/token/introspect` | Introspect a token (RFC 7662); requires client authentication |
| `POST /api/v1/auth/sessions/revoke` | End one session |
| `POST /api/v1/auth/sessions/revoke-all` | End every session of the caller |

Signing out of a session issued through a connection whose identity provider has a logout endpoint answers with an `end_session_url`, and the client sends the browser there.

## Where Govern asks for a fresh second factor

The general step-up authentication package in the source is not mounted, so there is no single step-up setting. These flows ask for a fresh factor themselves:

| Flow | What is required |
|---|---|
| Activating a just-in-time or break-glass role | A fresh TOTP code in the activation request. See [Just-in-time access](https://neuralinverse.com/docs/govern/jit-access/). |
| Transferring a verified domain, designating emergency administrators | A session whose MFA is at most 15 minutes old, or an `MfaCode` in the request (`403 STEP_UP_REQUIRED` otherwise). |
| Privileged actions in an SSO session | Re-authentication at the identity provider when the last authentication is older than the connection's re-authentication window (`reauthentication_required`). |
| Owner or Admin actions in an SSO session, when the connection requires identity provider MFA | An authentication that carried the configured `amr` or `acr` values (`idp_mfa_required`). |
| Root user tasks | `POST /api/v1/auth/root/step-up` re-authenticates the caller's own login. |
| Break-glass sign-in while SSO is required | A TOTP code or passkey after the password, always. |

Conditions on role bindings can read `request.mfa_age_seconds`, but HTTP requests do not report it yet, so a condition that reads it evaluates to an error: a deny that requires recent MFA applies, and an allow that requires it does not grant.

## Risk-based sign-in

Risk policies are managed through `POST/PUT/DELETE /api/v1/risk/policies*` with `security:manage` and read with `security:read`; the console shows them read-only on the Security Hub risk policies page. A person may read their own risk history; reading someone else's needs `security:investigate`.
