OPAQUE never exposes the password to the server, so "changing a password" is a re-registration with a new password, authorized by proving knowledge of the current password via a valid JWT from a prior authentication.
Client Server
| |
|── authenticate(oldPassword) ──────────>| (standard OPAQUE auth -> JWT)
|<── JWT ────────────────────────────────|
| |
|── POST /opaque/password/start ────────>| (JWT auth + blinded element)
| Authorization: Bearer <jwt> |
|<── RegistrationStartResponse ─────────| (evaluated element + server public key)
| |
| [client derives new randomized_pwd, |
| creates new envelope + keys] |
| |
|── POST /opaque/password/finish ───────>| (JWT auth + new registration record)
| Authorization: Bearer <jwt> |
|<── 204 No Content ────────────────────| (old record replaced, sessions revoked)
Accepts the client's credential identifier and blinded OPRF element for a new password. Returns the server's OPRF-evaluated element and long-term public key.
- Request body:
RegistrationStartRequest(same as registration) - Response body:
RegistrationStartResponse(same as registration) - Authorization:
Bearer <jwt>required; subject must match credential identifier - Errors: 400 (bad request), 401 (unauthorized), 429 (rate limited)
Atomically replaces the old registration record, revokes all active JWT sessions for the credential, and stores the new registration record.
- Request body:
RegistrationFinishRequest(same as registration) - Response:
204 No Content - Authorization:
Bearer <jwt>required; same JWT used in/password/start - Errors: 400 (bad request), 401 (unauthorized)
-
Dedicated endpoints rather than overloading existing registration endpoints. This avoids ambiguity between recovery tokens and JWTs, allows separate rate limiting, and provides clearer API semantics.
-
JWT-based authorization. The user authenticates with their current password to obtain a JWT, which proves knowledge of the old password without the server ever seeing it.
-
Atomic replacement. The finish endpoint atomically deletes the old record, revokes all sessions, and stores the new record. The user must re-authenticate after changing their password.
-
No new DTOs. Request/response bodies are identical to the registration endpoints. The only difference is the mandatory
Authorization: Bearer <jwt>header. -
No changes to hofmann-rfc. The OPAQUE crypto layer already supports re-registration — password change is just registration at the crypto level.
HofmannOpaqueServerManager—changePasswordStart()andchangePasswordFinish()methods that validate the JWT, check subject matches the credential, and delegate to the existing registration crypto.OpaqueResource(JAX-RS) — thin adapter mappingPOST /password/startandPOST /password/finishto the manager. Exception mapping: SecurityException -> 401, IllegalArgumentException -> 400.OpaqueController(Spring Boot) — same endpoints as Spring@PostMappingmethods.
HofmannOpaqueAccessor—changePasswordStart()andchangePasswordFinish()HTTP transport methods targeting/opaque/password/startand/opaque/password/finish.HofmannOpaqueClientManager— high-levelchangePassword(serverId, credentialId, newPassword, bearerToken)method that orchestrates the full three-step flow.
OpaqueHttpClient.changePassword(credentialId, newPassword, token)— full orchestration: creates registration request, POSTs to/password/startwith JWT, finalizes registration locally, POSTs to/password/finishwith JWT.
OpaqueCli—change-passwordcommand that authenticates with the old password, then re-registers with the new password.
demo.html/demo.ts— Change Password card (step 3) with credential ID, new password, and JWT token fields. JWT is auto-filled from a prior authentication.
docs/opaque-api.yaml— Password Change tag and endpoint definitions for/opaque/password/startand/opaque/password/finish.
// Step 1: Authenticate with current password
AuthFinishResponse auth = manager.authenticate(serverId, credentialId, oldPassword);
// Step 2: Change password using the JWT
manager.changePassword(serverId, credentialId, newPassword, auth.token());
// Step 3: Re-authenticate with new password
AuthFinishResponse newAuth = manager.authenticate(serverId, credentialId, newPassword);// Step 1: Authenticate with current password
const { token } = await client.authenticate(credentialId, oldPassword);
// Step 2: Change password using the JWT
await client.changePassword(credentialId, newPassword, token);
// Step 3: Re-authenticate with new password
const newAuth = await client.authenticate(credentialId, newPassword);./gradlew :hofmann-testserver:runOpaqueCli \
--args="change-password alice@example.com oldpass newpass" -q- No password exposure: The server never sees the old or new password. The JWT proves knowledge of the old password; the OPRF registration proves knowledge of the new password.
- Subject matching: Both endpoints verify the JWT subject matches the credential identifier, preventing user A from changing user B's password.
- Session revocation: All existing sessions are revoked on password change, ensuring that compromised sessions are invalidated.
- Rate limiting: Password change endpoints are rate-limited to prevent abuse.