0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): reauth by password

This commit is contained in:
wangsijie 2022-09-14 16:55:49 +08:00
parent e405ef7bb8
commit 393bf36f23
No known key found for this signature in database
GPG key ID: C72642FE24F7D42B
5 changed files with 123 additions and 12 deletions

View file

@ -7,6 +7,8 @@ import RequestError from '@/errors/RequestError';
import { findUserById, updateUserById } from '@/queries/user';
import { maskUserInfo } from '@/utils/format';
import { updateLastSignInAt } from './user';
export const assignInteractionResults = async (
ctx: Context,
provider: Provider,
@ -73,6 +75,32 @@ export const checkProtectedAccess = async (
}
};
export const reAuthenticateSession = async (ctx: Context, provider: Provider) => {
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
if (!result?.login?.accountId) {
throw new RequestError('auth.unauthorized');
}
await updateLastSignInAt(result.login.accountId);
const ts = dayjs().unix();
await provider.interactionResult(
ctx.req,
ctx.res,
{
...result,
login: {
...result.login,
ts,
},
},
{ mergeWithLastSubmission: true }
);
return { ts };
};
export const saveUserFirstConsentedAppId = async (userId: string, applicationId: string) => {
const { applicationId: firstConsentedAppId } = await findUserById(userId);

View file

@ -6,7 +6,11 @@ import { mockUser } from '@/__mocks__';
import RequestError from '@/errors/RequestError';
import { createRequester } from '@/utils/test-utils';
import usernamePasswordRoutes, { registerRoute, signInRoute } from './username-password';
import usernamePasswordRoutes, {
registerRoute,
signInRoute,
reAuthRoute,
} from './username-password';
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const findUserById = jest.fn(async (): Promise<User> => mockUser);
@ -29,7 +33,7 @@ jest.mock('@/queries/user', () => ({
jest.mock('@/lib/user', () => ({
async findUserByUsernameAndPassword(username: string, password: string) {
if (username !== 'username' && username !== 'admin') {
if (username !== 'foo' && username !== 'admin') {
throw new RequestError('session.invalid_credentials');
}
@ -103,7 +107,7 @@ describe('sessionRoutes', () => {
it('assign result and redirect', async () => {
interactionDetails.mockResolvedValueOnce({ params: {} });
const response = await sessionRequest.post(signInRoute).send({
username: 'username',
username: 'foo',
password: 'password',
});
expect(response.statusCode).toEqual(200);
@ -129,7 +133,7 @@ describe('sessionRoutes', () => {
it('throw if user found but wrong password', async () => {
interactionDetails.mockResolvedValueOnce({ params: {} });
const response = await sessionRequest.post(signInRoute).send({
username: 'username',
username: 'foo',
password: '_password',
});
expect(response.statusCode).toEqual(400);
@ -140,7 +144,7 @@ describe('sessionRoutes', () => {
params: { client_id: adminConsoleApplicationId },
});
const response = await sessionRequest.post(signInRoute).send({
username: 'username',
username: 'foo',
password: 'password',
});
@ -167,11 +171,11 @@ describe('sessionRoutes', () => {
const response = await sessionRequest
.post(registerRoute)
.send({ username: 'username', password: 'password' });
.send({ username: 'foo', password: 'password' });
expect(insertUser).toHaveBeenCalledWith(
expect.objectContaining({
id: 'user1',
username: 'username',
username: 'foo',
passwordEncrypted: 'password_user1',
passwordEncryptionMethod: 'Argon2i',
roleNames: [],
@ -194,7 +198,7 @@ describe('sessionRoutes', () => {
hasActiveUsers.mockResolvedValueOnce(false);
await sessionRequest.post(registerRoute).send({ username: 'username', password: 'password' });
await sessionRequest.post(registerRoute).send({ username: 'foo', password: 'password' });
expect(insertUser).toHaveBeenCalledWith(
expect.objectContaining({
@ -208,7 +212,7 @@ describe('sessionRoutes', () => {
params: { client_id: adminConsoleApplicationId },
});
await sessionRequest.post(registerRoute).send({ username: 'username', password: 'password' });
await sessionRequest.post(registerRoute).send({ username: 'foo', password: 'password' });
expect(insertUser).toHaveBeenCalledWith(
expect.objectContaining({
@ -232,4 +236,45 @@ describe('sessionRoutes', () => {
expect(response.statusCode).toEqual(422);
});
});
describe('POST /session/re-auth/username-password', () => {
it('should update login.ts', async () => {
interactionDetails.mockResolvedValue({
params: {},
result: { login: { accountId: 'foo', ts: 0 } },
});
const response = await sessionRequest.post(reAuthRoute).send({
password: 'password',
});
expect(response.statusCode).toEqual(200);
expect(response.body).toHaveProperty('ts');
expect(response.body.ts).toBeGreaterThan(0);
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
expect.objectContaining({ login: expect.objectContaining({ ts: expect.anything() }) }),
expect.anything()
);
});
it('should throw if the password is wrong', async () => {
interactionDetails.mockResolvedValue({
params: {},
result: { login: { accountId: 'foo', ts: 0 } },
});
const response = await sessionRequest.post(reAuthRoute).send({
password: '_password',
});
expect(response.statusCode).toEqual(400);
});
it('should throw if current session is not authenticated before', async () => {
interactionDetails.mockResolvedValue({ params: {} });
const response = await sessionRequest.post(reAuthRoute).send({
password: 'password',
});
expect(response.statusCode).toEqual(401);
});
});
});

View file

@ -5,7 +5,7 @@ import { Provider } from 'oidc-provider';
import { object, string } from 'zod';
import RequestError from '@/errors/RequestError';
import { assignInteractionResults } from '@/lib/session';
import { assignInteractionResults, reAuthenticateSession } from '@/lib/session';
import {
encryptUserPassword,
generateUserId,
@ -14,7 +14,7 @@ import {
insertUser,
} from '@/lib/user';
import koaGuard from '@/middleware/koa-guard';
import { hasUser, hasActiveUsers } from '@/queries/user';
import { hasUser, hasActiveUsers, findUserById } from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { AnonymousRouter } from '../types';
@ -22,6 +22,7 @@ import { getRoutePrefix } from './utils';
export const registerRoute = getRoutePrefix('register', 'username-password');
export const signInRoute = getRoutePrefix('sign-in', 'username-password');
export const reAuthRoute = getRoutePrefix('re-auth', 'username-password');
export default function usernamePasswordRoutes<T extends AnonymousRouter>(
router: T,
@ -109,4 +110,35 @@ export default function usernamePasswordRoutes<T extends AnonymousRouter>(
return next();
}
);
router.post(
reAuthRoute,
koaGuard({
body: object({
password: string().min(1),
}),
}),
async (ctx, next) => {
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
assertThat(
result?.login?.accountId,
new RequestError({
code: 'auth.unauthorized',
status: 401,
})
);
const user = await findUserById(result.login.accountId);
assertThat(user.username, 'session.invalid_sign_in_method');
const { password } = ctx.guard.body;
const type = 'ReAuthUsernamePassword';
ctx.log(type, { username: user.username, userId: user.id });
await findUserByUsernameAndPassword(user.username, password);
ctx.body = await reAuthenticateSession(ctx, provider);
return next();
}
);
}

View file

@ -1,7 +1,7 @@
import { Truthy } from '@silverhand/essentials';
export const getRoutePrefix = (
type: 'sign-in' | 'register',
type: 'sign-in' | 'register' | 're-auth',
method?: 'passwordless' | 'username-password' | 'social'
) => {
return ['session', type, method]

View file

@ -96,6 +96,11 @@ type SignInSocialLogPayload = SignInSocialBindLogPayload & {
redirectTo?: string;
};
type ReAuthUsernamePasswordLogPayload = ArbitraryLogPayload & {
userId?: string;
username?: string;
};
export enum TokenType {
AccessToken = 'AccessToken',
RefreshToken = 'RefreshToken',
@ -131,6 +136,7 @@ export type LogPayloads = {
SignInSms: SignInSmsLogPayload;
SignInSocialBind: SignInSocialBindLogPayload;
SignInSocial: SignInSocialLogPayload;
ReAuthUsernamePassword: ReAuthUsernamePasswordLogPayload;
CodeExchangeToken: ExchangeTokenLogPayload;
RefreshTokenExchangeToken: ExchangeTokenLogPayload;
RevokeToken: RevokeTokenLogPayload;