directus 9.2.0 → 9.2.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.
@@ -94,7 +94,10 @@ class LDAPAuthDriver extends auth_1.AuthDriver {
94
94
  const { userDn, userAttribute, userScope } = this.config;
95
95
  return new Promise((resolve, reject) => {
96
96
  // Search for the user in LDAP by attribute
97
- this.bindClient.search(userDn, { filter: `(${userAttribute !== null && userAttribute !== void 0 ? userAttribute : 'cn'}=${identifier})`, scope: userScope !== null && userScope !== void 0 ? userScope : 'one' }, (err, res) => {
97
+ this.bindClient.search(userDn, {
98
+ filter: new ldapjs_1.EqualityFilter({ attribute: userAttribute !== null && userAttribute !== void 0 ? userAttribute : 'cn', value: identifier }),
99
+ scope: userScope !== null && userScope !== void 0 ? userScope : 'one',
100
+ }, (err, res) => {
98
101
  if (err) {
99
102
  reject(handleError(err));
100
103
  return;
@@ -151,7 +154,7 @@ class LDAPAuthDriver extends auth_1.AuthDriver {
151
154
  // Search for the user info in LDAP by group attribute
152
155
  this.bindClient.search(groupDn, {
153
156
  attributes: ['cn'],
154
- filter: `(${groupAttribute !== null && groupAttribute !== void 0 ? groupAttribute : 'member'}=${userDn})`,
157
+ filter: new ldapjs_1.EqualityFilter({ attribute: groupAttribute !== null && groupAttribute !== void 0 ? groupAttribute : 'member', value: userDn }),
155
158
  scope: groupScope !== null && groupScope !== void 0 ? groupScope : 'one',
156
159
  }, (err, res) => {
157
160
  if (err) {
@@ -10,7 +10,7 @@ export declare class OAuth2AuthDriver extends LocalAuthDriver {
10
10
  config: Record<string, any>;
11
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
12
12
  generateCodeVerifier(): string;
13
- generateAuthUrl(codeVerifier: string): string;
13
+ generateAuthUrl(codeVerifier: string, prompt?: boolean): string;
14
14
  private fetchUserId;
15
15
  getUserID(payload: Record<string, any>): Promise<string>;
16
16
  login(user: User): Promise<SessionData>;
@@ -32,7 +32,8 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
32
32
  authorization_endpoint: authorizeUrl,
33
33
  token_endpoint: accessUrl,
34
34
  userinfo_endpoint: profileUrl,
35
- issuer: additionalConfig.provider,
35
+ // Required for openid providers (openid flow should be preferred!)
36
+ issuer: additionalConfig.issuerUrl,
36
37
  });
37
38
  this.client = new issuer.Client({
38
39
  client_id: clientId,
@@ -44,17 +45,20 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
44
45
  generateCodeVerifier() {
45
46
  return openid_client_1.generators.codeVerifier();
46
47
  }
47
- generateAuthUrl(codeVerifier) {
48
+ generateAuthUrl(codeVerifier, prompt = false) {
48
49
  var _a;
49
50
  try {
50
51
  const codeChallenge = openid_client_1.generators.codeChallenge(codeVerifier);
52
+ const paramsConfig = typeof this.config.params === 'object' ? this.config.params : {};
51
53
  return this.client.authorizationUrl({
52
54
  scope: (_a = this.config.scope) !== null && _a !== void 0 ? _a : 'email',
55
+ access_type: 'offline',
56
+ prompt: prompt ? 'consent' : undefined,
57
+ ...paramsConfig,
53
58
  code_challenge: codeChallenge,
54
59
  code_challenge_method: 'S256',
55
60
  // Some providers require state even with PKCE
56
61
  state: codeChallenge,
57
- access_type: 'offline',
58
62
  });
59
63
  }
60
64
  catch (e) {
@@ -72,6 +76,7 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
72
76
  async getUserID(payload) {
73
77
  var _a;
74
78
  if (!payload.code || !payload.codeVerifier) {
79
+ logger_1.default.trace('[OAuth2] No code or codeVerifier in payload');
75
80
  throw new exceptions_1.InvalidCredentialsException();
76
81
  }
77
82
  let tokenSet;
@@ -112,6 +117,7 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
112
117
  }
113
118
  // Is public registration allowed?
114
119
  if (!allowPublicRegistration) {
120
+ logger_1.default.trace(`[OAuth2] User doesn't exist, and public registration not allowed for provider "${this.config.provider}"`);
115
121
  throw new exceptions_1.InvalidCredentialsException();
116
122
  }
117
123
  await this.usersService.createOne({
@@ -136,35 +142,44 @@ class OAuth2AuthDriver extends local_1.LocalAuthDriver {
136
142
  logger_1.default.warn(`Session data isn't valid JSON: ${authData}`);
137
143
  }
138
144
  }
139
- if (!(authData === null || authData === void 0 ? void 0 : authData.refreshToken)) {
140
- return sessionData;
141
- }
142
- try {
143
- const tokenSet = await this.client.refresh(authData.refreshToken);
144
- return { accessToken: tokenSet.access_token };
145
- }
146
- catch (e) {
147
- throw handleError(e);
145
+ if (authData === null || authData === void 0 ? void 0 : authData.refreshToken) {
146
+ try {
147
+ const tokenSet = await this.client.refresh(authData.refreshToken);
148
+ // Update user refreshToken if provided
149
+ if (tokenSet.refresh_token) {
150
+ await this.usersService.updateOne(user.id, {
151
+ auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
152
+ });
153
+ }
154
+ }
155
+ catch (e) {
156
+ throw handleError(e);
157
+ }
148
158
  }
159
+ return sessionData;
149
160
  }
150
161
  }
151
162
  exports.OAuth2AuthDriver = OAuth2AuthDriver;
152
163
  const handleError = (e) => {
153
164
  if (e instanceof openid_client_1.errors.OPError) {
154
165
  if (e.error === 'invalid_grant') {
166
+ logger_1.default.trace(e, `[OAuth2] Invalid grant.`);
155
167
  // Invalid token
156
- return new exceptions_1.InvalidCredentialsException();
168
+ return new exceptions_1.InvalidTokenException();
157
169
  }
170
+ logger_1.default.trace(e, `[OAuth2] Unknown OP error.`);
158
171
  // Server response error
159
172
  return new exceptions_1.ServiceUnavailableException('Service returned unexpected response', {
160
- service: 'openid',
173
+ service: 'oauth2',
161
174
  message: e.error_description,
162
175
  });
163
176
  }
164
177
  else if (e instanceof openid_client_1.errors.RPError) {
165
178
  // Internal client error
179
+ logger_1.default.trace(e, `[OAuth2] Unknown RP error.`);
166
180
  return new exceptions_1.InvalidCredentialsException();
167
181
  }
182
+ logger_1.default.trace(e, `[OAuth2] Unknown error.`);
168
183
  return e;
169
184
  };
170
185
  function createOAuth2AuthRouter(providerName) {
@@ -172,7 +187,8 @@ function createOAuth2AuthRouter(providerName) {
172
187
  router.get('/', (req, res) => {
173
188
  const provider = (0, auth_1.getAuthProvider)(providerName);
174
189
  const codeVerifier = provider.generateCodeVerifier();
175
- const token = jsonwebtoken_1.default.sign({ verifier: codeVerifier, redirect: req.query.redirect }, env_1.default.SECRET, {
190
+ const prompt = !!req.query.prompt;
191
+ const token = jsonwebtoken_1.default.sign({ verifier: codeVerifier, redirect: req.query.redirect, prompt }, env_1.default.SECRET, {
176
192
  expiresIn: '5m',
177
193
  issuer: 'directus',
178
194
  });
@@ -180,7 +196,7 @@ function createOAuth2AuthRouter(providerName) {
180
196
  httpOnly: true,
181
197
  sameSite: 'lax',
182
198
  });
183
- return res.redirect(provider.generateAuthUrl(codeVerifier));
199
+ return res.redirect(provider.generateAuthUrl(codeVerifier, prompt));
184
200
  }, respond_1.respond);
185
201
  router.get('/callback', (0, async_handler_1.default)(async (req, res, next) => {
186
202
  var _a;
@@ -189,9 +205,10 @@ function createOAuth2AuthRouter(providerName) {
189
205
  tokenData = jsonwebtoken_1.default.verify(req.cookies[`oauth2.${providerName}`], env_1.default.SECRET, { issuer: 'directus' });
190
206
  }
191
207
  catch (e) {
208
+ logger_1.default.warn(e, `[OAuth2] Couldn't verify OAuth2 cookie`);
192
209
  throw new exceptions_1.InvalidCredentialsException();
193
210
  }
194
- const { verifier, redirect } = tokenData;
211
+ const { verifier, redirect, prompt } = tokenData;
195
212
  const authenticationService = new services_1.AuthenticationService({
196
213
  accountability: {
197
214
  ip: req.ip,
@@ -204,7 +221,7 @@ function createOAuth2AuthRouter(providerName) {
204
221
  try {
205
222
  res.clearCookie(`oauth2.${providerName}`);
206
223
  if (!req.query.code || !req.query.state) {
207
- logger_1.default.warn(`Couldn't extract OAuth2 code or state from query: ${JSON.stringify(req.query)}`);
224
+ logger_1.default.warn(`[OAuth2]Couldn't extract OAuth2 code or state from query: ${JSON.stringify(req.query)}`);
208
225
  }
209
226
  authResponse = await authenticationService.login(providerName, {
210
227
  code: req.query.code,
@@ -213,7 +230,10 @@ function createOAuth2AuthRouter(providerName) {
213
230
  });
214
231
  }
215
232
  catch (error) {
216
- logger_1.default.warn(error);
233
+ // Prompt user for a new refresh_token if invalidated
234
+ if (error instanceof exceptions_1.InvalidTokenException && !prompt) {
235
+ return res.redirect(`./?${redirect ? `redirect=${redirect}&` : ''}prompt=true`);
236
+ }
217
237
  if (redirect) {
218
238
  let reason = 'UNKNOWN_EXCEPTION';
219
239
  if (error instanceof exceptions_1.ServiceUnavailableException) {
@@ -222,8 +242,15 @@ function createOAuth2AuthRouter(providerName) {
222
242
  else if (error instanceof exceptions_1.InvalidCredentialsException) {
223
243
  reason = 'INVALID_USER';
224
244
  }
245
+ else if (error instanceof exceptions_1.InvalidTokenException) {
246
+ reason = 'INVALID_TOKEN';
247
+ }
248
+ else {
249
+ logger_1.default.warn(error, `[OAuth2] Unexpected error during OAuth2 login`);
250
+ }
225
251
  return res.redirect(`${redirect.split('?')[0]}?reason=${reason}`);
226
252
  }
253
+ logger_1.default.warn(error, `[OAuth2] Unexpected error during OAuth2 login`);
227
254
  throw error;
228
255
  }
229
256
  const { accessToken, refreshToken, expires } = authResponse;
@@ -10,7 +10,7 @@ export declare class OpenIDAuthDriver extends LocalAuthDriver {
10
10
  config: Record<string, any>;
11
11
  constructor(options: AuthDriverOptions, config: Record<string, any>);
12
12
  generateCodeVerifier(): string;
13
- generateAuthUrl(codeVerifier: string): Promise<string>;
13
+ generateAuthUrl(codeVerifier: string, prompt?: boolean): Promise<string>;
14
14
  private fetchUserId;
15
15
  getUserID(payload: Record<string, any>): Promise<string>;
16
16
  login(user: User): Promise<SessionData>;
@@ -50,18 +50,21 @@ class OpenIDAuthDriver extends local_1.LocalAuthDriver {
50
50
  generateCodeVerifier() {
51
51
  return openid_client_1.generators.codeVerifier();
52
52
  }
53
- async generateAuthUrl(codeVerifier) {
53
+ async generateAuthUrl(codeVerifier, prompt = false) {
54
54
  var _a;
55
55
  try {
56
56
  const client = await this.client;
57
57
  const codeChallenge = openid_client_1.generators.codeChallenge(codeVerifier);
58
+ const paramsConfig = typeof this.config.params === 'object' ? this.config.params : {};
58
59
  return client.authorizationUrl({
59
60
  scope: (_a = this.config.scope) !== null && _a !== void 0 ? _a : 'openid profile email',
61
+ access_type: 'offline',
62
+ prompt: prompt ? 'consent' : undefined,
63
+ ...paramsConfig,
60
64
  code_challenge: codeChallenge,
61
65
  code_challenge_method: 'S256',
62
66
  // Some providers require state even with PKCE
63
67
  state: codeChallenge,
64
- access_type: 'offline',
65
68
  });
66
69
  }
67
70
  catch (e) {
@@ -144,17 +147,22 @@ class OpenIDAuthDriver extends local_1.LocalAuthDriver {
144
147
  logger_1.default.warn(`Session data isn't valid JSON: ${authData}`);
145
148
  }
146
149
  }
147
- if (!(authData === null || authData === void 0 ? void 0 : authData.refreshToken)) {
148
- return sessionData;
149
- }
150
- try {
151
- const client = await this.client;
152
- const tokenSet = await client.refresh(authData.refreshToken);
153
- return { accessToken: tokenSet.access_token };
154
- }
155
- catch (e) {
156
- throw handleError(e);
150
+ if (authData === null || authData === void 0 ? void 0 : authData.refreshToken) {
151
+ try {
152
+ const client = await this.client;
153
+ const tokenSet = await client.refresh(authData.refreshToken);
154
+ // Update user refreshToken if provided
155
+ if (tokenSet.refresh_token) {
156
+ await this.usersService.updateOne(user.id, {
157
+ auth_data: JSON.stringify({ refreshToken: tokenSet.refresh_token }),
158
+ });
159
+ }
160
+ }
161
+ catch (e) {
162
+ throw handleError(e);
163
+ }
157
164
  }
165
+ return sessionData;
158
166
  }
159
167
  }
160
168
  exports.OpenIDAuthDriver = OpenIDAuthDriver;
@@ -162,7 +170,7 @@ const handleError = (e) => {
162
170
  if (e instanceof openid_client_1.errors.OPError) {
163
171
  if (e.error === 'invalid_grant') {
164
172
  // Invalid token
165
- return new exceptions_1.InvalidCredentialsException();
173
+ return new exceptions_1.InvalidTokenException();
166
174
  }
167
175
  // Server response error
168
176
  return new exceptions_1.ServiceUnavailableException('Service returned unexpected response', {
@@ -181,7 +189,8 @@ function createOpenIDAuthRouter(providerName) {
181
189
  router.get('/', (0, async_handler_1.default)(async (req, res) => {
182
190
  const provider = (0, auth_1.getAuthProvider)(providerName);
183
191
  const codeVerifier = provider.generateCodeVerifier();
184
- const token = jsonwebtoken_1.default.sign({ verifier: codeVerifier, redirect: req.query.redirect }, env_1.default.SECRET, {
192
+ const prompt = !!req.query.prompt;
193
+ const token = jsonwebtoken_1.default.sign({ verifier: codeVerifier, redirect: req.query.redirect, prompt }, env_1.default.SECRET, {
185
194
  expiresIn: '5m',
186
195
  issuer: 'directus',
187
196
  });
@@ -189,7 +198,7 @@ function createOpenIDAuthRouter(providerName) {
189
198
  httpOnly: true,
190
199
  sameSite: 'lax',
191
200
  });
192
- return res.redirect(await provider.generateAuthUrl(codeVerifier));
201
+ return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt));
193
202
  }), respond_1.respond);
194
203
  router.get('/callback', (0, async_handler_1.default)(async (req, res, next) => {
195
204
  var _a;
@@ -198,9 +207,10 @@ function createOpenIDAuthRouter(providerName) {
198
207
  tokenData = jsonwebtoken_1.default.verify(req.cookies[`openid.${providerName}`], env_1.default.SECRET, { issuer: 'directus' });
199
208
  }
200
209
  catch (e) {
210
+ logger_1.default.warn(e, `[OpenID] Couldn't verify OpenID cookie`);
201
211
  throw new exceptions_1.InvalidCredentialsException();
202
212
  }
203
- const { verifier, redirect } = tokenData;
213
+ const { verifier, redirect, prompt } = tokenData;
204
214
  const authenticationService = new services_1.AuthenticationService({
205
215
  accountability: {
206
216
  ip: req.ip,
@@ -213,7 +223,7 @@ function createOpenIDAuthRouter(providerName) {
213
223
  try {
214
224
  res.clearCookie(`openid.${providerName}`);
215
225
  if (!req.query.code || !req.query.state) {
216
- logger_1.default.warn(`Couldn't extract OpenID code or state from query: ${JSON.stringify(req.query)}`);
226
+ logger_1.default.warn(`[OpenID] Couldn't extract OpenID code or state from query: ${JSON.stringify(req.query)}`);
217
227
  }
218
228
  authResponse = await authenticationService.login(providerName, {
219
229
  code: req.query.code,
@@ -222,6 +232,10 @@ function createOpenIDAuthRouter(providerName) {
222
232
  });
223
233
  }
224
234
  catch (error) {
235
+ // Prompt user for a new refresh_token if invalidated
236
+ if (error instanceof exceptions_1.InvalidTokenException && !prompt) {
237
+ return res.redirect(`./?${redirect ? `redirect=${redirect}&` : ''}prompt=true`);
238
+ }
225
239
  logger_1.default.warn(error);
226
240
  if (redirect) {
227
241
  let reason = 'UNKNOWN_EXCEPTION';
@@ -231,6 +245,9 @@ function createOpenIDAuthRouter(providerName) {
231
245
  else if (error instanceof exceptions_1.InvalidCredentialsException) {
232
246
  reason = 'INVALID_USER';
233
247
  }
248
+ else if (error instanceof exceptions_1.InvalidTokenException) {
249
+ reason = 'INVALID_TOKEN';
250
+ }
234
251
  return res.redirect(`${redirect.split('?')[0]}?reason=${reason}`);
235
252
  }
236
253
  throw error;
@@ -8,6 +8,7 @@ export * from './invalid-ip';
8
8
  export * from './invalid-otp';
9
9
  export * from './invalid-payload';
10
10
  export * from './invalid-query';
11
+ export * from './invalid-token';
11
12
  export * from './method-not-allowed';
12
13
  export * from './range-not-satisfiable';
13
14
  export * from './route-not-found';
@@ -20,6 +20,7 @@ __exportStar(require("./invalid-ip"), exports);
20
20
  __exportStar(require("./invalid-otp"), exports);
21
21
  __exportStar(require("./invalid-payload"), exports);
22
22
  __exportStar(require("./invalid-query"), exports);
23
+ __exportStar(require("./invalid-token"), exports);
23
24
  __exportStar(require("./method-not-allowed"), exports);
24
25
  __exportStar(require("./range-not-satisfiable"), exports);
25
26
  __exportStar(require("./route-not-found"), exports);
@@ -0,0 +1,4 @@
1
+ import { BaseException } from '@directus/shared/exceptions';
2
+ export declare class InvalidTokenException extends BaseException {
3
+ constructor(message?: string);
4
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidTokenException = void 0;
4
+ const exceptions_1 = require("@directus/shared/exceptions");
5
+ class InvalidTokenException extends exceptions_1.BaseException {
6
+ constructor(message = 'Invalid token') {
7
+ super(message, 403, 'INVALID_TOKEN');
8
+ }
9
+ }
10
+ exports.InvalidTokenException = InvalidTokenException;
@@ -8,7 +8,6 @@ const lodash_1 = require("lodash");
8
8
  const nanoid_1 = require("nanoid");
9
9
  const uuid_validate_1 = __importDefault(require("uuid-validate"));
10
10
  const exceptions_1 = require("../exceptions");
11
- const apply_function_to_column_name_1 = require("./apply-function-to-column-name");
12
11
  const get_column_1 = require("./get-column");
13
12
  const get_relation_type_1 = require("./get-relation-type");
14
13
  const helpers_1 = require("../database/helpers");
@@ -44,7 +43,7 @@ function applyQuery(knex, collection, dbQuery, query, schema, subQuery = false)
44
43
  applySearch(schema, dbQuery, query.search, collection);
45
44
  }
46
45
  if (query.group) {
47
- dbQuery.groupBy(`${collection}.${query.group.map(apply_function_to_column_name_1.applyFunctionToColumnName)}`);
46
+ dbQuery.groupBy(query.group.map((column) => (0, get_column_1.getColumn)(knex, collection, column, false)));
48
47
  }
49
48
  if (query.aggregate) {
50
49
  applyAggregate(dbQuery, query.aggregate, collection);
@@ -23,13 +23,13 @@ function mergePerm(currentPerm, newPerm) {
23
23
  let validation = currentPerm.validation;
24
24
  let fields = currentPerm.fields;
25
25
  let presets = currentPerm.presets;
26
- if (newPerm.permissions && !(0, lodash_1.isEmpty)(newPerm.permissions)) {
26
+ if (newPerm.permissions) {
27
27
  if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === '_or') {
28
28
  permissions = {
29
29
  _or: [...currentPerm.permissions._or, newPerm.permissions],
30
30
  };
31
31
  }
32
- else if (currentPerm.permissions && !(0, lodash_1.isEmpty)(currentPerm.permissions)) {
32
+ else if (currentPerm.permissions) {
33
33
  permissions = {
34
34
  _or: [currentPerm.permissions, newPerm.permissions],
35
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus",
3
- "version": "9.2.0",
3
+ "version": "9.2.1",
4
4
  "license": "GPL-3.0-only",
5
5
  "homepage": "https://github.com/directus/directus#readme",
6
6
  "description": "Directus is a real-time API and App dashboard for managing SQL database content.",
@@ -76,16 +76,16 @@
76
76
  ],
77
77
  "dependencies": {
78
78
  "@aws-sdk/client-ses": "^3.40.0",
79
- "@directus/app": "9.2.0",
80
- "@directus/drive": "9.2.0",
81
- "@directus/drive-azure": "9.2.0",
82
- "@directus/drive-gcs": "9.2.0",
83
- "@directus/drive-s3": "9.2.0",
84
- "@directus/extensions-sdk": "9.2.0",
85
- "@directus/format-title": "9.2.0",
86
- "@directus/schema": "9.2.0",
87
- "@directus/shared": "9.2.0",
88
- "@directus/specs": "9.2.0",
79
+ "@directus/app": "9.2.1",
80
+ "@directus/drive": "9.2.1",
81
+ "@directus/drive-azure": "9.2.1",
82
+ "@directus/drive-gcs": "9.2.1",
83
+ "@directus/drive-s3": "9.2.1",
84
+ "@directus/extensions-sdk": "9.2.1",
85
+ "@directus/format-title": "9.2.1",
86
+ "@directus/schema": "9.2.1",
87
+ "@directus/shared": "9.2.1",
88
+ "@directus/specs": "9.2.1",
89
89
  "@godaddy/terminus": "^4.9.0",
90
90
  "@rollup/plugin-alias": "^3.1.2",
91
91
  "@rollup/plugin-virtual": "^2.0.3",
@@ -169,7 +169,7 @@
169
169
  "sqlite3": "^5.0.2",
170
170
  "tedious": "^13.0.0"
171
171
  },
172
- "gitHead": "776c105aacfda559a1ebf49cb2220599652f6c48",
172
+ "gitHead": "1d4e9c425c10bcdc5831ae63c1bd03649648eb5e",
173
173
  "devDependencies": {
174
174
  "@types/async": "3.2.10",
175
175
  "@types/atob": "2.1.2",
@@ -199,7 +199,7 @@
199
199
  "@types/nodemailer": "6.4.4",
200
200
  "@types/object-hash": "2.2.1",
201
201
  "@types/qs": "6.9.7",
202
- "@types/sanitize-html": "^2.5.0",
202
+ "@types/sanitize-html": "2.5.0",
203
203
  "@types/sharp": "0.29.4",
204
204
  "@types/stream-json": "1.7.1",
205
205
  "@types/supertest": "2.0.11",