mirror of
https://github.com/directus/directus.git
synced 2026-01-23 00:08:46 -05:00
API hooks for event added for auth.login (#4255)
* Rotate JPG image on upload #4206 * fixes #3949 width/height generated for gif and tif * API hooks for event added for auth.login #4079 * updated doc for api hooks for new auth.login event * Style tweaks * Update docs * Tweak docs some more * Spelling error * Allow non-required flags and pass to hook Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -21,7 +21,7 @@ const loginSchema = Joi.object({
|
||||
password: Joi.string().required(),
|
||||
mode: Joi.string().valid('cookie', 'json'),
|
||||
otp: Joi.string(),
|
||||
});
|
||||
}).unknown();
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
@@ -40,19 +40,15 @@ router.post(
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
|
||||
const { email, password, otp } = req.body;
|
||||
|
||||
const mode = req.body.mode || 'json';
|
||||
|
||||
const ip = req.ip;
|
||||
const userAgent = req.get('user-agent');
|
||||
|
||||
const { accessToken, refreshToken, expires } = await authenticationService.authenticate({
|
||||
...req.body,
|
||||
ip,
|
||||
userAgent,
|
||||
email,
|
||||
password,
|
||||
otp,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
|
||||
@@ -4,11 +4,13 @@ import argon2 from 'argon2';
|
||||
import { nanoid } from 'nanoid';
|
||||
import ms from 'ms';
|
||||
import { InvalidCredentialsException, InvalidPayloadException, InvalidOTPException } from '../exceptions';
|
||||
import { Session, Accountability, AbstractServiceOptions, Action } from '../types';
|
||||
import { Session, Accountability, AbstractServiceOptions, Action, SchemaOverview } from '../types';
|
||||
import Knex from 'knex';
|
||||
import { ActivityService } from '../services/activity';
|
||||
import env from '../env';
|
||||
import { authenticator } from 'otplib';
|
||||
import emitter, { emitAsyncSafe } from '../emitter';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
type AuthenticateOptions = {
|
||||
email: string;
|
||||
@@ -16,17 +18,20 @@ type AuthenticateOptions = {
|
||||
ip?: string | null;
|
||||
userAgent?: string | null;
|
||||
otp?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export class AuthenticationService {
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
activityService: ActivityService;
|
||||
schema: SchemaOverview;
|
||||
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || database;
|
||||
this.accountability = options.accountability || null;
|
||||
this.activityService = new ActivityService({ knex: this.knex, schema: options.schema });
|
||||
this.schema = options.schema;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,28 +40,58 @@ export class AuthenticationService {
|
||||
* Password is optional to allow usage of this function within the SSO flow and extensions. Make sure
|
||||
* to handle password existence checks elsewhere
|
||||
*/
|
||||
async authenticate({ email, password, ip, userAgent, otp }: AuthenticateOptions) {
|
||||
async authenticate(options: AuthenticateOptions) {
|
||||
const { email, password, ip, userAgent, otp } = options;
|
||||
|
||||
const hookPayload = omit(options, 'password', 'otp');
|
||||
|
||||
const user = await database
|
||||
.select('id', 'password', 'role', 'tfa_secret', 'status')
|
||||
.from('directus_users')
|
||||
.where({ email })
|
||||
.first();
|
||||
|
||||
await emitter.emitAsync('auth.login.before', hookPayload, {
|
||||
event: 'auth.login.before',
|
||||
action: 'login',
|
||||
schema: this.schema,
|
||||
payload: hookPayload,
|
||||
accountability: this.accountability,
|
||||
status: 'pending',
|
||||
user: user?.id,
|
||||
});
|
||||
|
||||
const emitStatus = (status: 'fail' | 'success') => {
|
||||
emitAsyncSafe('auth.login', hookPayload, {
|
||||
event: 'auth.login',
|
||||
action: 'login',
|
||||
schema: this.schema,
|
||||
payload: hookPayload,
|
||||
accountability: this.accountability,
|
||||
status,
|
||||
user: user?.id,
|
||||
});
|
||||
};
|
||||
|
||||
if (!user || user.status !== 'active') {
|
||||
emitStatus('fail');
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
if (password !== undefined) {
|
||||
if (!user.password) {
|
||||
emitStatus('fail');
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
if ((await argon2.verify(user.password, password)) === false) {
|
||||
emitStatus('fail');
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
}
|
||||
|
||||
if (user.tfa_secret && !otp) {
|
||||
emitStatus('fail');
|
||||
throw new InvalidOTPException(`"otp" is required`);
|
||||
}
|
||||
|
||||
@@ -64,6 +99,7 @@ export class AuthenticationService {
|
||||
const otpValid = await this.verifyOTP(user.id, otp);
|
||||
|
||||
if (otpValid === false) {
|
||||
emitStatus('fail');
|
||||
throw new InvalidOTPException(`"otp" is invalid`);
|
||||
}
|
||||
}
|
||||
@@ -103,6 +139,8 @@ export class AuthenticationService {
|
||||
});
|
||||
}
|
||||
|
||||
emitStatus('success');
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
|
||||
@@ -72,31 +72,31 @@ module.exports = function registerHook({ exceptions }) {
|
||||
|
||||
### Event Format Options
|
||||
|
||||
| Scope | Actions | Before |
|
||||
| -------------------- | ---------------------------------- | -------- |
|
||||
| `server` | `start` and `stop` | Optional |
|
||||
| `init` | | Optional |
|
||||
| `routes.init` | `before` and `after` | No |
|
||||
| `routes.custom.init` | `before` and `after` | No |
|
||||
| `middlewares.init` | `before` and `after` | No |
|
||||
| `request` | `not_found` | No |
|
||||
| `response` | | No† |
|
||||
| `error` | | No |
|
||||
| `auth` | `success`†, `fail`† and `refresh`† | No |
|
||||
| `items` | `create`, `update` and `delete` | Optional |
|
||||
| `activity` | `create`, `update` and `delete` | Optional |
|
||||
| `collections` | `create`, `update` and `delete` | Optional |
|
||||
| `fields` | `create`, `update` and `delete` | Optional |
|
||||
| `files` | `create`, `update` and `delete` | Optional |
|
||||
| `folders` | `create`, `update` and `delete` | Optional |
|
||||
| `permissions` | `create`, `update` and `delete` | Optional |
|
||||
| `presets` | `create`, `update` and `delete` | Optional |
|
||||
| `relations` | `create`, `update` and `delete` | Optional |
|
||||
| `revisions` | `create`, `update` and `delete` | Optional |
|
||||
| `roles` | `create`, `update` and `delete` | Optional |
|
||||
| `settings` | `create`, `update` and `delete` | Optional |
|
||||
| `users` | `create`, `update` and `delete` | Optional |
|
||||
| `webhooks` | `create`, `update` and `delete` | Optional |
|
||||
| Scope | Actions | Before |
|
||||
| -------------------- | --------------------------------- | -------- |
|
||||
| `server` | `start` and `stop` | Optional |
|
||||
| `init` | | Optional |
|
||||
| `routes.init` | `before` and `after` | No |
|
||||
| `routes.custom.init` | `before` and `after` | No |
|
||||
| `middlewares.init` | `before` and `after` | No |
|
||||
| `request` | `not_found` | No |
|
||||
| `response` | | No† |
|
||||
| `error` | | No |
|
||||
| `auth` | `login`, `logout`† and `refresh`† | Optional |
|
||||
| `items` | `create`, `update` and `delete` | Optional |
|
||||
| `activity` | `create`, `update` and `delete` | Optional |
|
||||
| `collections` | `create`, `update` and `delete` | Optional |
|
||||
| `fields` | `create`, `update` and `delete` | Optional |
|
||||
| `files` | `create`, `update` and `delete` | Optional |
|
||||
| `folders` | `create`, `update` and `delete` | Optional |
|
||||
| `permissions` | `create`, `update` and `delete` | Optional |
|
||||
| `presets` | `create`, `update` and `delete` | Optional |
|
||||
| `relations` | `create`, `update` and `delete` | Optional |
|
||||
| `revisions` | `create`, `update` and `delete` | Optional |
|
||||
| `roles` | `create`, `update` and `delete` | Optional |
|
||||
| `settings` | `create`, `update` and `delete` | Optional |
|
||||
| `users` | `create`, `update` and `delete` | Optional |
|
||||
| `webhooks` | `create`, `update` and `delete` | Optional |
|
||||
|
||||
† Feature Coming Soon
|
||||
|
||||
@@ -123,14 +123,14 @@ and the value is the handler function itself.
|
||||
|
||||
The `registerHook` function receives a context parameter with the following properties:
|
||||
|
||||
- `services` — All API interal services
|
||||
- `services` — All API internal services
|
||||
- `exceptions` — API exception objects that can be used for throwing "proper" errors
|
||||
- `database` — Knex instance that is connected to the current database
|
||||
- `env` — Parsed environment variables
|
||||
|
||||
### Event Handler Function
|
||||
|
||||
The event handler function (eg: `'items.create': function()`) recieves a context parameter with the following
|
||||
The event handler function (eg: `'items.create': function()`) receives a context parameter with the following
|
||||
properties:
|
||||
|
||||
- `event` — Full event string
|
||||
@@ -139,6 +139,19 @@ properties:
|
||||
- `item` — Primary key(s) of the item(s) being modified
|
||||
- `action` — Action that is performed
|
||||
- `payload` — Payload of the request
|
||||
- `schema` - The current API schema in use
|
||||
|
||||
#### Auth
|
||||
|
||||
The `auth` hooks have the following context properties:
|
||||
|
||||
- `event` — Full event string
|
||||
- `accountability` — Information about the current user
|
||||
- `action` — Action that is performed
|
||||
- `payload` — Payload of the request
|
||||
- `schema` - The current API schema in use
|
||||
- `status` - One of `pending`, `success`, `fail`
|
||||
- `user` - ID of the user that tried logging in/has logged in
|
||||
|
||||
## 5. Restart the API
|
||||
|
||||
|
||||
Reference in New Issue
Block a user