@wbce-d9/api 10.3.0 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +6 -4
- package/dist/auth/drivers/ldap.js +4 -2
- package/dist/auth/drivers/local.js +3 -2
- package/dist/auth/drivers/oauth2.js +11 -5
- package/dist/auth/drivers/openid.js +11 -5
- package/dist/auth/drivers/saml.js +14 -5
- package/dist/cli/utils/create-env/env-stub.liquid +15 -0
- package/dist/constants.d.ts +4 -1
- package/dist/constants.js +15 -2
- package/dist/controllers/auth.js +10 -8
- package/dist/controllers/shares.js +4 -2
- package/dist/database/run-ast.js +1 -1
- package/dist/env.js +10 -0
- package/dist/middleware/authenticate.js +15 -1
- package/dist/middleware/extract-token.d.ts +1 -0
- package/dist/middleware/extract-token.js +8 -0
- package/dist/services/authentication.js +20 -18
- package/dist/services/graphql/index.js +12 -17
- package/dist/services/payload.d.ts +2 -0
- package/dist/services/payload.js +9 -1
- package/dist/services/shares.js +12 -11
- package/dist/types/auth.d.ts +1 -0
- package/dist/utils/is-redirect-allowed-on-login.d.ts +4 -0
- package/dist/utils/is-redirect-allowed-on-login.js +42 -0
- package/dist/utils/is-redirect-allowed-on-login.test.d.ts +1 -0
- package/dist/utils/is-url-allowed.d.ts +1 -1
- package/dist/utils/is-url-allowed.js +5 -5
- package/dist/utils/is-url-allowed.test.d.ts +1 -0
- package/dist/utils/jwt.js +3 -3
- package/package.json +17 -17
package/dist/app.js
CHANGED
|
@@ -108,10 +108,12 @@ export default async function createApp() {
|
|
|
108
108
|
await emitter.emitInit('app.before', { app });
|
|
109
109
|
await emitter.emitInit('middlewares.before', { app });
|
|
110
110
|
app.use(expressLogger);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
if (env['X_POWERED_BY_ENABLED'] === true) {
|
|
112
|
+
app.use((_req, res, next) => {
|
|
113
|
+
res.setHeader('X-Powered-By', 'Directus');
|
|
114
|
+
next();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
115
117
|
if (env['CORS_ENABLED'] === true) {
|
|
116
118
|
app.use(cors);
|
|
117
119
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
3
|
import ldap from 'ldapjs';
|
|
4
|
-
import {
|
|
4
|
+
import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
|
|
5
5
|
import getDatabase from '../../database/index.js';
|
|
6
6
|
import emitter from '../../emitter.js';
|
|
7
|
+
import env from '../../env.js';
|
|
7
8
|
import { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
|
|
8
9
|
import { InvalidConfigException, InvalidCredentialsException, InvalidPayloadException, InvalidProviderException, ServiceUnavailableException, UnexpectedResponseException, } from '../../exceptions/index.js';
|
|
9
10
|
import logger from '../../logger.js';
|
|
@@ -314,7 +315,8 @@ export function createLDAPAuthRouter(provider) {
|
|
|
314
315
|
payload['data']['refresh_token'] = refreshToken;
|
|
315
316
|
}
|
|
316
317
|
if (mode === 'cookie') {
|
|
317
|
-
res
|
|
318
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
319
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
318
320
|
}
|
|
319
321
|
res.locals['payload'] = payload;
|
|
320
322
|
return next();
|
|
@@ -2,7 +2,7 @@ import argon2 from 'argon2';
|
|
|
2
2
|
import { Router } from 'express';
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import { performance } from 'perf_hooks';
|
|
5
|
-
import {
|
|
5
|
+
import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
|
|
6
6
|
import env from '../../env.js';
|
|
7
7
|
import { InvalidCredentialsException, InvalidPayloadException } from '../../exceptions/index.js';
|
|
8
8
|
import { respond } from '../../middleware/respond.js';
|
|
@@ -74,7 +74,8 @@ export function createLocalAuthRouter(provider) {
|
|
|
74
74
|
payload['data']['refresh_token'] = refreshToken;
|
|
75
75
|
}
|
|
76
76
|
if (mode === 'cookie') {
|
|
77
|
-
res
|
|
77
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
78
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
78
79
|
}
|
|
79
80
|
res.locals['payload'] = payload;
|
|
80
81
|
return next();
|
|
@@ -2,11 +2,11 @@ import { BaseException } from '@wbce-d9/exceptions';
|
|
|
2
2
|
import express, { Router } from 'express';
|
|
3
3
|
import flatten from 'flat';
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
|
-
import {
|
|
5
|
+
import { Issuer, generators } from 'openid-client';
|
|
6
6
|
import { getAuthProvider } from '../../auth.js';
|
|
7
|
-
import {
|
|
7
|
+
import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
|
|
8
8
|
import env from '../../env.js';
|
|
9
|
-
import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
|
|
9
|
+
import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException, InvalidPayloadException, } from '../../exceptions/index.js';
|
|
10
10
|
import logger from '../../logger.js';
|
|
11
11
|
import { respond } from '../../middleware/respond.js';
|
|
12
12
|
import { AuthenticationService } from '../../services/authentication.js';
|
|
@@ -16,6 +16,7 @@ import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
|
16
16
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
17
17
|
import { Url } from '../../utils/url.js';
|
|
18
18
|
import { BaseOAuthDriver } from './baseoauth.js';
|
|
19
|
+
import { isRedirectAllowedOnLogin } from '../../utils/is-redirect-allowed-on-login.js';
|
|
19
20
|
export class OAuth2AuthDriver extends BaseOAuthDriver {
|
|
20
21
|
client;
|
|
21
22
|
redirectUrl;
|
|
@@ -118,7 +119,11 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
118
119
|
const provider = getAuthProvider(providerName);
|
|
119
120
|
const codeVerifier = provider.generateCodeVerifier();
|
|
120
121
|
const prompt = !!req.query['prompt'];
|
|
121
|
-
const
|
|
122
|
+
const redirect = req.query['redirect'];
|
|
123
|
+
if (isRedirectAllowedOnLogin(redirect, providerName) === false) {
|
|
124
|
+
throw new InvalidPayloadException(`URL "${redirect}" can't be used to redirect after login`);
|
|
125
|
+
}
|
|
126
|
+
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'], {
|
|
122
127
|
expiresIn: '5m',
|
|
123
128
|
issuer: 'directus',
|
|
124
129
|
});
|
|
@@ -186,7 +191,8 @@ export function createOAuth2AuthRouter(providerName) {
|
|
|
186
191
|
}
|
|
187
192
|
const { accessToken, refreshToken, expires } = authResponse;
|
|
188
193
|
if (redirect) {
|
|
189
|
-
res
|
|
194
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
195
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
190
196
|
return res.redirect(redirect);
|
|
191
197
|
}
|
|
192
198
|
res.locals['payload'] = {
|
|
@@ -2,11 +2,11 @@ import { BaseException } from '@wbce-d9/exceptions';
|
|
|
2
2
|
import express, { Router } from 'express';
|
|
3
3
|
import flatten from 'flat';
|
|
4
4
|
import jwt from 'jsonwebtoken';
|
|
5
|
-
import {
|
|
5
|
+
import { Issuer, generators } from 'openid-client';
|
|
6
6
|
import { getAuthProvider } from '../../auth.js';
|
|
7
|
-
import {
|
|
7
|
+
import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS } from '../../constants.js';
|
|
8
8
|
import env from '../../env.js';
|
|
9
|
-
import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException } from '../../exceptions/index.js';
|
|
9
|
+
import { InvalidConfigException, InvalidCredentialsException, InvalidTokenException, InvalidPayloadException, } from '../../exceptions/index.js';
|
|
10
10
|
import logger from '../../logger.js';
|
|
11
11
|
import { respond } from '../../middleware/respond.js';
|
|
12
12
|
import { AuthenticationService } from '../../services/authentication.js';
|
|
@@ -15,6 +15,7 @@ import asyncHandler from '../../utils/async-handler.js';
|
|
|
15
15
|
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
16
16
|
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
|
|
17
17
|
import { Url } from '../../utils/url.js';
|
|
18
|
+
import { isRedirectAllowedOnLogin } from '../../utils/is-redirect-allowed-on-login.js';
|
|
18
19
|
import { BaseOAuthDriver } from './baseoauth.js';
|
|
19
20
|
export class OpenIDAuthDriver extends BaseOAuthDriver {
|
|
20
21
|
client;
|
|
@@ -138,7 +139,11 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
138
139
|
const provider = getAuthProvider(providerName);
|
|
139
140
|
const codeVerifier = provider.generateCodeVerifier();
|
|
140
141
|
const prompt = !!req.query['prompt'];
|
|
141
|
-
const
|
|
142
|
+
const redirect = req.query['redirect'];
|
|
143
|
+
if (isRedirectAllowedOnLogin(redirect, providerName) === false) {
|
|
144
|
+
throw new InvalidPayloadException(`URL "${redirect}" can't be used to redirect after login`);
|
|
145
|
+
}
|
|
146
|
+
const token = jwt.sign({ verifier: codeVerifier, redirect, prompt }, env['SECRET'], {
|
|
142
147
|
expiresIn: '5m',
|
|
143
148
|
issuer: 'directus',
|
|
144
149
|
});
|
|
@@ -211,7 +216,8 @@ export function createOpenIDAuthRouter(providerName) {
|
|
|
211
216
|
}
|
|
212
217
|
const { accessToken, refreshToken, expires } = authResponse;
|
|
213
218
|
if (redirect) {
|
|
214
|
-
res
|
|
219
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
220
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
215
221
|
return res.redirect(redirect);
|
|
216
222
|
}
|
|
217
223
|
res.locals['payload'] = {
|
|
@@ -3,18 +3,19 @@ import { BaseException } from '@wbce-d9/exceptions';
|
|
|
3
3
|
import express, { Router } from 'express';
|
|
4
4
|
import * as samlify from 'samlify';
|
|
5
5
|
import { getAuthProvider } from '../../auth.js';
|
|
6
|
-
import {
|
|
6
|
+
import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../../constants.js';
|
|
7
7
|
import getDatabase from '../../database/index.js';
|
|
8
8
|
import emitter from '../../emitter.js';
|
|
9
9
|
import env from '../../env.js';
|
|
10
10
|
import { RecordNotUniqueException } from '../../exceptions/database/record-not-unique.js';
|
|
11
|
-
import { InvalidCredentialsException, InvalidProviderException } from '../../exceptions/index.js';
|
|
11
|
+
import { InvalidCredentialsException, InvalidProviderException, InvalidPayloadException, } from '../../exceptions/index.js';
|
|
12
12
|
import logger from '../../logger.js';
|
|
13
13
|
import { respond } from '../../middleware/respond.js';
|
|
14
14
|
import { AuthenticationService } from '../../services/authentication.js';
|
|
15
15
|
import { UsersService } from '../../services/users.js';
|
|
16
16
|
import asyncHandler from '../../utils/async-handler.js';
|
|
17
17
|
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
|
|
18
|
+
import { isRedirectAllowedOnLogin } from '../../utils/is-redirect-allowed-on-login.js';
|
|
18
19
|
import { LocalAuthDriver } from './local.js';
|
|
19
20
|
// Register the samlify schema validator
|
|
20
21
|
samlify.setSchemaValidator(validator);
|
|
@@ -89,7 +90,11 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
89
90
|
const { context: url } = sp.createLoginRequest(idp, 'redirect');
|
|
90
91
|
const parsedUrl = new URL(url);
|
|
91
92
|
if (req.query['redirect']) {
|
|
92
|
-
|
|
93
|
+
const redirect = req.query['redirect'];
|
|
94
|
+
if (isRedirectAllowedOnLogin(redirect, providerName) === false) {
|
|
95
|
+
throw new InvalidPayloadException(`URL "${redirect}" can't be used to redirect after login`);
|
|
96
|
+
}
|
|
97
|
+
parsedUrl.searchParams.append('RelayState', redirect);
|
|
93
98
|
}
|
|
94
99
|
return res.redirect(parsedUrl.toString());
|
|
95
100
|
}));
|
|
@@ -97,11 +102,14 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
97
102
|
const { sp, idp } = getAuthProvider(providerName);
|
|
98
103
|
const { context } = sp.createLogoutRequest(idp, 'redirect', req.body);
|
|
99
104
|
const authService = new AuthenticationService({ accountability: req.accountability, schema: req.schema });
|
|
105
|
+
if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
|
|
106
|
+
res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
|
|
107
|
+
}
|
|
100
108
|
if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
|
|
101
109
|
const currentRefreshToken = req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
|
|
102
110
|
if (currentRefreshToken) {
|
|
103
111
|
await authService.logout(currentRefreshToken);
|
|
104
|
-
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'],
|
|
112
|
+
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
return res.redirect(context);
|
|
@@ -121,7 +129,8 @@ export function createSAMLAuthRouter(providerName) {
|
|
|
121
129
|
},
|
|
122
130
|
};
|
|
123
131
|
if (relayState) {
|
|
124
|
-
res
|
|
132
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
133
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
125
134
|
return res.redirect(relayState);
|
|
126
135
|
}
|
|
127
136
|
return next();
|
|
@@ -199,6 +199,18 @@ STORAGE_LOCAL_ROOT="./uploads"
|
|
|
199
199
|
# The duration that the access token is valid ["15m"]
|
|
200
200
|
ACCESS_TOKEN_TTL="15m"
|
|
201
201
|
|
|
202
|
+
# Whether or not to set the secure attribute for the access token cookie [false]
|
|
203
|
+
ACCESS_TOKEN_COOKIE_SECURE=false
|
|
204
|
+
|
|
205
|
+
# Value of sameSite for the access token cookie ["lax"]
|
|
206
|
+
ACCESS_TOKEN_COOKIE_SAME_SITE="lax"
|
|
207
|
+
|
|
208
|
+
# Name of the access token cookie ["directus_refresh_token"]
|
|
209
|
+
ACCESS_TOKEN_COOKIE_NAME="directus_session_token"
|
|
210
|
+
|
|
211
|
+
# Which domain to use for the access token cookie. Useful for development mode.
|
|
212
|
+
# ACCESS_TOKEN_COOKIE_DOMAIN
|
|
213
|
+
|
|
202
214
|
# The duration that the refresh token is valid, and also how long users stay logged-in to the App ["7d"]
|
|
203
215
|
REFRESH_TOKEN_TTL="7d"
|
|
204
216
|
|
|
@@ -218,6 +230,9 @@ REFRESH_TOKEN_COOKIE_NAME="directus_refresh_token"
|
|
|
218
230
|
# and it should be greater than the time taken for a login request with an invalid password [500]
|
|
219
231
|
# LOGIN_STALL_TIME=500
|
|
220
232
|
|
|
233
|
+
# Whether or not to enable the X-Powered-By Header [true]
|
|
234
|
+
X_POWERED_BY_ENABLED=false
|
|
235
|
+
|
|
221
236
|
# Whether or not to enable the CORS headers [false]
|
|
222
237
|
CORS_ENABLED=true
|
|
223
238
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -8,8 +8,11 @@ export declare const DEFAULT_AUTH_PROVIDER = "default";
|
|
|
8
8
|
export declare const COLUMN_TRANSFORMS: string[];
|
|
9
9
|
export declare const GENERATE_SPECIAL: string[];
|
|
10
10
|
export declare const UUID_REGEX = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const REFRESH_COOKIE_OPTIONS: CookieOptions;
|
|
12
|
+
export declare const REFRESH_COOKIE_CLEAR_OPTIONS: CookieOptions;
|
|
12
13
|
export declare const GET_SET_HEADER: (cookieValue: string) => string;
|
|
14
|
+
export declare const ACCESS_COOKIE_OPTIONS: CookieOptions;
|
|
15
|
+
export declare const ACCESS_COOKIE_CLEAR_OPTIONS: CookieOptions;
|
|
13
16
|
export declare const OAS_REQUIRED_SCHEMAS: string[];
|
|
14
17
|
/** Formats from which transformation is supported */
|
|
15
18
|
export declare const SUPPORTED_IMAGE_TRANSFORM_FORMATS: string[];
|
package/dist/constants.js
CHANGED
|
@@ -48,19 +48,32 @@ export const DEFAULT_AUTH_PROVIDER = 'default';
|
|
|
48
48
|
export const COLUMN_TRANSFORMS = ['year', 'month', 'day', 'weekday', 'hour', 'minute', 'second'];
|
|
49
49
|
export const GENERATE_SPECIAL = ['uuid', 'date-created', 'role-created', 'user-created'];
|
|
50
50
|
export const UUID_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
51
|
-
export const
|
|
51
|
+
export const REFRESH_COOKIE_OPTIONS = {
|
|
52
52
|
httpOnly: true,
|
|
53
53
|
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
|
|
54
54
|
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
|
|
55
55
|
secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
|
|
56
56
|
sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
|
|
57
|
+
path: '/auth',
|
|
57
58
|
};
|
|
59
|
+
const { maxAge: refreshCookieMaxAge, expires: refreshCookieExpires, ...refresh_cookie_clear_options } = REFRESH_COOKIE_OPTIONS;
|
|
60
|
+
export const REFRESH_COOKIE_CLEAR_OPTIONS = refresh_cookie_clear_options;
|
|
58
61
|
export const GET_SET_HEADER = (cookieValue) => {
|
|
59
62
|
const domainHeader = env['REFRESH_TOKEN_COOKIE_DOMAIN'] ? ` Domain=${env['REFRESH_TOKEN_COOKIE_DOMAIN']};` : '';
|
|
60
63
|
const partitionedHeader = env['REFRESH_TOKEN_COOKIE_PARTITIONED'] == true ? ' Partitioned;' : '';
|
|
61
|
-
const cookieHeaderValue = `${env['REFRESH_TOKEN_COOKIE_NAME']}=${cookieValue}; HttpOnly; Max-Age=${getMilliseconds(env['REFRESH_TOKEN_TTL'])}; Secure=${env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false}; SameSite=${env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict'}; Path
|
|
64
|
+
const cookieHeaderValue = `${env['REFRESH_TOKEN_COOKIE_NAME']}=${cookieValue}; HttpOnly; Max-Age=${getMilliseconds(env['REFRESH_TOKEN_TTL'])}; Secure=${env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false}; SameSite=${env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict'}; Path=/auth;${domainHeader}${partitionedHeader}`;
|
|
62
65
|
return cookieHeaderValue;
|
|
63
66
|
};
|
|
67
|
+
export const ACCESS_COOKIE_OPTIONS = {
|
|
68
|
+
httpOnly: true,
|
|
69
|
+
domain: env['ACCESS_TOKEN_COOKIE_DOMAIN'],
|
|
70
|
+
maxAge: getMilliseconds(env['ACCESS_TOKEN_TTL']),
|
|
71
|
+
secure: env['ACCESS_TOKEN_COOKIE_SECURE'] ?? false,
|
|
72
|
+
sameSite: env['ACCESS_TOKEN_COOKIE_SAME_SITE'] || 'strict',
|
|
73
|
+
path: '/',
|
|
74
|
+
};
|
|
75
|
+
const { maxAge: accessCookieMaxAge, expires: accessCookieExpires, ...access_cookie_clear_options } = ACCESS_COOKIE_OPTIONS;
|
|
76
|
+
export const ACCESS_COOKIE_CLEAR_OPTIONS = access_cookie_clear_options;
|
|
64
77
|
export const OAS_REQUIRED_SCHEMAS = ['Diff', 'Schema', 'Query', 'x-metadata'];
|
|
65
78
|
/** Formats from which transformation is supported */
|
|
66
79
|
export const SUPPORTED_IMAGE_TRANSFORM_FORMATS = ['image/jpeg', 'image/png', 'image/webp', 'image/tiff', 'image/avif'];
|
package/dist/controllers/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { createLDAPAuthRouter, createLocalAuthRouter, createOAuth2AuthRouter, createOpenIDAuthRouter, createSAMLAuthRouter, } from '../auth/drivers/index.js';
|
|
3
|
-
import { DEFAULT_AUTH_PROVIDER,
|
|
3
|
+
import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../constants.js';
|
|
4
4
|
import env from '../env.js';
|
|
5
5
|
import { InvalidPayloadException } from '../exceptions/index.js';
|
|
6
6
|
import logger from '../logger.js';
|
|
@@ -55,6 +55,9 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
|
|
|
55
55
|
accountability: accountability,
|
|
56
56
|
schema: req.schema,
|
|
57
57
|
});
|
|
58
|
+
if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
|
|
59
|
+
res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
|
|
60
|
+
}
|
|
58
61
|
const currentRefreshToken = req.body.refresh_token || req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
|
|
59
62
|
if (!currentRefreshToken) {
|
|
60
63
|
throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
|
|
@@ -68,7 +71,8 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
|
|
|
68
71
|
payload['data']['refresh_token'] = refreshToken;
|
|
69
72
|
}
|
|
70
73
|
if (mode === 'cookie') {
|
|
71
|
-
res
|
|
74
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
75
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
72
76
|
}
|
|
73
77
|
res.locals['payload'] = payload;
|
|
74
78
|
return next();
|
|
@@ -93,13 +97,11 @@ router.post('/logout', asyncHandler(async (req, res, next) => {
|
|
|
93
97
|
throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
|
|
94
98
|
}
|
|
95
99
|
await authenticationService.logout(currentRefreshToken);
|
|
100
|
+
if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
|
|
101
|
+
res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
|
|
102
|
+
}
|
|
96
103
|
if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
|
|
97
|
-
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'],
|
|
98
|
-
httpOnly: true,
|
|
99
|
-
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
|
|
100
|
-
secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
|
|
101
|
-
sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
|
|
102
|
-
});
|
|
104
|
+
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
|
|
103
105
|
}
|
|
104
106
|
return next();
|
|
105
107
|
}), respond);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
|
-
import {
|
|
3
|
+
import { ACCESS_COOKIE_OPTIONS, REFRESH_COOKIE_OPTIONS, UUID_REGEX } from '../constants.js';
|
|
4
|
+
import env from '../env.js';
|
|
4
5
|
import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
|
|
5
6
|
import { respond } from '../middleware/respond.js';
|
|
6
7
|
import useCollection from '../middleware/use-collection.js';
|
|
@@ -24,7 +25,8 @@ router.post('/auth', asyncHandler(async (req, res, next) => {
|
|
|
24
25
|
throw new InvalidPayloadException(error.message);
|
|
25
26
|
}
|
|
26
27
|
const { accessToken, refreshToken, expires } = await service.login(req.body);
|
|
27
|
-
res
|
|
28
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
29
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
28
30
|
res.locals['payload'] = { data: { access_token: accessToken, expires } };
|
|
29
31
|
return next();
|
|
30
32
|
}), respond);
|
package/dist/database/run-ast.js
CHANGED
|
@@ -35,7 +35,7 @@ export default async function runAST(originalAST, schema, options) {
|
|
|
35
35
|
return null;
|
|
36
36
|
// Run the items through the special transforms
|
|
37
37
|
const payloadService = new PayloadService(collection, { knex, schema });
|
|
38
|
-
let items = await payloadService.processValues('read', rawItems);
|
|
38
|
+
let items = await payloadService.processValues('read', rawItems, query.alias ?? {});
|
|
39
39
|
if (!items || (Array.isArray(items) && items.length === 0))
|
|
40
40
|
return items;
|
|
41
41
|
// Apply the `_in` filters to the nested collection batches
|
package/dist/env.js
CHANGED
|
@@ -34,6 +34,10 @@ const allowedEnvironmentVars = [
|
|
|
34
34
|
'KEY',
|
|
35
35
|
'SECRET',
|
|
36
36
|
'ACCESS_TOKEN_TTL',
|
|
37
|
+
'ACCESS_TOKEN_COOKIE_DOMAIN',
|
|
38
|
+
'ACCESS_TOKEN_COOKIE_SECURE',
|
|
39
|
+
'ACCESS_TOKEN_COOKIE_SAME_SITE',
|
|
40
|
+
'ACCESS_TOKEN_COOKIE_NAME',
|
|
37
41
|
'REFRESH_TOKEN_TTL',
|
|
38
42
|
'REFRESH_TOKEN_COOKIE_DOMAIN',
|
|
39
43
|
'REFRESH_TOKEN_COOKIE_SECURE',
|
|
@@ -50,6 +54,7 @@ const allowedEnvironmentVars = [
|
|
|
50
54
|
'HSTS_.+',
|
|
51
55
|
// hashing
|
|
52
56
|
'HASH_.+',
|
|
57
|
+
'X_POWERED_BY_ENABLED',
|
|
53
58
|
// cors
|
|
54
59
|
'CORS_ENABLED',
|
|
55
60
|
'CORS_ORIGIN',
|
|
@@ -143,6 +148,7 @@ const allowedEnvironmentVars = [
|
|
|
143
148
|
'AUTH_.+_GROUP_SCOPE',
|
|
144
149
|
'AUTH_.+_IDP.+',
|
|
145
150
|
'AUTH_.+_SP.+',
|
|
151
|
+
'AUTH_.+_REDIRECT_ALLOW_LIST',
|
|
146
152
|
// extensions
|
|
147
153
|
'PACKAGE_FILE_LOCATION',
|
|
148
154
|
'EXTENSIONS_PATH',
|
|
@@ -211,6 +217,9 @@ const defaults = {
|
|
|
211
217
|
RATE_LIMITER_GLOBAL_DURATION: 1,
|
|
212
218
|
RATE_LIMITER_GLOBAL_STORE: 'memory',
|
|
213
219
|
ACCESS_TOKEN_TTL: '15m',
|
|
220
|
+
ACCESS_TOKEN_COOKIE_SECURE: false,
|
|
221
|
+
ACCESS_TOKEN_COOKIE_SAME_SITE: 'lax',
|
|
222
|
+
ACCESS_TOKEN_COOKIE_NAME: 'directus_access_token',
|
|
214
223
|
REFRESH_TOKEN_TTL: '7d',
|
|
215
224
|
REFRESH_TOKEN_COOKIE_SECURE: false,
|
|
216
225
|
REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
|
|
@@ -218,6 +227,7 @@ const defaults = {
|
|
|
218
227
|
LOGIN_STALL_TIME: 500,
|
|
219
228
|
SERVER_SHUTDOWN_TIMEOUT: 1000,
|
|
220
229
|
ROOT_REDIRECT: './admin',
|
|
230
|
+
X_POWERED_BY_ENABLED: true,
|
|
221
231
|
CORS_ENABLED: false,
|
|
222
232
|
CORS_ORIGIN: false,
|
|
223
233
|
CORS_METHODS: 'GET,POST,PATCH,DELETE',
|
|
@@ -2,7 +2,7 @@ import { isEqual } from 'lodash-es';
|
|
|
2
2
|
import getDatabase from '../database/index.js';
|
|
3
3
|
import emitter from '../emitter.js';
|
|
4
4
|
import env from '../env.js';
|
|
5
|
-
import { InvalidCredentialsException } from '../exceptions/index.js';
|
|
5
|
+
import { InvalidCredentialsException, TokenExpiredException } from '../exceptions/index.js';
|
|
6
6
|
import asyncHandler from '../utils/async-handler.js';
|
|
7
7
|
import { getIPFromReq } from '../utils/get-ip-from-req.js';
|
|
8
8
|
import isDirectusJWT from '../utils/is-directus-jwt.js';
|
|
@@ -40,6 +40,20 @@ export const handler = async (req, _res, next) => {
|
|
|
40
40
|
if (req.token) {
|
|
41
41
|
if (isDirectusJWT(req.token)) {
|
|
42
42
|
const payload = verifyAccessJWT(req.token, env['SECRET']);
|
|
43
|
+
// Try finding the user with a valid refresh token stored in the access token
|
|
44
|
+
const user = await database
|
|
45
|
+
.select({
|
|
46
|
+
session_expires: 's.expires',
|
|
47
|
+
user_id: 'u.id',
|
|
48
|
+
})
|
|
49
|
+
.from('directus_sessions AS s')
|
|
50
|
+
.leftJoin('directus_users AS u', 's.user', 'u.id')
|
|
51
|
+
.where('s.token', payload.refresh_token)
|
|
52
|
+
.andWhere('s.expires', '>=', new Date())
|
|
53
|
+
.first();
|
|
54
|
+
if (!user) {
|
|
55
|
+
throw new TokenExpiredException();
|
|
56
|
+
}
|
|
43
57
|
req.accountability.role = payload.role;
|
|
44
58
|
req.accountability.admin = payload.admin_access === true || payload.admin_access == 1;
|
|
45
59
|
req.accountability.app = payload.app_access === true || payload.app_access == 1;
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Authorization: Bearer
|
|
5
5
|
* access_token query parameter
|
|
6
|
+
* Access token cookie
|
|
6
7
|
*
|
|
7
8
|
* and store in req.token
|
|
8
9
|
*/
|
|
10
|
+
import env from '../env.js';
|
|
9
11
|
const extractToken = (req, _res, next) => {
|
|
10
12
|
let token = null;
|
|
11
13
|
if (req.query && req.query['access_token']) {
|
|
@@ -17,6 +19,12 @@ const extractToken = (req, _res, next) => {
|
|
|
17
19
|
token = parts[1];
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if there is an access token stored in the cookies
|
|
24
|
+
*/
|
|
25
|
+
if (req?.cookies?.[env['ACCESS_TOKEN_COOKIE_NAME']]) {
|
|
26
|
+
token = req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']];
|
|
27
|
+
}
|
|
20
28
|
/**
|
|
21
29
|
* @TODO
|
|
22
30
|
* Look into RFC6750 compliance:
|
|
@@ -128,11 +128,22 @@ export class AuthenticationService {
|
|
|
128
128
|
throw new InvalidOTPException(`"otp" is invalid`);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
const refreshToken = nanoid(64);
|
|
132
|
+
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
|
133
|
+
await this.knex('directus_sessions').insert({
|
|
134
|
+
token: refreshToken,
|
|
135
|
+
user: user.id,
|
|
136
|
+
expires: refreshTokenExpiration,
|
|
137
|
+
ip: this.accountability?.ip,
|
|
138
|
+
user_agent: this.accountability?.userAgent,
|
|
139
|
+
origin: this.accountability?.origin,
|
|
140
|
+
});
|
|
131
141
|
const tokenPayload = {
|
|
132
142
|
id: user.id,
|
|
133
143
|
role: user.role,
|
|
134
144
|
app_access: user.app_access,
|
|
135
145
|
admin_access: user.admin_access,
|
|
146
|
+
refresh_token: refreshToken,
|
|
136
147
|
};
|
|
137
148
|
const customClaims = await emitter.emitFilter('auth.jwt', tokenPayload, {
|
|
138
149
|
status: 'pending',
|
|
@@ -148,16 +159,6 @@ export class AuthenticationService {
|
|
|
148
159
|
expiresIn: env['ACCESS_TOKEN_TTL'],
|
|
149
160
|
issuer: 'directus',
|
|
150
161
|
});
|
|
151
|
-
const refreshToken = nanoid(64);
|
|
152
|
-
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
|
153
|
-
await this.knex('directus_sessions').insert({
|
|
154
|
-
token: refreshToken,
|
|
155
|
-
user: user.id,
|
|
156
|
-
expires: refreshTokenExpiration,
|
|
157
|
-
ip: this.accountability?.ip,
|
|
158
|
-
user_agent: this.accountability?.userAgent,
|
|
159
|
-
origin: this.accountability?.origin,
|
|
160
|
-
});
|
|
161
162
|
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
|
162
163
|
if (this.accountability) {
|
|
163
164
|
await this.activityService.createOne({
|
|
@@ -260,11 +261,20 @@ export class AuthenticationService {
|
|
|
260
261
|
admin_access: record.role_admin_access,
|
|
261
262
|
});
|
|
262
263
|
}
|
|
264
|
+
const newRefreshToken = nanoid(64);
|
|
265
|
+
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
|
266
|
+
await this.knex('directus_sessions')
|
|
267
|
+
.update({
|
|
268
|
+
token: newRefreshToken,
|
|
269
|
+
expires: refreshTokenExpiration,
|
|
270
|
+
})
|
|
271
|
+
.where({ token: refreshToken });
|
|
263
272
|
const tokenPayload = {
|
|
264
273
|
id: record.user_id,
|
|
265
274
|
role: record.role_id,
|
|
266
275
|
app_access: record.role_app_access,
|
|
267
276
|
admin_access: record.role_admin_access,
|
|
277
|
+
refresh_token: newRefreshToken,
|
|
268
278
|
};
|
|
269
279
|
if (record.share_id) {
|
|
270
280
|
tokenPayload.share = record.share_id;
|
|
@@ -291,14 +301,6 @@ export class AuthenticationService {
|
|
|
291
301
|
expiresIn: env['ACCESS_TOKEN_TTL'],
|
|
292
302
|
issuer: 'directus',
|
|
293
303
|
});
|
|
294
|
-
const newRefreshToken = nanoid(64);
|
|
295
|
-
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
|
296
|
-
await this.knex('directus_sessions')
|
|
297
|
-
.update({
|
|
298
|
-
token: newRefreshToken,
|
|
299
|
-
expires: refreshTokenExpiration,
|
|
300
|
-
})
|
|
301
|
-
.where({ token: refreshToken });
|
|
302
304
|
if (record.user_id) {
|
|
303
305
|
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: record.user_id });
|
|
304
306
|
}
|
|
@@ -5,14 +5,13 @@ import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID,
|
|
|
5
5
|
import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
|
|
6
6
|
import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
|
|
7
7
|
import { clearSystemCache, getCache } from '../../cache.js';
|
|
8
|
-
import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
|
|
8
|
+
import { ACCESS_COOKIE_CLEAR_OPTIONS, ACCESS_COOKIE_OPTIONS, DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_CLEAR_OPTIONS, REFRESH_COOKIE_OPTIONS, } from '../../constants.js';
|
|
9
9
|
import getDatabase from '../../database/index.js';
|
|
10
10
|
import env from '../../env.js';
|
|
11
11
|
import { ForbiddenException, GraphQLValidationException, InvalidPayloadException } from '../../exceptions/index.js';
|
|
12
12
|
import { getExtensionManager } from '../../extensions.js';
|
|
13
13
|
import { generateHash } from '../../utils/generate-hash.js';
|
|
14
14
|
import { getGraphQLType } from '../../utils/get-graphql-type.js';
|
|
15
|
-
import { getMilliseconds } from '../../utils/get-milliseconds.js';
|
|
16
15
|
import { reduceSchema } from '../../utils/reduce-schema.js';
|
|
17
16
|
import { sanitizeQuery } from '../../utils/sanitize-query.js';
|
|
18
17
|
import { validateQuery } from '../../utils/validate-query.js';
|
|
@@ -1696,13 +1695,8 @@ export class GraphQLService {
|
|
|
1696
1695
|
});
|
|
1697
1696
|
const result = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, args?.otp);
|
|
1698
1697
|
if (args['mode'] === 'cookie') {
|
|
1699
|
-
res?.cookie(env['
|
|
1700
|
-
|
|
1701
|
-
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
|
|
1702
|
-
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
|
|
1703
|
-
secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
|
|
1704
|
-
sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
|
|
1705
|
-
});
|
|
1698
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], result['accessToken'], ACCESS_COOKIE_OPTIONS);
|
|
1699
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], REFRESH_COOKIE_OPTIONS);
|
|
1706
1700
|
}
|
|
1707
1701
|
return {
|
|
1708
1702
|
access_token: result['accessToken'],
|
|
@@ -1737,13 +1731,8 @@ export class GraphQLService {
|
|
|
1737
1731
|
}
|
|
1738
1732
|
const result = await authenticationService.refresh(currentRefreshToken);
|
|
1739
1733
|
if (args['mode'] === 'cookie') {
|
|
1740
|
-
res?.cookie(env['
|
|
1741
|
-
|
|
1742
|
-
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
|
|
1743
|
-
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
|
|
1744
|
-
secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
|
|
1745
|
-
sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
|
|
1746
|
-
});
|
|
1734
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], result['accessToken'], ACCESS_COOKIE_OPTIONS);
|
|
1735
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], REFRESH_COOKIE_OPTIONS);
|
|
1747
1736
|
}
|
|
1748
1737
|
return {
|
|
1749
1738
|
access_token: result['accessToken'],
|
|
@@ -1757,7 +1746,7 @@ export class GraphQLService {
|
|
|
1757
1746
|
args: {
|
|
1758
1747
|
refresh_token: GraphQLString,
|
|
1759
1748
|
},
|
|
1760
|
-
resolve: async (_, args, { req }) => {
|
|
1749
|
+
resolve: async (_, args, { req, res }) => {
|
|
1761
1750
|
const accountability = { role: null };
|
|
1762
1751
|
if (req?.ip)
|
|
1763
1752
|
accountability.ip = req.ip;
|
|
@@ -1776,6 +1765,12 @@ export class GraphQLService {
|
|
|
1776
1765
|
throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
|
|
1777
1766
|
}
|
|
1778
1767
|
await authenticationService.logout(currentRefreshToken);
|
|
1768
|
+
if (req?.cookies?.[env['ACCESS_TOKEN_COOKIE_NAME']]) {
|
|
1769
|
+
res?.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_CLEAR_OPTIONS);
|
|
1770
|
+
}
|
|
1771
|
+
if (req?.cookies?.[env['REFRESH_TOKEN_COOKIE_NAME']]) {
|
|
1772
|
+
res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_CLEAR_OPTIONS);
|
|
1773
|
+
}
|
|
1779
1774
|
return true;
|
|
1780
1775
|
},
|
|
1781
1776
|
},
|
|
@@ -27,6 +27,8 @@ export declare class PayloadService {
|
|
|
27
27
|
transformers: Transformers;
|
|
28
28
|
processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
|
29
29
|
processValues(action: Action, payload: Partial<Item>): Promise<Partial<Item>>;
|
|
30
|
+
processValues(action: Action, payloads: Partial<Item>[], aliasMap: Record<string, string>): Promise<Partial<Item>[]>;
|
|
31
|
+
processValues(action: Action, payload: Partial<Item>, aliasMap: Record<string, string>): Promise<Partial<Item>>;
|
|
30
32
|
processAggregates(payload: Partial<Item>[]): void;
|
|
31
33
|
processField(field: SchemaOverview['collections'][string]['fields'][string], payload: Partial<Item>, action: Action, accountability: Accountability | null): Promise<any>;
|
|
32
34
|
/**
|
package/dist/services/payload.js
CHANGED
|
@@ -121,12 +121,20 @@ export class PayloadService {
|
|
|
121
121
|
return value;
|
|
122
122
|
},
|
|
123
123
|
};
|
|
124
|
-
async processValues(action, payload) {
|
|
124
|
+
async processValues(action, payload, aliasMap = {}) {
|
|
125
125
|
const processedPayload = toArray(payload);
|
|
126
126
|
if (processedPayload.length === 0)
|
|
127
127
|
return [];
|
|
128
128
|
const fieldsInPayload = Object.keys(processedPayload[0]);
|
|
129
129
|
let specialFieldsInCollection = Object.entries(this.schema.collections[this.collection].fields).filter(([_name, field]) => field.special && field.special.length > 0);
|
|
130
|
+
const aliasEntries = Object.entries(aliasMap);
|
|
131
|
+
for (const [name, field] of specialFieldsInCollection) {
|
|
132
|
+
for (const [aliasName, fieldName] of aliasEntries) {
|
|
133
|
+
if (fieldName === name) {
|
|
134
|
+
specialFieldsInCollection.push([aliasName, { ...field, field: aliasName }]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
130
138
|
if (action === 'read') {
|
|
131
139
|
specialFieldsInCollection = specialFieldsInCollection.filter(([name]) => {
|
|
132
140
|
return fieldsInPayload.includes(name);
|
package/dist/services/shares.js
CHANGED
|
@@ -59,6 +59,17 @@ export class SharesService extends ItemsService {
|
|
|
59
59
|
await this.knex('directus_shares')
|
|
60
60
|
.update({ times_used: record.share_times_used + 1 })
|
|
61
61
|
.where('id', record.share_id);
|
|
62
|
+
const refreshToken = nanoid(64);
|
|
63
|
+
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
|
64
|
+
await this.knex('directus_sessions').insert({
|
|
65
|
+
token: refreshToken,
|
|
66
|
+
expires: refreshTokenExpiration,
|
|
67
|
+
ip: this.accountability?.ip,
|
|
68
|
+
user_agent: this.accountability?.userAgent,
|
|
69
|
+
origin: this.accountability?.origin,
|
|
70
|
+
share: record.share_id,
|
|
71
|
+
});
|
|
72
|
+
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
|
62
73
|
const tokenPayload = {
|
|
63
74
|
app_access: false,
|
|
64
75
|
admin_access: false,
|
|
@@ -68,22 +79,12 @@ export class SharesService extends ItemsService {
|
|
|
68
79
|
item: record.share_item,
|
|
69
80
|
collection: record.share_collection,
|
|
70
81
|
},
|
|
82
|
+
refresh_token: refreshToken,
|
|
71
83
|
};
|
|
72
84
|
const accessToken = jwt.sign(tokenPayload, env['SECRET'], {
|
|
73
85
|
expiresIn: env['ACCESS_TOKEN_TTL'],
|
|
74
86
|
issuer: 'directus',
|
|
75
87
|
});
|
|
76
|
-
const refreshToken = nanoid(64);
|
|
77
|
-
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
|
78
|
-
await this.knex('directus_sessions').insert({
|
|
79
|
-
token: refreshToken,
|
|
80
|
-
expires: refreshTokenExpiration,
|
|
81
|
-
ip: this.accountability?.ip,
|
|
82
|
-
user_agent: this.accountability?.userAgent,
|
|
83
|
-
origin: this.accountability?.origin,
|
|
84
|
-
share: record.share_id,
|
|
85
|
-
});
|
|
86
|
-
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
|
|
87
88
|
return {
|
|
88
89
|
accessToken,
|
|
89
90
|
refreshToken,
|
package/dist/types/auth.d.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { toArray } from '@wbce-d9/utils';
|
|
2
|
+
import env from '../env.js';
|
|
3
|
+
import isUrlAllowed from './is-url-allowed.js';
|
|
4
|
+
function isUrlValid(string) {
|
|
5
|
+
try {
|
|
6
|
+
new URL(string);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Checks if the defined redirect after successful SSO login is in the allow list
|
|
15
|
+
*/
|
|
16
|
+
export function isRedirectAllowedOnLogin(redirect, provider) {
|
|
17
|
+
if (!redirect)
|
|
18
|
+
return true; // empty redirect
|
|
19
|
+
if (typeof redirect !== 'string')
|
|
20
|
+
return false; // invalid type
|
|
21
|
+
const publicUrl = env['PUBLIC_URL'];
|
|
22
|
+
if (!isUrlValid(redirect)) {
|
|
23
|
+
if (!redirect.startsWith('//')) {
|
|
24
|
+
// should be a relative path like `/admin/test`
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// domain without protocol `//example.com/test`
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const { protocol: redirectProtocol, hostname: redirectDomain } = new URL(redirect);
|
|
31
|
+
const envKey = `AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`;
|
|
32
|
+
if (envKey in env) {
|
|
33
|
+
if (isUrlAllowed(redirect, [...toArray(env[envKey]), publicUrl]))
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (!isUrlValid(publicUrl)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// allow redirects to the defined PUBLIC_URL
|
|
40
|
+
const { protocol: publicProtocol, hostname: publicDomain } = new URL(publicUrl);
|
|
41
|
+
return `${redirectProtocol}//${redirectDomain}` === `${publicProtocol}//${publicDomain}`;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,7 +2,7 @@ import { toArray } from '@wbce-d9/utils';
|
|
|
2
2
|
import logger from '../logger.js';
|
|
3
3
|
import { URL } from 'url';
|
|
4
4
|
/**
|
|
5
|
-
* Check if url matches allow list either exactly or by
|
|
5
|
+
* Check if url matches allow list either exactly or by origin+pathname
|
|
6
6
|
*/
|
|
7
7
|
export default function isUrlAllowed(url, allowList) {
|
|
8
8
|
const urlAllowList = toArray(allowList);
|
|
@@ -11,8 +11,8 @@ export default function isUrlAllowed(url, allowList) {
|
|
|
11
11
|
const parsedWhitelist = urlAllowList
|
|
12
12
|
.map((allowedURL) => {
|
|
13
13
|
try {
|
|
14
|
-
const {
|
|
15
|
-
return
|
|
14
|
+
const { origin, pathname } = new URL(allowedURL);
|
|
15
|
+
return origin + pathname;
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
18
|
logger.warn(`Invalid URL used "${url}"`);
|
|
@@ -21,8 +21,8 @@ export default function isUrlAllowed(url, allowList) {
|
|
|
21
21
|
})
|
|
22
22
|
.filter((f) => f);
|
|
23
23
|
try {
|
|
24
|
-
const {
|
|
25
|
-
return parsedWhitelist.includes(
|
|
24
|
+
const { origin, pathname } = new URL(url);
|
|
25
|
+
return parsedWhitelist.includes(origin + pathname);
|
|
26
26
|
}
|
|
27
27
|
catch {
|
|
28
28
|
return false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils/jwt.js
CHANGED
|
@@ -21,9 +21,9 @@ export function verifyJWT(token, secret) {
|
|
|
21
21
|
return payload;
|
|
22
22
|
}
|
|
23
23
|
export function verifyAccessJWT(token, secret) {
|
|
24
|
-
const { id, role, app_access, admin_access, share, share_scope } = verifyJWT(token, secret);
|
|
25
|
-
if (role === undefined || app_access === undefined || admin_access === undefined) {
|
|
24
|
+
const { id, role, app_access, admin_access, share, share_scope, refresh_token } = verifyJWT(token, secret);
|
|
25
|
+
if (role === undefined || app_access === undefined || admin_access === undefined || refresh_token === undefined) {
|
|
26
26
|
throw new InvalidTokenException('Invalid token payload.');
|
|
27
27
|
}
|
|
28
|
-
return { id, role, app_access, admin_access, share, share_scope };
|
|
28
|
+
return { id, role, app_access, admin_access, share, share_scope, refresh_token };
|
|
29
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wbce-d9/api",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.4.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@wbce-d9/format-title": "10.0.0",
|
|
73
73
|
"argon2": "0.30.3",
|
|
74
74
|
"async": "3.2.4",
|
|
75
|
-
"axios": "1.
|
|
75
|
+
"axios": "^1.7.2",
|
|
76
76
|
"busboy": "1.6.0",
|
|
77
77
|
"bytes": "3.1.2",
|
|
78
78
|
"camelcase": "7.0.1",
|
|
@@ -143,20 +143,20 @@
|
|
|
143
143
|
"uuid-validate": "0.0.3",
|
|
144
144
|
"vm2": "^3.9.19",
|
|
145
145
|
"wellknown": "0.5.0",
|
|
146
|
-
"@wbce-d9/app": "9.
|
|
147
|
-
"@wbce-d9/
|
|
148
|
-
"@wbce-d9/
|
|
149
|
-
"@wbce-d9/extensions-sdk": "9.26.
|
|
150
|
-
"@wbce-d9/schema": "9.26.
|
|
151
|
-
"@wbce-d9/storage
|
|
152
|
-
"@wbce-d9/specs": "9.26.
|
|
153
|
-
"@wbce-d9/storage": "9.26.
|
|
154
|
-
"@wbce-d9/storage-driver-cloudinary": "9.26.
|
|
155
|
-
"@wbce-d9/storage-driver-gcs": "9.26.
|
|
156
|
-
"@wbce-d9/
|
|
157
|
-
"@wbce-d9/
|
|
158
|
-
"@wbce-d9/
|
|
159
|
-
"@wbce-d9/
|
|
146
|
+
"@wbce-d9/app": "9.30.0",
|
|
147
|
+
"@wbce-d9/exceptions": "9.26.4",
|
|
148
|
+
"@wbce-d9/constants": "9.27.0",
|
|
149
|
+
"@wbce-d9/extensions-sdk": "9.26.7",
|
|
150
|
+
"@wbce-d9/schema": "9.26.4",
|
|
151
|
+
"@wbce-d9/storage": "9.26.4",
|
|
152
|
+
"@wbce-d9/specs": "9.26.4",
|
|
153
|
+
"@wbce-d9/storage-driver-azure": "9.26.7",
|
|
154
|
+
"@wbce-d9/storage-driver-cloudinary": "9.26.7",
|
|
155
|
+
"@wbce-d9/storage-driver-gcs": "9.26.7",
|
|
156
|
+
"@wbce-d9/update-check": "9.26.4",
|
|
157
|
+
"@wbce-d9/storage-driver-local": "9.26.7",
|
|
158
|
+
"@wbce-d9/storage-driver-s3": "9.26.9",
|
|
159
|
+
"@wbce-d9/utils": "9.29.0"
|
|
160
160
|
},
|
|
161
161
|
"devDependencies": {
|
|
162
162
|
"@ngneat/falso": "6.4.0",
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
"supertest": "6.3.3",
|
|
205
205
|
"typescript": "5.0.4",
|
|
206
206
|
"vitest": "0.30.1",
|
|
207
|
-
"@wbce-d9/types": "9.
|
|
207
|
+
"@wbce-d9/types": "9.27.0"
|
|
208
208
|
},
|
|
209
209
|
"optionalDependencies": {
|
|
210
210
|
"@keyv/redis": "2.5.7",
|