|
35 | 35 | * { "currentPassword": "...", "newPassword": "..." } |
36 | 36 | * }</pre> |
37 | 37 | * |
| 38 | + * <p>{@code currentPassword} is still required in the request body, but is only verified |
| 39 | + * against the stored hash when the account actually has one. Accounts with no password yet |
| 40 | + * (e.g. OAuth-only signups) can set their first password through this same endpoint without |
| 41 | + * a matching current one to confirm — see the {@code hasPassword} check in {@link #handle}. |
| 42 | + * |
38 | 43 | * <p>Does not invalidate other active sessions/JWTs — out of scope for JWT-based |
39 | 44 | * auth unless a token-versioning/blacklist mechanism is added separately. |
40 | 45 | */ |
@@ -108,7 +113,15 @@ public void handle(JsonRequest req, JsonResponse res) { |
108 | 113 | var user = userOpt.get(); |
109 | 114 | var storedHash = user.containsKey("password") && user.get("password").isString() |
110 | 115 | ? user.getString("password").getValue() : null; |
111 | | - if (storedHash == null || !TokenUtils.checkPassword(currentPassword, storedHash)) { |
| 116 | + |
| 117 | + // Accounts that never had a password set (e.g. OAuth-only signups — restheart-accounts |
| 118 | + // stores an empty, not null, password field for those) have nothing to confirm here. |
| 119 | + // Skipping the check is safe: this endpoint is `secure = true` and always acts on the |
| 120 | + // authenticated principal's own document (`email` above comes from |
| 121 | + // account.getPrincipal().getName()), never an arbitrary user. Also avoids |
| 122 | + // TokenUtils.checkPassword() throwing on an empty/malformed BCrypt hash. |
| 123 | + var hasPassword = storedHash != null && !storedHash.isBlank(); |
| 124 | + if (hasPassword && !TokenUtils.checkPassword(currentPassword, storedHash)) { |
112 | 125 | Errors.error(res, HttpStatus.SC_UNAUTHORIZED, "Invalid current password"); |
113 | 126 | return; |
114 | 127 | } |
|
0 commit comments