Skip to main content

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.

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 for the backend configuration (direct step-up on prld:pwd:write). This page focuses on the mobile integration.

Flow at a glance

  1. Request the prld:pwd:write scope via requestStepUp.
  2. Fire OTP delivery for the returned challenge via sendStepUpOTP — the user receives the code on the identifier the server picked (verify_email or verify_sms).
  3. Submit the code via submitStepUpOTP.
  4. Once submitStepUpOTP returns no further step, the SDK has refreshed the session with the granted scope. Call client.changePassword.
changePassword consumes the scope atomically on save — the SDK invalidates the cached access token and runs a best-effort refresh so the next mint drops the now-spent scope. A leaked token therefore can’t change the password again without re-stepping up. For the full step-up SDK surface, see Step-Up.

Example

import PreludeSession

func changePassword(
    client: PreludeSessionClient,
    enteredCode: String,
    newPassword: String
) async throws {
    // 1. Acquire the scope.
    let challenge = try await client.requestStepUp(scope: "prld:pwd:write")
    guard challenge.status != .blocked else {
        throw PreludeSessionError.forbidden("Change password is not allowed")
    }

    // 2. Fire OTP delivery for the step the server picked.
    if challenge.currentStep == "verify_email"
        || challenge.currentStep == "verify_sms" {
        try await client.sendStepUpOTP(challenge)
    }

    // 3. Submit the code the user entered. In a real UI this is
    //    a separate submit handler bound to the user's input.
    let next = try await client.submitStepUpOTP(challenge, code: enteredCode)
    if next != nil {
        // Multi-step flow — call sendStepUpOTP(next) for the next
        // delivery step, then submitStepUpOTP again, until next == nil.
    }

    // 4. The SDK refreshed the session for us; the access token
    //    now carries `prld:pwd:write`. Save the new password.
    try await client.changePassword(RedactedString(newPassword))
}
Builds on the project from Introduction and a working password login (Password). The user you log in with must have an email_address or phone_number identifier.1. Configure direct step-up for prld:pwd:write
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
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 your entry file
Replace ContentView.swift:
ContentView.swift
import SwiftUI
import PreludeSession

private let appID = "YOUR_APP_ID"

enum CPView { case login, logged, stepUpOTP, newPassword, done }

@MainActor
final class ChangePasswordModel: ObservableObject {
    let client = try! PreludeSessionClient(
        endpoint: .custom("https://\(appID).session.prelude.dev")
    )

    @Published var view: CPView = .login
    @Published var error: String?
    var challenge: StepUpChallenge?

    func login(email: String, password: String) async {
        error = nil
        do {
            _ = try await client.loginWithPassword(
                LoginWithPasswordOptions(emailAddress: email, password: password)
            )
            view = .logged
        } catch { self.error = error.localizedDescription }
    }

    func startChangePassword() async {
        error = nil
        do {
            let c = try await client.requestStepUp(scope: "prld:pwd:write")
            if c.status == .blocked {
                error = "Couldn't change your password — please try again later."
                return
            }
            if c.currentStep == "verify_email" || c.currentStep == "verify_sms" {
                try await client.sendStepUpOTP(c)
            }
            challenge = c
            view = .stepUpOTP
        } catch { self.error = error.localizedDescription }
    }

    func submitOTP(_ code: String) async {
        guard let c = challenge else { return }
        error = nil
        do {
            if let next = try await client.submitStepUpOTP(c, code: code) {
                challenge = next  // unusual: more than one step
            } else {
                view = .newPassword  // scope granted
            }
        } catch PreludeSessionError.invalidOTPCode {
            error = "Wrong code."
        } catch { self.error = error.localizedDescription }
    }

    func savePassword(_ newPassword: String) async {
        error = nil
        do {
            try await client.changePassword(RedactedString(newPassword))
            view = .done
        } catch { self.error = error.localizedDescription }
    }
}

struct ContentView: View {
    @StateObject private var model = ChangePasswordModel()
    @State private var email = ""
    @State private var password = ""
    @State private var otp = ""
    @State private var newPassword = ""

    var body: some View {
        VStack(spacing: 12) {
            Text("Change Password Demo").font(.title3)
            if let error = model.error {
                Text(error).foregroundStyle(.red).font(.caption)
            }

            switch model.view {
            case .login:
                TextField("user@example.com", text: $email)
                    .textInputAutocapitalization(.never)
                    .keyboardType(.emailAddress)
                SecureField("Current password", text: $password)
                Button("Log in") {
                    Task { await model.login(email: email, password: password) }
                }.buttonStyle(.borderedProminent)
            case .logged:
                Text("Logged in.")
                Button("Change password") {
                    Task { await model.startChangePassword() }
                }.buttonStyle(.borderedProminent)
            case .stepUpOTP:
                Text("Enter the code we just sent you.")
                TextField("OTP code", text: $otp).keyboardType(.numberPad)
                Button("Verify") {
                    Task { await model.submitOTP(otp) }
                }.buttonStyle(.borderedProminent)
            case .newPassword:
                Text("Choose a new password.")
                SecureField("New password", text: $newPassword)
                Button("Save") {
                    Task { await model.savePassword(newPassword) }
                }.buttonStyle(.borderedProminent)
            case .done:
                Text("Password updated.").font(.headline)
                Text("`prld:pwd:write` has been consumed.")
                    .font(.caption).foregroundStyle(.secondary)
            }
        }
        .textFieldStyle(.roundedBorder)
        .padding()
    }
}
Build and run, log in, then tap 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.