
# Run a sign-in step

`POST /fapi/v1/transactions/current/steps/{step}` · called from the environment's sign-in host

Runs one step of the current transaction. This version accepts the `identifier` and `password` steps. Every failed
credential step returns the same `401 invalid_credentials`, so the response never reveals whether an account exists. When
the password step completes the transaction, the session cookies `__Host-idp_session` (and, when needed,
`__Host-idp_device`) are set and the transaction cookie is removed.

## Request

Headers:

| Header | Required | Description |
| --- | --- | --- |
| `Idp-Client` | yes | SDK identifier. |
| `Origin` | yes | Must be exactly `https://<sign-in host>`. |
| `Idp-Csrf-Token` | yes | The `csrf_token` from the transaction object. |

Path: `step`, the name of the step (`identifier`, `password`, `sso_start`).

On the `otp_send` step, `next_steps[].methods` lists the passwordless methods the effective policy allows (`email_otp`, `magic_link`); a client offers only these.

Body (fields depend on the step; `next_steps[].fields` lists them):

| Field | Type | Description |
| --- | --- | --- |
| `identifier` | string | Email address, phone number or username (`identifier` step), up to 320 characters. |
| `password` | string | The password (`password` step), up to 128 characters. |
| `connection_id` | string | A `sso_connections[].connection_id` value (`sso_start` step). |

## Response

`200 OK` with the transaction after the step. A `completed` transaction has no `csrf_token`.

When the environment requires a second factor and the user has none yet, a user still inside the environment's
`mfa.enrollment_grace_days` signs in anyway and the `completed` response carries `mfa_enrollment`:
`required_by` (RFC 3339, the end of the grace, computed by the server) and `methods` (`webauthn`, `totp`: the factors the
user can enrol now, in screen order). Show the date and let the user continue; enrolment is added in the account
settings. The field is absent when there is no grace, the user meets the policy, or enrolment is required at once. An
application's `min_acr` and the requested `acr_values` are never postponed by the grace.

## Enterprise SSO (home realm discovery)

When the `identifier` step receives an email address whose domain belongs to an enterprise connection, the sign-in
continues with that connection. Only the part after `@` is used for discovery.

- One connection: the transaction moves to `federated_redirect` and the response carries `redirect_url`, the
  identity provider start address. Navigate the top-level window there. The address contains a one-time token: do not
  store or log it.
- Several connections: the transaction stays in `identifier_pending` and the response lists `sso_connections`
  (`connection_id`, `display_name`, `protocol`). Show `display_name` as text and send the user's choice with the
  `sso_start` step (`identifier` and `connection_id`). A connection that is not offered for the domain returns
  `422 validation_failed`.
- No connection, or discovery is temporarily unavailable: the normal sign-in continues, so an outage never blocks
  the other sign-in methods. `sso_start` is an explicit choice and returns `503 temporarily_unavailable` instead.

When the sign-in request names an organization that requires SSO, the transaction offers only that organization's
connections: the password and email code steps are not offered, and the passkey stays available only when the
organization has emergency (break-glass) accounts. Creating the transaction already starts the only connection
(`federated_redirect` with `redirect_url`) or lists several in `sso_connections`; send the choice with `sso_start`
and `connection_id` alone, `identifier` is optional here. Under this requirement discovery does not fall back to
other methods: when enterprise sign-in cannot be reached, the step returns `503 temporarily_unavailable`. The same
applies when the email domain belongs to an organization that requires SSO for its verified domains.

After the identity provider, the browser returns to `/u/sso/complete` on the sign-in host; the hosted page finishes
the sign-in.

## Linking enterprise sign-in to an existing account

When an enterprise connection does not vouch for email addresses and the address it asserts is already verified on an
account of the environment, the enterprise sign-in does not open a session. The browser comes back to the hosted sign-in
page with a new transaction whose object carries `account_link`: `connection_name` and, when the connection belongs to
an organization, `organization_name`. Tell the user that they already have an account and must sign in to it to link
the connection; never name the account. In this transaction `sso_start` and `magic_link` are not offered, and the
`identifier` step does not route to an enterprise connection.

