> ## 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.

# Logged-in Change Password

> Let authenticated users change their password using direct step-up, without configuring a delegation hook.

Once a user is authenticated, you can let them change their password from the account area without asking for their current password and without forcing them to sign out. Prelude protects the change password endpoint with the `prld:pwd:write` session scope — the user acquires the scope by completing a short verification challenge (SMS or email OTP), then calls the [change password](/session/api-reference/frontend/reset-password) endpoint.

This guide shows how to wire that flow using a **direct** step-up scope configuration: you declare the challenge inline on the scope entry and Prelude serves it without calling any backend hook.

## How it works

```mermaid theme={null}
sequenceDiagram
  autonumber
  actor U as User (logged in)
  participant SDK as Frontend SDK
  participant P as Prelude API

  U->>SDK: Click "Change password"
  SDK->>P: POST /v1/session/stepup/request (scope=prld:pwd:write)
  P-->>SDK: status=review, steps=[verify_sms | verify_email]

  loop Managed OTP step
    U->>SDK: Enter OTP code
    SDK->>P: startOTP / checkOTP (challengeId)
    P-->>SDK: step done
  end

  SDK->>P: refresh(step_up_token)
  P-->>SDK: access_token with prld:pwd:write

  U->>SDK: Submit new password
  SDK->>P: POST /v1/session/me/password/reset
  P-->>SDK: 204 No Content (scope consumed)
```

Because the step-up response is configured inline on the scope entry (`mode: "direct"`), there is **no need to run a delegation hook**: no `delegation_hook` is involved and `jwks_url` can be left empty as long as every `allowed_scopes` entry uses `direct` mode.

## 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
* Password login configured on the application — see [Password Authentication](/session/documentation/integration-guide/password-authentication)
* Users with an `email_address` or `phone_number` identifier (used for the OTP step)

## Configure the step-up flow

