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:
Pyll Gomez
2021-02-24 10:37:13 -05:00
committed by GitHub
parent e130e80f50
commit aeb4ec0472
3 changed files with 82 additions and 35 deletions

View File

@@ -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 = {

View File

@@ -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,

View File

@@ -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