@wbce-d9/api 10.1.0 → 10.3.1
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/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 +12 -0
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +12 -3
- package/dist/controllers/auth.js +7 -8
- package/dist/controllers/shares.js +4 -2
- package/dist/env.js +8 -0
- package/dist/middleware/extract-token.d.ts +1 -0
- package/dist/middleware/extract-token.js +8 -0
- package/dist/services/graphql/index.js +12 -17
- 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/package.json +17 -17
|
@@ -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_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_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_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
|
|
package/dist/constants.d.ts
CHANGED
|
@@ -8,8 +8,9 @@ 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
12
|
export declare const GET_SET_HEADER: (cookieValue: string) => string;
|
|
13
|
+
export declare const ACCESS_COOKIE_OPTIONS: CookieOptions;
|
|
13
14
|
export declare const OAS_REQUIRED_SCHEMAS: string[];
|
|
14
15
|
/** Formats from which transformation is supported */
|
|
15
16
|
export declare const SUPPORTED_IMAGE_TRANSFORM_FORMATS: string[];
|
package/dist/constants.js
CHANGED
|
@@ -48,19 +48,28 @@ 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
|
};
|
|
58
59
|
export const GET_SET_HEADER = (cookieValue) => {
|
|
59
60
|
const domainHeader = env['REFRESH_TOKEN_COOKIE_DOMAIN'] ? ` Domain=${env['REFRESH_TOKEN_COOKIE_DOMAIN']};` : '';
|
|
60
|
-
const partitionedHeader = env['REFRESH_TOKEN_COOKIE_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
|
|
61
|
+
const partitionedHeader = env['REFRESH_TOKEN_COOKIE_PARTITIONED'] == true ? ' Partitioned;' : '';
|
|
62
|
+
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
63
|
return cookieHeaderValue;
|
|
63
64
|
};
|
|
65
|
+
export const ACCESS_COOKIE_OPTIONS = {
|
|
66
|
+
httpOnly: true,
|
|
67
|
+
domain: env['ACCESS_TOKEN_COOKIE_DOMAIN'],
|
|
68
|
+
maxAge: getMilliseconds(env['ACCESS_TOKEN_TTL']),
|
|
69
|
+
secure: env['ACCESS_TOKEN_COOKIE_SECURE'] ?? false,
|
|
70
|
+
sameSite: env['ACCESS_TOKEN_COOKIE_SAME_SITE'] || 'strict',
|
|
71
|
+
path: '/',
|
|
72
|
+
};
|
|
64
73
|
export const OAS_REQUIRED_SCHEMAS = ['Diff', 'Schema', 'Query', 'x-metadata'];
|
|
65
74
|
/** Formats from which transformation is supported */
|
|
66
75
|
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_OPTIONS, DEFAULT_AUTH_PROVIDER, 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';
|
|
@@ -68,7 +68,8 @@ router.post('/refresh', asyncHandler(async (req, res, next) => {
|
|
|
68
68
|
payload['data']['refresh_token'] = refreshToken;
|
|
69
69
|
}
|
|
70
70
|
if (mode === 'cookie') {
|
|
71
|
-
res
|
|
71
|
+
res?.cookie(env['ACCESS_TOKEN_COOKIE_NAME'], accessToken, ACCESS_COOKIE_OPTIONS);
|
|
72
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
72
73
|
}
|
|
73
74
|
res.locals['payload'] = payload;
|
|
74
75
|
return next();
|
|
@@ -93,13 +94,11 @@ router.post('/logout', asyncHandler(async (req, res, next) => {
|
|
|
93
94
|
throw new InvalidPayloadException(`"refresh_token" is required in either the JSON payload or Cookie`);
|
|
94
95
|
}
|
|
95
96
|
await authenticationService.logout(currentRefreshToken);
|
|
97
|
+
if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME']]) {
|
|
98
|
+
res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'], ACCESS_COOKIE_OPTIONS);
|
|
99
|
+
}
|
|
96
100
|
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
|
-
});
|
|
101
|
+
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
|
|
103
102
|
}
|
|
104
103
|
return next();
|
|
105
104
|
}), 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/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',
|
|
@@ -143,6 +147,7 @@ const allowedEnvironmentVars = [
|
|
|
143
147
|
'AUTH_.+_GROUP_SCOPE',
|
|
144
148
|
'AUTH_.+_IDP.+',
|
|
145
149
|
'AUTH_.+_SP.+',
|
|
150
|
+
'AUTH_.+_REDIRECT_ALLOW_LIST',
|
|
146
151
|
// extensions
|
|
147
152
|
'PACKAGE_FILE_LOCATION',
|
|
148
153
|
'EXTENSIONS_PATH',
|
|
@@ -211,6 +216,9 @@ const defaults = {
|
|
|
211
216
|
RATE_LIMITER_GLOBAL_DURATION: 1,
|
|
212
217
|
RATE_LIMITER_GLOBAL_STORE: 'memory',
|
|
213
218
|
ACCESS_TOKEN_TTL: '15m',
|
|
219
|
+
ACCESS_TOKEN_COOKIE_SECURE: false,
|
|
220
|
+
ACCESS_TOKEN_COOKIE_SAME_SITE: 'lax',
|
|
221
|
+
ACCESS_TOKEN_COOKIE_NAME: 'directus_access_token',
|
|
214
222
|
REFRESH_TOKEN_TTL: '7d',
|
|
215
223
|
REFRESH_TOKEN_COOKIE_SECURE: false,
|
|
216
224
|
REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
|
|
@@ -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:
|
|
@@ -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_OPTIONS, DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, 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_OPTIONS);
|
|
1770
|
+
}
|
|
1771
|
+
if (req?.cookies?.[env['REFRESH_TOKEN_COOKIE_NAME']]) {
|
|
1772
|
+
res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
|
|
1773
|
+
}
|
|
1779
1774
|
return true;
|
|
1780
1775
|
},
|
|
1781
1776
|
},
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wbce-d9/api",
|
|
3
|
-
"version": "10.1
|
|
3
|
+
"version": "10.3.1",
|
|
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.29.
|
|
147
|
-
"@wbce-d9/constants": "9.26.
|
|
148
|
-
"@wbce-d9/
|
|
149
|
-
"@wbce-d9/
|
|
150
|
-
"@wbce-d9/
|
|
151
|
-
"@wbce-d9/
|
|
152
|
-
"@wbce-d9/
|
|
153
|
-
"@wbce-d9/storage-driver-azure": "9.26.
|
|
154
|
-
"@wbce-d9/storage-driver-cloudinary": "9.26.
|
|
155
|
-
"@wbce-d9/storage-driver-
|
|
156
|
-
"@wbce-d9/storage-driver-
|
|
157
|
-
"@wbce-d9/
|
|
158
|
-
"@wbce-d9/
|
|
159
|
-
"@wbce-d9/
|
|
146
|
+
"@wbce-d9/app": "9.29.1",
|
|
147
|
+
"@wbce-d9/constants": "9.26.4",
|
|
148
|
+
"@wbce-d9/extensions-sdk": "9.26.6",
|
|
149
|
+
"@wbce-d9/specs": "9.26.4",
|
|
150
|
+
"@wbce-d9/exceptions": "9.26.4",
|
|
151
|
+
"@wbce-d9/storage": "9.26.4",
|
|
152
|
+
"@wbce-d9/schema": "9.26.4",
|
|
153
|
+
"@wbce-d9/storage-driver-azure": "9.26.6",
|
|
154
|
+
"@wbce-d9/storage-driver-cloudinary": "9.26.6",
|
|
155
|
+
"@wbce-d9/storage-driver-gcs": "9.26.6",
|
|
156
|
+
"@wbce-d9/storage-driver-local": "9.26.6",
|
|
157
|
+
"@wbce-d9/storage-driver-s3": "9.26.8",
|
|
158
|
+
"@wbce-d9/update-check": "9.26.4",
|
|
159
|
+
"@wbce-d9/utils": "9.28.1"
|
|
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.26.
|
|
207
|
+
"@wbce-d9/types": "9.26.4"
|
|
208
208
|
},
|
|
209
209
|
"optionalDependencies": {
|
|
210
210
|
"@keyv/redis": "2.5.7",
|