Phase 19: Local Auth (No-OIDC Mode) #23
@@ -257,7 +257,12 @@ meRouter.post(
|
|||||||
// T-19-07: verify current password before any update
|
// T-19-07: verify current password before any update
|
||||||
const isCorrect = verifyPassword(credRow.passwordHash, currentPassword);
|
const isCorrect = verifyPassword(credRow.passwordHash, currentPassword);
|
||||||
if (!isCorrect) {
|
if (!isCorrect) {
|
||||||
return c.json({ error: 'Current password incorrect' }, 401);
|
// CR-03: return 403 (NOT 401) for a wrong current password. The PWA's global
|
||||||
|
// MutationCache treats any 401 as "session expired" and arms the re-auth
|
||||||
|
// interstitial / login redirect — so a 401 here would force-log-out a user who
|
||||||
|
// merely mistyped their current password. 403 is in-app authorization-failure and
|
||||||
|
// lets the client surface "current password incorrect" without dropping the session.
|
||||||
|
return c.json({ error: 'Current password incorrect' }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
|
|||||||
expect(verifyPassword(updatedHash!, oldPassword)).toBe(false);
|
expect(verifyPassword(updatedHash!, oldPassword)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Test 2: wrong currentPassword → 401 and update is NOT called', async () => {
|
it('Test 2: wrong currentPassword → 403 and update is NOT called', async () => {
|
||||||
const { db } = await import('../../src/db/client.js');
|
const { db } = await import('../../src/db/client.js');
|
||||||
|
|
||||||
const realPassword = 'real-password-correct-789';
|
const realPassword = 'real-password-correct-789';
|
||||||
@@ -374,7 +374,9 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
|
|||||||
body: JSON.stringify({ currentPassword: 'WRONG-password', newPassword: 'new-pass-12345678' }),
|
body: JSON.stringify({ currentPassword: 'WRONG-password', newPassword: 'new-pass-12345678' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.status).toBe(401);
|
// CR-03: wrong current password returns 403 (in-app authz failure), NOT 401.
|
||||||
|
// A 401 would be interpreted by the PWA as session expiry and log the user out.
|
||||||
|
expect(res.status).toBe(403);
|
||||||
const body = (await res.json()) as { error: string };
|
const body = (await res.json()) as { error: string };
|
||||||
expect(body.error).toBe('Current password incorrect');
|
expect(body.error).toBe('Current password incorrect');
|
||||||
// Update must NOT have been called
|
// Update must NOT have been called
|
||||||
|
|||||||
@@ -141,9 +141,14 @@ export async function fetchLocalLogout(): Promise<void> {
|
|||||||
* Requires the user's current password and a new password (min 8 chars).
|
* Requires the user's current password and a new password (min 8 chars).
|
||||||
*
|
*
|
||||||
* Status codes:
|
* Status codes:
|
||||||
* 401 → wrong current password (throws Error with code 'wrong-current')
|
* 403 → wrong current password (throws Error('wrong-current')) — NOT a session expiry
|
||||||
* 422 → validation failure (throws Error with code 'validation')
|
* 401 / opaqueredirect → genuine session expiry (throws SessionExpiredError)
|
||||||
* other non-ok → generic error
|
* other non-ok → generic error (throws Error('server'))
|
||||||
|
*
|
||||||
|
* CR-03: the server returns 403 (not 401) for an incorrect current password so this
|
||||||
|
* client can distinguish an in-app authorization failure from a real session expiry.
|
||||||
|
* Treating that case as 401 would route it to the global MutationCache session-expiry
|
||||||
|
* handler and forcibly log the user out for a simple mistyped password.
|
||||||
*/
|
*/
|
||||||
export async function fetchChangePassword(body: {
|
export async function fetchChangePassword(body: {
|
||||||
currentPassword: string;
|
currentPassword: string;
|
||||||
@@ -157,6 +162,8 @@ export async function fetchChangePassword(body: {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 403 → wrong current password (in-app). Check BEFORE the 401 session-expiry branch.
|
||||||
|
if (res.status === 403) throw new Error('wrong-current');
|
||||||
if (res.type === 'opaqueredirect' || res.status === 401) throw new SessionExpiredError();
|
if (res.type === 'opaqueredirect' || res.status === 401) throw new SessionExpiredError();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const detail = (await res.json().catch(() => ({}))) as { code?: string };
|
const detail = (await res.json().catch(() => ({}))) as { code?: string };
|
||||||
|
|||||||
Reference in New Issue
Block a user