directus
279 строк · 7.3 Кб
1import { useEnv } from '@directus/env';2import { ErrorCode, InvalidPayloadError, isDirectusError } from '@directus/errors';3import type { Accountability } from '@directus/types';4import type { Request } from 'express';5import { Router } from 'express';6import {7createLDAPAuthRouter,8createLocalAuthRouter,9createOAuth2AuthRouter,10createOpenIDAuthRouter,11createSAMLAuthRouter,12} from '../auth/drivers/index.js';13import { DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../constants.js';14import { useLogger } from '../logger.js';15import { respond } from '../middleware/respond.js';16import { AuthenticationService } from '../services/authentication.js';17import { UsersService } from '../services/users.js';18import type { AuthenticationMode } from '../types/auth.js';19import asyncHandler from '../utils/async-handler.js';20import { getAuthProviders } from '../utils/get-auth-providers.js';21import { getIPFromReq } from '../utils/get-ip-from-req.js';22import { getSecret } from '../utils/get-secret.js';23import isDirectusJWT from '../utils/is-directus-jwt.js';24import { verifyAccessJWT } from '../utils/jwt.js';25
26const router = Router();27const env = useEnv();28const logger = useLogger();29
30const authProviders = getAuthProviders();31
32for (const authProvider of authProviders) {33let authRouter: Router | undefined;34
35switch (authProvider.driver) {36case 'local':37authRouter = createLocalAuthRouter(authProvider.name);38break;39
40case 'oauth2':41authRouter = createOAuth2AuthRouter(authProvider.name);42break;43
44case 'openid':45authRouter = createOpenIDAuthRouter(authProvider.name);46break;47
48case 'ldap':49authRouter = createLDAPAuthRouter(authProvider.name);50break;51
52case 'saml':53authRouter = createSAMLAuthRouter(authProvider.name);54break;55}56
57if (!authRouter) {58logger.warn(`Couldn't create login router for auth provider "${authProvider.name}"`);59continue;60}61
62router.use(`/login/${authProvider.name}`, authRouter);63}
64
65if (!env['AUTH_DISABLE_DEFAULT']) {66router.use('/login', createLocalAuthRouter(DEFAULT_AUTH_PROVIDER));67}
68
69function getCurrentMode(req: Request): AuthenticationMode {70if (req.body.mode) {71return req.body.mode as AuthenticationMode;72}73
74if (req.body.refresh_token) {75return 'json';76}77
78return 'cookie';79}
80
81function getCurrentRefreshToken(req: Request, mode: AuthenticationMode): string | undefined {82if (mode === 'json') {83return req.body.refresh_token;84}85
86if (mode === 'cookie') {87return req.cookies[env['REFRESH_TOKEN_COOKIE_NAME'] as string];88}89
90if (mode === 'session') {91const token = req.cookies[env['SESSION_COOKIE_NAME'] as string];92
93if (isDirectusJWT(token)) {94const payload = verifyAccessJWT(token, getSecret());95return payload.session;96}97}98
99return undefined;100}
101
102router.post(103'/refresh',104asyncHandler(async (req, res, next) => {105const accountability: Accountability = {106ip: getIPFromReq(req),107role: null,108};109
110const userAgent = req.get('user-agent')?.substring(0, 1024);111if (userAgent) accountability.userAgent = userAgent;112
113const origin = req.get('origin');114if (origin) accountability.origin = origin;115
116const authenticationService = new AuthenticationService({117accountability: accountability,118schema: req.schema,119});120
121const mode = getCurrentMode(req);122const currentRefreshToken = getCurrentRefreshToken(req, mode);123
124if (!currentRefreshToken) {125throw new InvalidPayloadError({126reason: `The refresh token is required in either the payload or cookie`,127});128}129
130const { accessToken, refreshToken, expires } = await authenticationService.refresh(currentRefreshToken, {131session: mode === 'session',132});133
134const payload = { expires } as { expires: number; access_token?: string; refresh_token?: string };135
136if (mode === 'json') {137payload.refresh_token = refreshToken;138payload.access_token = accessToken;139}140
141if (mode === 'cookie') {142res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, REFRESH_COOKIE_OPTIONS);143payload.access_token = accessToken;144}145
146if (mode === 'session') {147res.cookie(env['SESSION_COOKIE_NAME'] as string, accessToken, SESSION_COOKIE_OPTIONS);148}149
150res.locals['payload'] = { data: payload };151return next();152}),153respond,154);155
156router.post(157'/logout',158asyncHandler(async (req, res, next) => {159const accountability: Accountability = {160ip: getIPFromReq(req),161role: null,162};163
164const userAgent = req.get('user-agent')?.substring(0, 1024);165if (userAgent) accountability.userAgent = userAgent;166
167const origin = req.get('origin');168if (origin) accountability.origin = origin;169
170const authenticationService = new AuthenticationService({171accountability: accountability,172schema: req.schema,173});174
175const mode = getCurrentMode(req);176const currentRefreshToken = getCurrentRefreshToken(req, mode);177
178if (!currentRefreshToken) {179throw new InvalidPayloadError({180reason: `The refresh token is required in either the payload or cookie`,181});182}183
184await authenticationService.logout(currentRefreshToken);185
186if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME'] as string]) {187res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, REFRESH_COOKIE_OPTIONS);188}189
190if (req.cookies[env['SESSION_COOKIE_NAME'] as string]) {191res.clearCookie(env['SESSION_COOKIE_NAME'] as string, SESSION_COOKIE_OPTIONS);192}193
194return next();195}),196respond,197);198
199router.post(200'/password/request',201asyncHandler(async (req, _res, next) => {202if (typeof req.body.email !== 'string') {203throw new InvalidPayloadError({ reason: `"email" field is required` });204}205
206const accountability: Accountability = {207ip: getIPFromReq(req),208role: null,209};210
211const userAgent = req.get('user-agent')?.substring(0, 1024);212if (userAgent) accountability.userAgent = userAgent;213
214const origin = req.get('origin');215if (origin) accountability.origin = origin;216
217const service = new UsersService({ accountability, schema: req.schema });218
219try {220await service.requestPasswordReset(req.body.email, req.body.reset_url || null);221return next();222} catch (err: any) {223if (isDirectusError(err, ErrorCode.InvalidPayload)) {224throw err;225} else {226logger.warn(err, `[email] ${err}`);227return next();228}229}230}),231respond,232);233
234router.post(235'/password/reset',236asyncHandler(async (req, _res, next) => {237if (typeof req.body.token !== 'string') {238throw new InvalidPayloadError({ reason: `"token" field is required` });239}240
241if (typeof req.body.password !== 'string') {242throw new InvalidPayloadError({ reason: `"password" field is required` });243}244
245const accountability: Accountability = {246ip: getIPFromReq(req),247role: null,248};249
250const userAgent = req.get('user-agent')?.substring(0, 1024);251if (userAgent) accountability.userAgent = userAgent;252
253const origin = req.get('origin');254if (origin) accountability.origin = origin;255
256const service = new UsersService({ accountability, schema: req.schema });257await service.resetPassword(req.body.token, req.body.password);258return next();259}),260respond,261);262
263router.get(264'/',265asyncHandler(async (req, res, next) => {266const sessionOnly =267'sessionOnly' in req.query && (req.query['sessionOnly'] === '' || Boolean(req.query['sessionOnly']));268
269res.locals['payload'] = {270data: getAuthProviders({ sessionOnly }),271disableDefault: env['AUTH_DISABLE_DEFAULT'],272};273
274return next();275}),276respond,277);278
279export default router;280