mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
Merge pull request #2411 from logto-io/charles-log-4072-change-password-session-api
feat(core): add change password session api
This commit is contained in:
commit
610794b69e
4 changed files with 86 additions and 6 deletions
|
@ -197,7 +197,7 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(response).toHaveProperty('status', 400);
|
||||
expect(response).toHaveProperty('status', 422);
|
||||
expect(updateUserById).toBeCalledTimes(0);
|
||||
});
|
||||
it('should redirect when there was no old password', async () => {
|
||||
|
|
|
@ -46,9 +46,8 @@ export default function forgotPasswordRoutes<T extends AnonymousRouter>(
|
|||
const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId);
|
||||
|
||||
assertThat(
|
||||
!oldPasswordEncrypted ||
|
||||
(oldPasswordEncrypted && !(await argon2Verify({ password, hash: oldPasswordEncrypted }))),
|
||||
new RequestError({ code: 'user.same_password', status: 400 })
|
||||
!oldPasswordEncrypted || !(await argon2Verify({ password, hash: oldPasswordEncrypted })),
|
||||
new RequestError({ code: 'user.same_password', status: 422 })
|
||||
);
|
||||
|
||||
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { SignUpIdentifier } from '@logto/schemas';
|
|||
import { getUnixTime } from 'date-fns';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockUser, mockUserResponse } from '@/__mocks__';
|
||||
import { mockPasswordEncrypted, mockUser, mockUserResponse } from '@/__mocks__';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import profileRoutes, { profileRoute } from './profile';
|
||||
|
@ -18,6 +18,15 @@ const mockUpdateUserById = jest.fn(
|
|||
...data,
|
||||
})
|
||||
);
|
||||
const encryptUserPassword = jest.fn(async (password: string) => ({
|
||||
passwordEncrypted: password + '_user1',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
}));
|
||||
const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted);
|
||||
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({
|
||||
result: { login: { accountId: 'id', ts: getUnixTime(new Date()) - 60 } },
|
||||
}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
|
@ -27,6 +36,11 @@ jest.mock('oidc-provider', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
...jest.requireActual('@/lib/user'),
|
||||
encryptUserPassword: async (password: string) => encryptUserPassword(password),
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
...jest.requireActual('@/queries/user'),
|
||||
findUserById: async () => mockFindUserById(),
|
||||
|
@ -48,6 +62,10 @@ jest.mock('@/queries/sign-in-experience', () => ({
|
|||
findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()),
|
||||
}));
|
||||
|
||||
jest.mock('hash-wasm', () => ({
|
||||
argon2Verify: async (password: string) => mockArgon2Verify(password),
|
||||
}));
|
||||
|
||||
describe('session -> profileRoutes', () => {
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: profileRoutes,
|
||||
|
@ -90,4 +108,37 @@ describe('session -> profileRoutes', () => {
|
|||
expect(response.statusCode).toEqual(422);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/profile/password', () => {
|
||||
it('should update password with the new value', async () => {
|
||||
const response = await sessionRequest
|
||||
.post(`${profileRoute}/password`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
|
||||
expect(mockUpdateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
passwordEncrypted: 'a1b2c3_user1',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
})
|
||||
);
|
||||
expect(response.statusCode).toEqual(204);
|
||||
});
|
||||
|
||||
it('should throw if new password is identical to old password', async () => {
|
||||
jest.clearAllMocks();
|
||||
encryptUserPassword.mockImplementationOnce(async (password: string) => ({
|
||||
passwordEncrypted: password,
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
}));
|
||||
mockArgon2Verify.mockResolvedValueOnce(true);
|
||||
|
||||
const response = await sessionRequest
|
||||
.post(`${profileRoute}/password`)
|
||||
.send({ password: 'password' });
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(mockUpdateUserById).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { usernameRegEx } from '@logto/core-kit';
|
||||
import { passwordRegEx, usernameRegEx } from '@logto/core-kit';
|
||||
import { userInfoSelectFields } from '@logto/schemas';
|
||||
import { argon2Verify } from 'hash-wasm';
|
||||
import pick from 'lodash.pick';
|
||||
import type { Provider } from 'oidc-provider';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { checkSessionHealth } from '@/lib/session';
|
||||
import { encryptUserPassword } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import { findUserById, updateUserById } from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
@ -52,4 +54,32 @@ export default function profileRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${profileRoute}/password`,
|
||||
koaGuard({
|
||||
body: object({ password: string().regex(passwordRegEx) }),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
||||
|
||||
assertThat(userId, new RequestError('auth.unauthorized'));
|
||||
|
||||
const { password } = ctx.guard.body;
|
||||
const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId);
|
||||
|
||||
assertThat(
|
||||
!oldPasswordEncrypted || !(await argon2Verify({ password, hash: oldPasswordEncrypted })),
|
||||
new RequestError({ code: 'user.same_password', status: 422 })
|
||||
);
|
||||
|
||||
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
||||
|
||||
await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue