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

# Change Password

> Implement logged-in change password with the Prelude JavaScript SDK.

Let an authenticated user change their password from the account area. The SDK acquires the `prld:pwd:write` session scope via a short OTP challenge, then calls the change password endpoint — the scope is consumed atomically on save.

See the [Logged-in Change Password guide](/session/documentation/change-password) for the backend configuration (direct step-up on `prld:pwd:write`). This page focuses on the frontend integration.

## Flow at a glance

1. Request the `prld:pwd:write` scope via `requestStepUp`.
2. Drive the OTP step that Prelude returns (`verify_email` or `verify_sms`, depending on the user's identifiers).
3. Once the SDK has auto-refreshed the session with the granted scope, call `client.changePassword(password)`.

## Example

```javascript theme={null}
import { PrldErrors } from "@prelude.so/js-sdk";

async function changePassword(newPassword) {
  const { status } = await client.requestStepUp({
    scope: "prld:pwd:write",
    onChallenge: async (info) => {
      if (info.currentStep === "verify_email" || info.currentStep === "verify_sms") {
        // Send the OTP — the user will enter the code in your UI and
        // your form handler will call client.checkOTP(...) with it.
        await client.startOTP({ challengeId: info.challengeId });
      }
    },
  });

  if (status === "block") {
    throw new Error("Change password denied");
  }

  // The SDK refreshes the session automatically once the last step is
  // completed, so the access token now carries prld:pwd:write.
  await client.changePassword(newPassword);
}
```

For the full step-up SDK surface (`startOTP`, `checkOTP`, `retryOTP`, challenge callback shape), see [Step-Up](/session/documentation/frontend-sdks/web/step-up).

<Accordion title="Try it" icon="flask">
  This example builds on the project from [Introduction](/session/documentation/frontend-sdks/web/introduction). Make sure you have a working password login first ([Password](/session/documentation/frontend-sdks/web/password)) and that the user you log in with has an `email_address` or `phone_number` identifier.

  **1. Configure direct step-up for `prld:pwd:write`**

  ```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 }
            ]
          }
        }
      ]
    }'
  ```

  **2. Register the scope**

  ```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" }'
  ```

  **3. Replace `src/App.jsx`**

  ```jsx src/App.jsx theme={null}
  import "@picocss/pico";
  import { useState } from "react";
  import { PrldSessionClient, PrldErrors } from "@prelude.so/js-sdk";

  const client = new PrldSessionClient({ domain: `${import.meta.env.VITE_APP_ID}.session.prelude.dev` });

  export default function App() {
    const [view, setView] = useState("login"); // login | logged | stepup-otp | new-password | done
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const [otpCode, setOtpCode] = useState("");
    const [newPassword, setNewPassword] = useState("");
    const [challengeId, setChallengeId] = useState(null);
    const [error, setError] = useState(null);

    // ── Login ──────────────────────────────────────────────────

    const handleLogin = async (e) => {
      e.preventDefault();
      setError(null);
      try {
        await client.loginWithPassword({
          identifier: email,
          password,
        });
        await client.refresh();
        setView("logged");
      } catch (err) {
        setError(err.message);
      }
    };

    // ── Step-up for prld:pwd:write ─────────────────────────────

    const handleChallenge = (info) => {
      setChallengeId(info.challengeId);
      if (info.currentStep === "completed") {
        // Scope granted — session already refreshed by the SDK.
        setView("new-password");
      } else if (info.currentStep === "verify_email" || info.currentStep === "verify_sms") {
        setView("stepup-otp");
        client.startOTP({ challengeId: info.challengeId });
      }
    };

    const handleStartChangePassword = async () => {
      setError(null);
      try {
        const { status } = await client.requestStepUp({
          scope: "prld:pwd:write",
          onChallenge: handleChallenge,
        });
        if (status === "block") setError("Change password denied.");
      } catch (err) {
        setError(err.message);
      }
    };

    const handleCheckOTP = async (e) => {
      e.preventDefault();
      setError(null);
      try {
        await client.checkOTP({ code: otpCode, challengeId: challengeId, onChallenge: handleChallenge });
      } catch (err) {
        if (err instanceof PrldErrors.BadCheckCode) setError("Wrong code.");
        else setError(err.message);
      }
    };

    const handleSubmitPassword = async (e) => {
      e.preventDefault();
      setError(null);
      try {
        await client.changePassword(newPassword);
        setView("done");
      } catch (err) {
        setError(err.message);
      }
    };

    // ── Render ─────────────────────────────────────────────────

    return (
      <>
        <style>{`body, #root { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0; }`}</style>
        <main style={{ textAlign: "center", maxWidth: "600px", width: "100%" }}>
          <h1>Change Password Demo</h1>
          {error && <p role="alert" style={{ color: "var(--pico-color-red-500)" }}>{error}</p>}

          {view === "login" && (
            <form onSubmit={handleLogin}>
              <input type="email" placeholder="user@example.com" value={email} onChange={(e) => setEmail(e.target.value)} required />
              <input type="password" placeholder="Current password" value={password} onChange={(e) => setPassword(e.target.value)} required />
              <button type="submit">Log in</button>
            </form>
          )}

          {view === "logged" && (
            <article>
              <p>Logged in.</p>
              <button onClick={handleStartChangePassword}>Change password</button>
            </article>
          )}

          {view === "stepup-otp" && (
            <form onSubmit={handleCheckOTP}>
              <p>Enter the verification code we just sent you.</p>
              <input type="text" placeholder="OTP code" value={otpCode} onChange={(e) => setOtpCode(e.target.value)} required />
              <button type="submit">Verify</button>
              <button type="button" className="secondary" onClick={() => client.retryOTP()}>Resend</button>
            </form>
          )}

          {view === "new-password" && (
            <form onSubmit={handleSubmitPassword}>
              <p>Choose a new password.</p>
              <input type="password" placeholder="New password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required />
              <button type="submit">Save</button>
            </form>
          )}

          {view === "done" && (
            <article>
              <p>Password updated. The `prld:pwd:write` scope has been consumed.</p>
            </article>
          )}
        </main>
      </>
    );
  }
  ```

  Run `npm run dev`, log in, then click **Change password**. You'll receive an OTP on the identifier type configured above. After verification, enter a new password — the `prld:pwd:write` scope is consumed atomically on save and removed from the session.
</Accordion>
