> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prelude.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Step-Up Authentication

> Add scoped, multi-step authentication challenges to existing sessions.

Step-up authentication lets you require additional proof from an already-authenticated user before granting access to sensitive operations. Instead of re-authenticating from scratch, the user completes a challenge (SMS OTP, email OTP, etc.) and receives a time-limited scope on their session.

## Prerequisites

* A Prelude account with access to the Session API
* An **Application ID** (`appID`) — see [Applications](/session/documentation/applications)
* Your **Management API key** for backend calls
* A working login flow (users must be authenticated before requesting step-up)

## How it works

When a user requests a sensitive action, your frontend asks Prelude for a scope grant. Prelude calls a **hook** on your backend, where you decide whether to grant the scope immediately, require a challenge, or block the request entirely.

If you require a challenge, Prelude walks the user through each step you defined. Prelude natively handles **managed steps** like `verify_sms` and `verify_email` — no extra work on your side. Once every step is completed, the scope is granted to the user's access token.

```mermaid theme={null}
sequenceDiagram
  autonumber
  actor U as User
  participant P as Prelude API
  participant B as Your Backend

  U->>P: POST /v1/session/stepup/request (scope)
  P->>B: Hook (scope_requested, identifiers, signals, metadata)
  B-->>P: status, granted_for, grant_mode, steps[]

  alt status = continue
    P-->>U: Scope granted immediately
  else status = review
    P-->>U: challenge_token (steps to complete)

    loop For each managed step
      U->>P: Complete OTP flow with challenge_token
      P-->>U: Updated challenge_token
    end

    U->>P: POST /v1/session/refresh (step_up_token)
    P-->>U: access_token with granted scope
  else status = block
    P-->>U: Scope denied
  end
```

<Note>
  You can also define **custom steps** handled by your own backend (e.g. KYC review, biometric check). See [Custom Steps](/session/documentation/step-up-custom-steps) for details.
</Note>

## Configure step-up

<Steps>
  <Step title="Create a step-up configuration">
    Register each scope you want to expose and how its decision is produced. Use `mode: "delegated"` to call your delegation hook, or `mode: "direct"` to serve a static decision without any hook call.

    ```bash theme={null}
    curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/config/stepup \
      -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
      -H "Content-Type: application/json" \
      -d '{
        "jwks_url": "https://api.example.com/.well-known/jwks.json",
        "step_keys": [],
        "allowed_scopes": [
          {
            "scope": "transfer:write",
            "mode": "delegated",
            "delegated": {
              "delegation_hook": "https://api.example.com/hooks/stepup"
            }
          },
          {
            "scope": "payment:confirm",
            "mode": "delegated",
            "delegated": {
              "delegation_hook": "https://api.example.com/hooks/stepup"
            }
          }
        ]
      }'
    ```

    | Field                                        | Required                             | Description                                                                                                                                    |
    | -------------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
    | `jwks_url`                                   | When any scope uses `delegated` mode | Your JWKS endpoint. Used to verify verification tokens issued by your backend for [custom steps](/session/documentation/step-up-custom-steps). |
    | `step_keys`                                  | Yes                                  | Custom step keys for client-owned steps. Leave empty if you only use managed steps.                                                            |
    | `allowed_scopes[].scope`                     | Yes                                  | The scope name.                                                                                                                                |
    | `allowed_scopes[].mode`                      | Yes                                  | `delegated` to call your delegation hook, `direct` to serve a static decision.                                                                 |
    | `allowed_scopes[].delegated.delegation_hook` | When `mode` is `delegated`           | The URL Prelude calls when this scope is requested. See [Step-Up Hook](/session/documentation/step-up-hook).                                   |
    | `allowed_scopes[].direct`                    | When `mode` is `direct`              | The static decision to return. See [Change Password](/session/documentation/change-password) for an end-to-end direct example.                 |

    <Note>
      Scope names must only contain: lowercase letters, uppercase letters, numbers, and the characters `.-_:`.
    </Note>

    <Note>
      A scope may appear more than once in `allowed_scopes`. Multiple `direct` entries are allowed as long as each `(scope, identifier_type)` pair is unique — useful when the step differs per identifier type. At most one `delegated` entry per scope is allowed; when combined with `direct` entries, it is used as a fallback if no `direct` entry matches the user's identifier types at runtime.
    </Note>
  </Step>

  <Step title="Implement the hook endpoint">
    Your backend must expose the hook URL you registered. When Prelude receives a step-up request, it calls your hook with user and session context. Your hook decides whether to grant, challenge, or block.

    See the full [Step-Up Hook Reference](/session/documentation/step-up-hook) for the request/response format and constraints.
  </Step>