The user signs in to the existing account with its own methods; a user with a second factor enrolled is asked for it.
The step that completes the transaction opens no session: the connection is linked to the account, and the response is a
new transaction in `federated_redirect` with `redirect_url` for the same connection. Navigate the top-level window there;
the enterprise sign-in now finishes as usual. If enterprise sign-in cannot be started, the new transaction is
`identifier_pending` and an ordinary sign-in follows.

Completing with another account, without the enrolled second factor, or with a sign-in older than the link request links
nothing and opens no session: `409 transaction_state_conflict`. A link request that has expired (10 minutes) or was
already used returns `410 transaction_expired`. A link request is used once, whatever the outcome. To decline, call
`POST /fapi/v1/transactions/current:cancel`.

## Organization policy

When the authorization request is bound to an organization, the organization's sign-in policy is checked again after
the first factor and just before the session is opened:

- The client's IP address is not in the organization's allowlist: `403 ip_not_allowed`.
- The transaction's factors do not meet the organization's policy (a method the organization does not allow, a missing
  factor, or a security key or passkey outside the organization's AAGUID allowlist): `403 no_allowed_method`.
- The organization's policy cannot be read: `503 temporarily_unavailable`. The sign-in fails closed and never continues
  without the policy.

No session is opened. A refusal when the transaction completes removes the transaction cookie; a `503` after the first
factor leaves the transaction where it was, and the step can be retried. The error body never names the organization,
the user or the IP address.

## DPoP-bound headless transactions

For a transaction started with a `DPoP` header, send a new proof from the same key (`htm` `POST`, `htu` the step URL)
instead of `Idp-Csrf-Token`; no cookie is used. The step that completes the transaction returns
`authorization_response` once:

```json
{
  "state": "completed",
  "authorization_response": { "code": "<code>", "state": "<your state>", "iss": "https://example.keenauth.app" },
  "next_steps": [],
  "schema_version": 1,
  "expires_at": "2026-11-02T10:15:00Z"
}
```

Check that `state` and `iss` match, then exchange the code at `/oauth2/token` with your `code_verifier` and a DPoP proof
from the same key. A proof from another key returns `invalid_grant`. If the authorization request cannot end in a code
(consent, legal terms, an organization choice or a new sign-in is needed, or access is denied), the step returns
`403 authorization_failed` and the `error` member carries the OAuth error code; no code is issued.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 401 | `invalid_credentials` | The identifier or password is not accepted. |
| 403 | `origin_not_allowed` | `Origin` or `Idp-Csrf-Token` is missing or wrong. |
| 403 | `ip_not_allowed` | The client's IP address is not in the organization's allowlist. |
| 403 | `no_allowed_method` | The sign-in does not meet the organization's policy. |
| 403 | `test_feature_not_in_production` | `otp_verify` or `email_2fa` got the fixed test code `424242` in a production environment. The attempt is counted like a wrong code. |
| 404 | `not_found` | The step name is not a known step. |
| 409 | `transaction_state_conflict` | The step is not available in the current state. |
| 409 | `session_limit_reached` | The user has reached the session limit and new sessions are denied. |
| 410 | `transaction_expired` | The transaction has ended. |
| 422 | `validation_failed` | A field is missing or invalid. |
| 503 | `temporarily_unavailable` | `sso_start` could not reach enterprise sign-in, or the organization's policy could not be read; try again. |
| 401 | `invalid_dpop_proof` | The `DPoP` proof is missing, invalid or reused. |
| 403 | `authorization_failed` | The DPoP-bound transaction completed but its authorization request ended without a code. |

## Example

```bash
curl -X POST https://example.keenauth.app/fapi/v1/transactions/current/steps/identifier \
  -H "Idp-Client: my-app/1.0.0 (node/22.0.0)" \
  -H "Origin: https://example.keenauth.app" \
  -H "Idp-Csrf-Token: $CSRF_TOKEN" \
  -H "Content-Type: application/json" \
  -b cookies.txt \
  -d '{"identifier":"jane@example.com"}'
```

## Personal data

This endpoint carries personal data: end-user profile and credentials. [Personal data in the KeenAuth APIs](/docs/policies/personal-data) describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.
