0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

feat(core): add management API to verify user password (#3680)

* feat(core): add management API to verify user password

* chore: add changeset
This commit is contained in:
Charles Zhao 2023-04-09 22:01:35 +08:00 committed by GitHub
parent e07355c42e
commit 7af8e9c9b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/core": patch
---
Add new management API `/users/:userId/password/verify` to help verify user password, which would be helpful when building custom profile or sign-in pages

View file

@ -9,6 +9,7 @@ import {
mockUserListResponse,
mockUserResponse,
} from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import { MockTenant, type Partial2 } from '#src/test-utils/tenant.js';
@ -85,12 +86,16 @@ const { revokeInstanceByUserId } = mockedQueries.oidcModelInstances;
const { hasUser, findUserById, updateUserById, deleteUserIdentity, deleteUserById } =
mockedQueries.users;
const { encryptUserPassword } = await mockEsmWithActual('#src/libraries/user.js', () => ({
encryptUserPassword: jest.fn(() => ({
passwordEncrypted: 'password',
passwordEncryptionMethod: 'Argon2i',
})),
}));
const { encryptUserPassword, verifyUserPassword } = await mockEsmWithActual(
'#src/libraries/user.js',
() => ({
encryptUserPassword: jest.fn(() => ({
passwordEncrypted: 'password',
passwordEncryptionMethod: 'Argon2i',
})),
verifyUserPassword: jest.fn(),
})
);
const usersLibraries = {
generateUserId: jest.fn(async () => 'fooId'),
@ -347,6 +352,29 @@ describe('adminUserRoutes', () => {
expect(updateUserById).not.toHaveBeenCalled();
});
it('POST /users/:userId/password/verify', async () => {
const mockedUserId = 'foo';
const password = '1234asd$';
const response = await userRequest
.post(`/users/${mockedUserId}/password/verify`)
.send({ password });
expect(findUserById).toHaveBeenCalledWith(mockedUserId);
expect(verifyUserPassword).toHaveBeenCalledWith(mockUser, password);
expect(response.status).toEqual(204);
});
it('POST /users/:userId/password/verify should throw if password is invalid', async () => {
const password = 'invalidPassword';
verifyUserPassword.mockImplementationOnce(async () => {
throw new RequestError({ code: 'session.invalid_credentials', status: 422 });
});
await expect(
userRequest.post(`/users/foo/password/verify`).send({ password })
).resolves.toHaveProperty('status', 422);
expect(verifyUserPassword).toHaveBeenCalledWith(mockUser, password);
});
it('PATCH /users/:userId/is-suspended', async () => {
const mockedUserId = 'foo';
const response = await userRequest

View file

@ -4,7 +4,7 @@ import { conditional, has, pick, tryThat } from '@silverhand/essentials';
import { boolean, literal, object, string } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import { encryptUserPassword } from '#src/libraries/user.js';
import { encryptUserPassword, verifyUserPassword } from '#src/libraries/user.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import assertThat from '#src/utils/assert-that.js';
@ -234,6 +234,27 @@ export default function adminUserRoutes<T extends AuthedRouter>(
}
);
router.post(
'/users/:userId/password/verify',
koaGuard({
params: object({ userId: string() }),
body: object({ password: string() }),
}),
async (ctx, next) => {
const {
params: { userId },
body: { password },
} = ctx.guard;
const user = await findUserById(userId);
await verifyUserPassword(user, password);
ctx.status = 204;
return next();
}
);
router.patch(
'/users/:userId/is-suspended',
koaGuard({