</Steps>

## The step-up flow

### 1. Request a scope grant

The frontend SDK initiates the flow by calling `requestStepUp` with a scope. Prelude calls your [hook](/session/documentation/step-up-hook) and returns one of:

| Status     | Meaning                                                                                      |
| ---------- | -------------------------------------------------------------------------------------------- |
| `continue` | Scope granted immediately, no challenge needed. The SDK refreshes the session automatically. |
| `review`   | A challenge was created. The user must complete all steps.                                   |
| `block`    | Scope denied.                                                                                |

### 2. Complete managed steps

Prelude handles the following step types natively:

| Step key       | Description                                              |
| -------------- | -------------------------------------------------------- |
| `verify_sms`   | SMS OTP verification sent to the user's phone number.    |
| `verify_email` | Email OTP verification sent to the user's email address. |

The SDK provides `startOTP`, `checkOTP`, and `retryOTP` methods to drive these steps using the `challengeId` from the `onChallenge` callback.

### 3. Automatic completion

When the last step is completed, the SDK automatically refreshes the session with the granted scope. The new access token behavior depends on the `grant_mode` set by your hook:

| Grant mode      | Behavior                                                                                                                     |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `single-use`    | The scope is attached only to this access token and expires after `granted_for` seconds. It is not persisted on the session. |
| `session-bound` | The scope is stored on the session and included in every subsequent refresh for `granted_for` seconds.                       |

See the [Web SDK Step-Up guide](/session/documentation/frontend-sdks/web/step-up) for the full integration with code examples.

## Constraints and validation

### Field format

All external fields (scopes, step keys, metadata keys) must match:

```
a-z A-Z 0-9 . - _ :
```

No other characters are allowed.

### Metadata

| Rule                     | Limit         |
| ------------------------ | ------------- |
| Maximum number of fields | 5             |
| Maximum key length       | 12 characters |
| Maximum value length     | 32 characters |

### Hook response

| Rule                       | Limit                                                                                                                                                                      |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Maximum response size      | 64 KB                                                                                                                                                                      |
| `granted_for`              | In seconds. Must be between 0 and 86400 (24 hours). Cannot be negative.                                                                                                    |
| `granted_for` default      | When `grant_mode` is `session-bound` and `granted_for` \< 1, it defaults to 600 seconds (10 minutes). When `grant_mode` is `single-use`, `granted_for` must be at least 1. |
| Hook timeout               | **5 seconds**. Your endpoint must respond within this time.                                                                                                                |
| Step `expiration_duration` | In seconds. Must be between 0 and 86400 (24 hours). Cannot be negative.                                                                                                    |

### Step-Up JWKS

Prelude exposes a dedicated JWKS endpoint for step-up token verification at:

```
https://<app_id>.session.prelude.dev/.well-known/step-up-jwks.json
```

This is separate from the main [JWKS endpoint](/session/documentation/jwks) used for access token verification.

## Security recommendations

<Warning>
  **Anti-replay**: When your backend receives a request carrying a scope granted via step-up, you should allow each scoped token to be used **only once**. Track the token's `jti` claim and reject any replayed token. This prevents an attacker from reusing a captured token to repeat a sensitive action.
</Warning>

* **Limit scope lifetime**: Use the `granted_for` field to keep scoped access as short-lived as practical. A transfer confirmation might only need 60 seconds.
* **Use `grant_mode: "single-use"`** for high-sensitivity operations (e.g. fund transfers). This ensures the scope is attached to a single access token and not persisted on the session.
* **Validate signals in your hook**: Prelude sends `user_agent`, `platform`, and `ip` in the hook request. Use these to detect suspicious context changes (e.g. a different IP than the original login).

## What's next?

<CardGroup cols={2}>
  <Card title="Custom Steps" icon="puzzle-piece" href="/session/documentation/step-up-custom-steps">
    Add client-owned steps like KYC review or biometric checks to your challenges.
  </Card>

  <Card title="Step-Up Hook" icon="webhook" href="/session/documentation/step-up-hook">
    Full API reference for the hook endpoint your backend must implement.
  </Card>
</CardGroup>

* Browse the [Step-Up API Reference](/session/api-reference/frontend/stepup-request) for full endpoint details
* Configure step-up via the [Management API](/session/api-reference/management/config/stepup/create-stepup-config)