<Steps>
  <Step title="Register the prld:pwd:write scope">
    Add `prld:pwd:write` to the application's allowed scopes so it can be requested from the frontend:

    ```bash theme={null}
    curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/config/scopes \
      -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
      -H "Content-Type: application/json" \
      -d '{ "scope": "prld:pwd:write" }'
    ```
  </Step>

  <Step title="Create a direct step-up configuration">
    Create a step-up configuration that resolves `prld:pwd:write` directly. The `jwks_url` can stay empty because no delegated scope is configured.

    Because the step differs depending on the user's identifier (`verify_email` for email, `verify_sms` for phone), we register two `allowed_scopes` entries for the same scope, each scoped to a different identifier type. The first entry that matches one of the user's identifiers is used.

    ```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": "",
        "step_keys": [],
        "allowed_scopes": [
          {
            "scope": "prld:pwd:write",
            "mode": "direct",
            "direct": {
              "identifier_types": ["email_address"],
              "status": "review",
              "granted_for": 300,
              "grant_mode": "single-use",
              "steps": [
                { "order": 1, "key": "verify_email", "expiration_duration": 600 }
              ]
            }
          },
          {
            "scope": "prld:pwd:write",
            "mode": "direct",
            "direct": {
              "identifier_types": ["phone_number"],
              "status": "review",
              "granted_for": 300,
              "grant_mode": "single-use",
              "steps": [
                { "order": 1, "key": "verify_sms", "expiration_duration": 600 }
              ]
            }
          }
        ]
      }'
    ```

    | Field                                                                     | Description                                                                                                                                              |
    | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `allowed_scopes[].scope`                                                  | Scope this entry resolves.                                                                                                                               |
    | `allowed_scopes[].mode`                                                   | `direct` to serve a static decision, `delegated` to call a delegation hook.                                                                              |
    | `allowed_scopes[].direct.identifier_types`                                | Identifier types the user must hold for this entry to match. Valid values: `email_address`, `phone_number`.                                              |
    | `allowed_scopes[].direct.status` / `grant_mode` / `granted_for` / `steps` | The decision fields, flattened. Same shape as the [step-up hook response](/session/documentation/step-up-hook#hook-response), inline on the scope entry. |

    <Note>
      Entries are matched in declaration order. The first `direct` entry whose `scope` matches **and** whose `identifier_types` overlaps with the user's identifiers wins. Put the preferred challenge at the top.
    </Note>

    You **can** list multiple identifier types in a single entry when the same decision applies to all of them (e.g. a custom step that is identifier-agnostic). Here we need two entries because the step differs: `verify_email` for email users, `verify_sms` for phone users. If you only support one identifier type, provide a single entry and list just that type.

    <Note>
      If no `direct` entry's `identifier_types` match the user, Prelude falls back to a `delegated` entry for the same scope when one exists. Add one — pointing at your delegation hook — if you want to keep the flow alive for users whose identifier type isn't covered by any `direct` entry.
    </Note>
  </Step>
</Steps>

## Relationship with OTP login `grant_change_password`

The step-up path described above is for users who are **already logged in** and want to change their password from the account area. It is complementary to the `grant_change_password` flag on [OTP login configurations](/session/api-reference/management/config/login-otp/create-login-otp-config), which covers a different entry point:

| Entry point                                 | Mechanism                                                                                           | When to use                                                                                                                        |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Unlogged change password / account creation | OTP login with `grant_change_password: true` attaches `prld:pwd:write` to the session at login time | The user proves possession of the identifier via the login OTP itself, so requiring another step-up right after would be redundant |
| Logged-in change password                   | Static step-up on `prld:pwd:write`                                                                  | The user is already authenticated and we want a fresh proof of possession before changing the password                             |

Both can — and usually should — coexist on the same application.

## Trigger the flow from the frontend

Once the configuration above is in place, wire the flow into your frontend. See the [Web SDK Change Password guide](/session/documentation/frontend-sdks/web/change-password) for the full code example and a runnable **Try it** sandbox.

## What happens server-side

1. `POST /v1/session/stepup/request` with `scope=prld:pwd:write` looks up the app's step-up configuration.
2. Because `prld:pwd:write` has direct entries, Prelude picks the first entry whose `identifier_types` matches the user and returns its inline decision — no hook is invoked.
3. The user completes the OTP step. On completion Prelude mints a step-up token which the SDK uses to refresh the session; the new access token carries `prld:pwd:write`.
4. `POST /v1/session/me/password/reset` validates the scope, writes the new password, and **atomically removes the scope from the session** so it cannot be reused.

If you later want to upgrade the flow with custom signals — for example to deny the change password for suspicious contexts — switch the entry's `mode` from `direct` to `delegated` and point it at a [delegation hook](/session/documentation/step-up-hook). Direct and delegated scopes can coexist in the same configuration.

## Constraints

| Rule                                      | Limit                                                                                                                      |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `jwks_url`                                | Required when at least one `allowed_scopes` entry uses `delegated` mode. Can be empty when every entry uses `direct` mode. |
| `direct.identifier_types`                 | Must not be empty. Allowed values: `email_address`, `phone_number`.                                                        |
| `(scope, identifier_type)` pairs (direct) | Must be unique across all `direct` entries.                                                                                |
| Delegated entries per scope               | At most one. It is used as a fallback when no `direct` entry matches.                                                      |
| Response `granted_for`                    | 0 to 86400 seconds. For a change password, keep it short (60–300 seconds is typical).                                      |
| Response `grant_mode`                     | Use `single-use` so the scope is attached to a single access token only.                                                   |

## What's next?

<CardGroup cols={2}>
  <Card title="Step-Up Authentication" icon="shield-check" href="/session/documentation/step-up-authentication">
    Full overview of step-up, including dynamic decisions via the delegation hook.
  </Card>

  <Card title="Step-Up Hook" icon="webhook" href="/session/documentation/step-up-hook">
    Response shape used by delegated scopes. The same fields are inlined on direct scope entries.
  </Card>
</CardGroup>
