api: pass user object to auth hooks (#5251)

* api: pass user object to auth hooks
The motivation for this is the ability to migrate from
bcrypt passwords to argon2 seamlessly.

Fixes #4718
Fixes #4335

* receive user from `auth.login.before` hook

* pass `otp` as well on `auth.login` hooks

* api: last registered hook should have priority
When registering multiple hooks for a single event,
the last hook should have priority on return value.

* Fix linter warning

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
José Varela
2021-05-14 17:33:51 +01:00
committed by GitHub
parent e8e295ef6a
commit 4d242ab5bf
3 changed files with 13 additions and 14 deletions

View File

@@ -15,8 +15,8 @@ import { RouteNotFoundException } from '../exceptions';
*/
const notFound: RequestHandler = async (req, res, next) => {
try {
const ret = await emitter.emitAsync('request.not_found', req, res);
if (ret.reduce((prev, current) => current || prev, false)) {
const hooksResult = await emitter.emitAsync('request.not_found', req, res);
if (hooksResult.reduce((prev, current) => current || prev, false)) {
return next();
}
next(new RouteNotFoundException(req.path));

View File

@@ -1,7 +1,6 @@
import argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import { Knex } from 'knex';
import { omit } from 'lodash';
import ms from 'ms';
import { nanoid } from 'nanoid';
import { authenticator } from 'otplib';
@@ -59,31 +58,33 @@ export class AuthenticationService {
const { email, password, ip, userAgent, otp } = options;
const hookPayload = omit(options, 'password', 'otp');
const user = await database
let user = await database
.select('id', 'password', 'role', 'tfa_secret', 'status')
.from('directus_users')
.whereRaw('LOWER(??) = ?', ['email', email.toLowerCase()])
.first();
await emitter.emitAsync('auth.login.before', hookPayload, {
const updatedUser = await emitter.emitAsync('auth.login.before', options, {
event: 'auth.login.before',
action: 'login',
schema: this.schema,
payload: hookPayload,
payload: options,
accountability: this.accountability,
status: 'pending',
user: user?.id,
database: this.knex,
});
if (updatedUser) {
user = updatedUser;
}
const emitStatus = (status: 'fail' | 'success') => {
emitAsyncSafe('auth.login', hookPayload, {
emitAsyncSafe('auth.login', options, {
event: 'auth.login',
action: 'login',
schema: this.schema,
payload: hookPayload,
payload: options,
accountability: this.accountability,
status,
user: user?.id,

View File

@@ -114,8 +114,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
// The events are fired last-to-first based on when they were created. By reversing the
// output array of results, we ensure that the augmentations are applied in
// "chronological" order
const payloadAfterHooks =
hooksResult.length > 0 ? hooksResult.reverse().reduce((val, acc) => merge(acc, val)) : payload;
const payloadAfterHooks = hooksResult.length > 0 ? hooksResult.reduce((val, acc) => merge(acc, val)) : payload;
const payloadWithPresets = this.accountability
? await authorizationService.validatePayload('create', this.collection, payloadAfterHooks)
@@ -402,8 +401,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
// The events are fired last-to-first based on when they were created. By reversing the
// output array of results, we ensure that the augmentations are applied in
// "chronological" order
const payloadAfterHooks =
hooksResult.length > 0 ? hooksResult.reverse().reduce((val, acc) => merge(acc, val)) : payload;
const payloadAfterHooks = hooksResult.length > 0 ? hooksResult.reduce((val, acc) => merge(acc, val)) : payload;
if (this.accountability) {
await authorizationService.checkAccess('update', this.collection, keys);