# Multi-Factor Authentication

How Polyester MFA works: TOTP, passkeys, recovery codes, elevated sessions, and fresh step-up for high-risk actions.

Polyester uses multi-factor authentication (MFA) to protect interactive sessions. Routine requests use the session token created at login. Sensitive operations require stronger proof: either a recent MFA-elevated session or a fresh, single-use step-up.

> **Use passkeys when available**
>
> Passkeys are fast and phishing-resistant. TOTP remains available when passkeys are not supported.

***

## Factors

- **Passkeys** use WebAuthn and a device credential such as Face ID, Touch ID, Windows Hello, a laptop PIN, or a hardware security key.
- **TOTP** uses a time-based code from an authenticator app.
- **Recovery codes** are one-time backup credentials. Store them offline or in a password manager.

The first enrolled factor creates the account's recovery codes. Adding another factor requires proof from an existing factor. After TOTP or passkey enrollment succeeds, replace the current bearer token with the returned `access_token`. The enrollment response also includes `session` and `access_token_expires_at`.

***

## Session assurance

| Requirement        | Client credential                                 | Validity                       |
| ------------------ | ------------------------------------------------- | ------------------------------ |
| Primary session    | Bearer token created at login                     | Routine authenticated access   |
| Recent MFA session | MFA-elevated bearer token                         | 30 minutes from MFA completion |
| Fresh step-up      | Separate `step_up_token` sent in `X-Auth-Step-Up` | One protected action           |

A fresh step-up is bound to the interactive account and session. The proof is not reusable, but successful verification also returns an independent MFA-elevated `access_token` for subsequent Recent MFA requirements.

***

## Completing a challenge

When an endpoint requires a fresh proof:

1. Call `MFAService/BeginMFAChallenge` with `MFA_CHALLENGE_PURPOSE_FRESH_STEP_UP`.
2. Verify the challenge with TOTP, a passkey, or a recovery code.
3. Replace the current bearer token with the returned `access_token`.
4. Retry the original request with the returned `step_up_token` in `X-Auth-Step-Up`.

For endpoints that accept a recent MFA session, use `MFA_CHALLENGE_PURPOSE_SESSION_ELEVATION` instead and replace the bearer token with the returned MFA-elevated `access_token`. Both challenge purposes return `session`, `access_token`, and `access_token_expires_at`. A Fresh Step-Up response additionally returns `step_up_token` and `step_up_expires_at`.

> **Step-up tokens are single-use**
>
> Send the token only with the protected operation and do not reuse it across requests.

***

## Endpoint reference

The following tables describe the current production policy by API method. The REST and ConnectRPC references provide the transport-specific paths.

### Recent MFA session required

| Area                 | Methods                                                                                           | Notes                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Address book entries | `CreateAddressBookEntry` `UpdateAddressBookEntry` `DeleteAddressBookEntry` `CopyAddressBookEntry` | With an enrolled factor, MFA completed within the previous 30 minutes satisfies the requirement. |

When no factor is enrolled, address-book entry mutations do not prompt for MFA. After enrollment, elevate the session again when its recent-MFA window expires.

### Fresh step-up required

| Area                | Methods                                                                                          | Notes                                                                                                           |
| ------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| API keys            | `CreateApiKey` `UpdateApiKey` `DeleteApiKey`                                                     | Creating a key also requires MFA enrollment. Every API key update is protected.                                 |
| Subaccount policies | `CreateSubaccountPolicy` `UpdateSubaccountPolicy` `DeleteSubaccountPolicy` `SetSubaccountPolicy` | Setting a policy includes attaching or clearing it.                                                             |
| API key policies    | `CreateApiPolicy` `UpdateApiPolicy` `DeleteApiPolicy` `SetApiKeyPolicy`                          | Creating and assigning a policy in one request uses one proof.                                                  |
| MFA security        | `DeleteMFAFactor` `RegenerateRecoveryCodes`                                                      | The last factor cannot be deleted. Regeneration invalidates previous recovery codes.                            |
| Subaccount members  | `InviteSubaccountMember` `UpdateSubaccountMemberRole`                                            | —                                                                                                               |
| Guard approvals     | `SignProtectedAction` `BatchSignProtectedActions`                                                | Required for supported external and internal whitelist mutations. A batch needs one when any item is protected. |
| Guard wallet        | `RotateGuardSignerWallet` `ExportGuardSignerWallet`                                              | Both require MFA enrollment.                                                                                    |

### Conditional requirements

| Area              | Methods                                          | Rule                                                                                                                                                                                                                                                        |
| ----------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Factor enrollment | `BeginTOTPEnrollment` `BeginPasskeyEnrollment`   | Fresh step-up is required when the account already has a factor. The first factor is exempt.                                                                                                                                                                |
| Finish enrollment | `FinishTOTPEnrollment` `FinishPasskeyEnrollment` | Additional-factor enrollment must follow a begin request that validated step-up. Successful completion returns a new MFA-elevated access token.                                                                                                             |
| Member MFA policy | `SetSubaccountMemberMFARequirement`              | Disabling `require_member_mfa` requires fresh step-up. Enabling it does not.                                                                                                                                                                                |
| Remove member     | `RemoveSubaccountMember`                         | Removing another member requires fresh step-up. Leaving the subaccount yourself does not.                                                                                                                                                                   |
| Respond to invite | `RespondSubaccountInvite`                        | Accepting requires enrollment and Recent MFA when `require_member_mfa=true`. Declining or cancelling does not.                                                                                                                                              |
| Wallet withdrawal | `CreateWalletTradingWithdraw`                    | Root-owner Trading-to-Funding and root-owner withdrawals to explicitly whitelisted external destinations accept a recent MFA session. Delegated and other interactive movements require fresh step-up. An existing idempotent intent requires no new proof. |
| Internal transfer | `CreateInternalTransfer`                         | Root-owner same-owner transfers and root-owner transfers to explicitly whitelisted destinations accept a recent MFA session. Delegated and other interactive transfers require fresh step-up. API key requests are exempt.                                  |

### Similar endpoints that do not require MFA

- `CreateTradingWithdraw` is API-key-only. It requires a signed payload and the relevant policy permission.
- API-key-authenticated internal transfers do not use interactive MFA.
- `CreateGuardSignerWallet` and `GetGuardSignerStatus` do not require MFA.
- Read-only API key, policy, address-book, destination, and whitelist methods do not require MFA.
- Order placement, amendment, and cancellation do not require MFA.

API key sessions cannot satisfy an interactive MFA requirement. Where an API key is not explicitly exempt, the endpoint rejects that session type.

See [Withdrawals and Transfers Security](https://testnet.polyester.com/docs/developer-docs/authentication-security/withdrawals-and-transfers-security) for the complete money-movement model.

***

## MFA, signatures, and whitelists

These controls solve different problems:

- MFA confirms that the interactive user is present.
- A wallet or API key signature binds the asset, amount, source, destination, nonce, and expiry.
- A whitelist limits where funds may move.

Protected money-movement flows can require more than one of these checks.
