@directus/api 32.2.0 → 33.1.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.
Files changed (297) hide show
  1. package/dist/ai/chat/controllers/chat.post.js +19 -4
  2. package/dist/ai/chat/lib/create-ui-stream.d.ts +8 -7
  3. package/dist/ai/chat/lib/create-ui-stream.js +28 -25
  4. package/dist/ai/chat/middleware/load-settings.js +31 -7
  5. package/dist/ai/chat/models/chat-request.d.ts +135 -2
  6. package/dist/ai/chat/models/chat-request.js +56 -2
  7. package/dist/ai/chat/models/providers.d.ts +16 -2
  8. package/dist/ai/chat/models/providers.js +16 -2
  9. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
  10. package/dist/ai/chat/utils/format-context.d.ts +5 -0
  11. package/dist/ai/chat/utils/format-context.js +122 -0
  12. package/dist/ai/mcp/server.d.ts +27 -1
  13. package/dist/ai/providers/index.d.ts +3 -0
  14. package/dist/ai/providers/index.js +3 -0
  15. package/dist/ai/providers/options.d.ts +14 -0
  16. package/dist/ai/providers/options.js +26 -0
  17. package/dist/ai/providers/registry.d.ts +6 -0
  18. package/dist/ai/providers/registry.js +65 -0
  19. package/dist/ai/providers/types.d.ts +34 -0
  20. package/dist/ai/providers/types.js +1 -0
  21. package/dist/ai/tools/assets/index.js +1 -1
  22. package/dist/ai/tools/collections/index.js +2 -2
  23. package/dist/ai/tools/fields/index.js +2 -2
  24. package/dist/ai/tools/files/index.js +1 -1
  25. package/dist/ai/tools/flows/index.js +1 -1
  26. package/dist/ai/tools/folders/index.js +1 -1
  27. package/dist/ai/tools/items/index.js +6 -3
  28. package/dist/ai/tools/items/prompt.md +7 -9
  29. package/dist/ai/tools/relations/index.js +1 -1
  30. package/dist/ai/tools/schema.js +1 -1
  31. package/dist/ai/tools/trigger-flow/index.js +1 -1
  32. package/dist/app.js +12 -8
  33. package/dist/auth/drivers/ldap.d.ts +1 -1
  34. package/dist/auth/drivers/ldap.js +144 -139
  35. package/dist/auth/drivers/local.js +1 -1
  36. package/dist/auth/drivers/oauth2.d.ts +1 -2
  37. package/dist/auth/drivers/oauth2.js +22 -17
  38. package/dist/auth/drivers/openid.d.ts +1 -2
  39. package/dist/auth/drivers/openid.js +18 -13
  40. package/dist/auth/drivers/saml.js +3 -3
  41. package/dist/auth/utils/generate-callback-url.d.ts +11 -0
  42. package/dist/auth/utils/generate-callback-url.js +40 -0
  43. package/dist/auth/utils/is-login-redirect-allowed.d.ts +7 -0
  44. package/dist/{utils → auth/utils}/is-login-redirect-allowed.js +12 -9
  45. package/dist/cache.d.ts +12 -0
  46. package/dist/cache.js +27 -3
  47. package/dist/cli/commands/bootstrap/index.js +2 -2
  48. package/dist/cli/commands/database/install.js +1 -1
  49. package/dist/cli/commands/database/migrate.js +1 -1
  50. package/dist/cli/commands/init/index.js +2 -2
  51. package/dist/cli/commands/roles/create.js +4 -4
  52. package/dist/cli/commands/schema/apply.js +3 -3
  53. package/dist/cli/commands/schema/snapshot.js +1 -1
  54. package/dist/cli/utils/create-db-connection.d.ts +1 -1
  55. package/dist/cli/utils/create-db-connection.js +1 -1
  56. package/dist/cli/utils/create-env/env-stub.liquid +3 -0
  57. package/dist/cli/utils/create-env/index.js +1 -1
  58. package/dist/constants.d.ts +7 -3
  59. package/dist/constants.js +7 -3
  60. package/dist/controllers/access.js +1 -1
  61. package/dist/controllers/assets.js +1 -1
  62. package/dist/controllers/deployment.js +481 -0
  63. package/dist/controllers/extensions.js +1 -1
  64. package/dist/controllers/fields.js +8 -6
  65. package/dist/controllers/files.js +1 -1
  66. package/dist/controllers/items.js +1 -1
  67. package/dist/controllers/not-found.js +1 -1
  68. package/dist/controllers/relations.js +1 -1
  69. package/dist/database/errors/dialects/mysql.d.ts +1 -1
  70. package/dist/database/errors/dialects/postgres.d.ts +1 -1
  71. package/dist/database/errors/dialects/sqlite.d.ts +1 -1
  72. package/dist/database/errors/translate.d.ts +1 -1
  73. package/dist/database/errors/translate.js +1 -1
  74. package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
  75. package/dist/database/helpers/date/dialects/mssql.js +1 -1
  76. package/dist/database/helpers/date/dialects/mysql.js +1 -1
  77. package/dist/database/helpers/date/types.js +1 -1
  78. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
  79. package/dist/database/helpers/schema/dialects/cockroachdb.js +24 -1
  80. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  81. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  82. package/dist/database/helpers/schema/dialects/mysql.js +16 -3
  83. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  84. package/dist/database/helpers/schema/types.d.ts +13 -0
  85. package/dist/database/helpers/schema/types.js +24 -0
  86. package/dist/database/index.js +4 -4
  87. package/dist/database/migrations/20220429A-add-flows.js +1 -1
  88. package/dist/database/migrations/20230526A-migrate-translation-strings.js +1 -1
  89. package/dist/database/migrations/20231009A-update-csv-fields-to-text.js +1 -1
  90. package/dist/database/migrations/20240204A-marketplace.js +9 -7
  91. package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +15 -0
  92. package/dist/database/migrations/20240311A-deprecate-webhooks.js +1 -1
  93. package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
  94. package/dist/database/migrations/20240924A-migrate-legacy-comments.js +1 -1
  95. package/dist/database/migrations/20251014A-add-project-owner.js +1 -1
  96. package/dist/database/migrations/20251224A-remove-webhooks.d.ts +3 -0
  97. package/dist/database/migrations/20251224A-remove-webhooks.js +19 -0
  98. package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
  99. package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
  100. package/dist/database/migrations/20260113A-add-revisions-index.d.ts +3 -0
  101. package/dist/database/migrations/20260113A-add-revisions-index.js +41 -0
  102. package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
  103. package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
  104. package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
  105. package/dist/database/migrations/20260204A-add-deployment.js +32 -0
  106. package/dist/database/migrations/run.js +3 -3
  107. package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
  108. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
  109. package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +1 -1
  110. package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
  111. package/dist/database/run-ast/lib/apply-query/filter/operator.js +1 -1
  112. package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
  113. package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
  114. package/dist/database/run-ast/utils/get-column.js +1 -1
  115. package/dist/database/seeds/run.js +3 -3
  116. package/dist/deployment/deployment.d.ts +94 -0
  117. package/dist/deployment/deployment.js +29 -0
  118. package/dist/deployment/drivers/index.d.ts +1 -0
  119. package/dist/deployment/drivers/index.js +1 -0
  120. package/dist/deployment/drivers/vercel.d.ts +32 -0
  121. package/dist/deployment/drivers/vercel.js +208 -0
  122. package/dist/deployment/index.d.ts +2 -0
  123. package/dist/deployment/index.js +2 -0
  124. package/dist/deployment.d.ts +24 -0
  125. package/dist/deployment.js +39 -0
  126. package/dist/extensions/lib/get-extensions-path.js +1 -1
  127. package/dist/extensions/lib/get-extensions-settings.js +1 -1
  128. package/dist/extensions/lib/get-extensions.js +1 -1
  129. package/dist/extensions/lib/get-shared-deps-mapping.js +3 -3
  130. package/dist/extensions/lib/installation/manager.js +3 -3
  131. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -1
  132. package/dist/extensions/lib/sync/status.js +1 -1
  133. package/dist/extensions/lib/sync/sync.js +7 -7
  134. package/dist/extensions/lib/sync/utils.js +2 -2
  135. package/dist/extensions/manager.d.ts +1 -1
  136. package/dist/extensions/manager.js +8 -8
  137. package/dist/flows.d.ts +1 -1
  138. package/dist/logger/index.js +1 -1
  139. package/dist/logger/logs-stream.d.ts +1 -1
  140. package/dist/logger/logs-stream.js +1 -1
  141. package/dist/mailer.js +1 -1
  142. package/dist/metrics/lib/create-metrics.js +2 -2
  143. package/dist/middleware/authenticate.js +3 -3
  144. package/dist/middleware/collection-exists.js +1 -1
  145. package/dist/middleware/extract-token.js +1 -1
  146. package/dist/middleware/graphql.js +2 -2
  147. package/dist/middleware/respond.js +27 -14
  148. package/dist/middleware/validate-batch.js +1 -1
  149. package/dist/operations/exec/index.js +2 -1
  150. package/dist/operations/mail/index.js +1 -1
  151. package/dist/operations/mail/rate-limiter.js +2 -2
  152. package/dist/permissions/cache.js +5 -0
  153. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -1
  154. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -1
  155. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -2
  156. package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -1
  157. package/dist/permissions/modules/process-ast/process-ast.js +1 -1
  158. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
  159. package/dist/permissions/modules/process-payload/process-payload.js +1 -1
  160. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +14 -2
  161. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +72 -13
  162. package/dist/permissions/modules/validate-access/validate-access.js +3 -2
  163. package/dist/rate-limiter.js +1 -1
  164. package/dist/request/is-denied-ip.js +1 -1
  165. package/dist/schedules/project.js +1 -1
  166. package/dist/schedules/telemetry.js +1 -1
  167. package/dist/schedules/tus.js +1 -1
  168. package/dist/server.js +6 -5
  169. package/dist/services/assets.d.ts +2 -1
  170. package/dist/services/assets.js +35 -8
  171. package/dist/services/authentication.js +2 -2
  172. package/dist/services/collections.js +1 -1
  173. package/dist/services/deployment-projects.d.ts +20 -0
  174. package/dist/services/deployment-projects.js +34 -0
  175. package/dist/services/deployment-runs.d.ts +13 -0
  176. package/dist/services/deployment-runs.js +6 -0
  177. package/dist/services/deployment.d.ts +40 -0
  178. package/dist/services/deployment.js +202 -0
  179. package/dist/services/extensions.d.ts +1 -1
  180. package/dist/services/files/utils/get-metadata.d.ts +1 -1
  181. package/dist/services/files/utils/get-metadata.js +1 -1
  182. package/dist/services/files.d.ts +1 -1
  183. package/dist/services/files.js +4 -4
  184. package/dist/services/graphql/index.d.ts +1 -1
  185. package/dist/services/graphql/index.js +1 -1
  186. package/dist/services/graphql/resolvers/mutation.js +1 -1
  187. package/dist/services/graphql/resolvers/system-admin.js +2 -3
  188. package/dist/services/graphql/schema/get-types.d.ts +1 -1
  189. package/dist/services/graphql/schema/read.js +1 -1
  190. package/dist/services/graphql/subscription.d.ts +1 -1
  191. package/dist/services/graphql/types/date.js +1 -1
  192. package/dist/services/graphql/types/hash.js +1 -1
  193. package/dist/services/graphql/utils/add-path-to-validation-error.js +1 -1
  194. package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
  195. package/dist/services/import-export.d.ts +1 -1
  196. package/dist/services/import-export.js +2 -2
  197. package/dist/services/index.d.ts +3 -1
  198. package/dist/services/index.js +3 -1
  199. package/dist/services/mail/index.js +2 -2
  200. package/dist/services/mail/rate-limiter.js +2 -2
  201. package/dist/services/payload.js +2 -2
  202. package/dist/services/schema.js +1 -1
  203. package/dist/services/server.js +13 -4
  204. package/dist/services/settings.js +2 -2
  205. package/dist/services/specifications.js +2 -2
  206. package/dist/services/tfa.js +1 -1
  207. package/dist/services/translations.js +1 -1
  208. package/dist/services/tus/data-store.d.ts +1 -3
  209. package/dist/services/tus/data-store.js +2 -5
  210. package/dist/services/tus/server.js +6 -6
  211. package/dist/services/users.js +4 -4
  212. package/dist/services/versions.js +1 -1
  213. package/dist/telemetry/lib/get-report.js +2 -0
  214. package/dist/telemetry/lib/send-report.d.ts +1 -1
  215. package/dist/telemetry/lib/send-report.js +1 -1
  216. package/dist/telemetry/lib/track.js +1 -1
  217. package/dist/telemetry/types/report.d.ts +8 -0
  218. package/dist/telemetry/utils/get-settings.d.ts +2 -0
  219. package/dist/telemetry/utils/get-settings.js +5 -0
  220. package/dist/test-utils/knex.js +1 -1
  221. package/dist/types/collection.d.ts +1 -1
  222. package/dist/utils/async-handler.d.ts +1 -1
  223. package/dist/utils/calculate-field-depth.js +1 -1
  224. package/dist/utils/compress.js +1 -1
  225. package/dist/utils/deep-map-response.d.ts +1 -1
  226. package/dist/utils/deep-map-response.js +2 -2
  227. package/dist/utils/get-cache-key.js +1 -1
  228. package/dist/utils/get-column-path.js +1 -1
  229. package/dist/utils/get-field-system-rows.js +1 -1
  230. package/dist/utils/get-ip-from-req.d.ts +1 -1
  231. package/dist/utils/get-ip-from-req.js +1 -1
  232. package/dist/utils/get-local-type.js +7 -3
  233. package/dist/utils/get-service.js +7 -3
  234. package/dist/utils/get-snapshot-diff.js +1 -1
  235. package/dist/utils/is-field-allowed.d.ts +4 -0
  236. package/dist/utils/is-field-allowed.js +9 -0
  237. package/dist/utils/is-url-allowed.js +1 -1
  238. package/dist/utils/jwt.js +1 -1
  239. package/dist/utils/sanitize-schema.d.ts +1 -1
  240. package/dist/utils/should-clear-cache.d.ts +1 -1
  241. package/dist/utils/should-skip-cache.js +2 -2
  242. package/dist/utils/validate-diff.js +1 -1
  243. package/dist/utils/validate-snapshot.js +3 -3
  244. package/dist/utils/validate-storage.js +2 -2
  245. package/dist/utils/verify-session-jwt.js +1 -1
  246. package/dist/utils/versioning/handle-version.js +1 -1
  247. package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
  248. package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
  249. package/dist/websocket/collab/collab.d.ts +63 -0
  250. package/dist/websocket/collab/collab.js +481 -0
  251. package/dist/websocket/collab/constants.d.ts +1 -0
  252. package/dist/websocket/collab/constants.js +13 -0
  253. package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
  254. package/dist/websocket/collab/filter-to-fields.js +11 -0
  255. package/dist/websocket/collab/messenger.d.ts +43 -0
  256. package/dist/websocket/collab/messenger.js +225 -0
  257. package/dist/websocket/collab/payload-permissions.d.ts +18 -0
  258. package/dist/websocket/collab/payload-permissions.js +158 -0
  259. package/dist/websocket/collab/permissions-cache.d.ts +52 -0
  260. package/dist/websocket/collab/permissions-cache.js +204 -0
  261. package/dist/websocket/collab/room.d.ts +125 -0
  262. package/dist/websocket/collab/room.js +593 -0
  263. package/dist/websocket/collab/store.d.ts +7 -0
  264. package/dist/websocket/collab/store.js +33 -0
  265. package/dist/websocket/collab/types.d.ts +21 -0
  266. package/dist/websocket/collab/types.js +1 -0
  267. package/dist/websocket/collab/verify-permissions.d.ts +11 -0
  268. package/dist/websocket/collab/verify-permissions.js +100 -0
  269. package/dist/websocket/controllers/base.d.ts +2 -2
  270. package/dist/websocket/controllers/base.js +3 -3
  271. package/dist/websocket/controllers/graphql.d.ts +1 -1
  272. package/dist/websocket/controllers/graphql.js +1 -1
  273. package/dist/websocket/controllers/logs.d.ts +1 -1
  274. package/dist/websocket/controllers/rest.d.ts +1 -1
  275. package/dist/websocket/controllers/rest.js +2 -2
  276. package/dist/websocket/handlers/heartbeat.js +1 -1
  277. package/dist/websocket/handlers/index.d.ts +2 -0
  278. package/dist/websocket/handlers/index.js +9 -0
  279. package/dist/websocket/handlers/items.js +2 -2
  280. package/dist/websocket/handlers/subscribe.js +1 -1
  281. package/dist/websocket/types.d.ts +1 -1
  282. package/dist/websocket/utils/items.d.ts +2 -2
  283. package/dist/websocket/utils/message.d.ts +1 -1
  284. package/dist/websocket/utils/message.js +2 -2
  285. package/dist/websocket/utils/wait-for-message.js +1 -1
  286. package/package.json +35 -33
  287. package/dist/controllers/webhooks.js +0 -74
  288. package/dist/services/webhooks.d.ts +0 -14
  289. package/dist/services/webhooks.js +0 -32
  290. package/dist/utils/get-relation-info.d.ts +0 -6
  291. package/dist/utils/get-relation-info.js +0 -43
  292. package/dist/utils/get-relation-type.d.ts +0 -6
  293. package/dist/utils/get-relation-type.js +0 -18
  294. package/dist/utils/is-login-redirect-allowed.d.ts +0 -4
  295. package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
  296. package/dist/utils/versioning/deep-map-with-schema.js +0 -81
  297. /package/dist/controllers/{webhooks.d.ts → deployment.d.ts} +0 -0
@@ -1,8 +1,8 @@
1
1
  import { useEnv } from '@directus/env';
2
- import { ErrorCode, InvalidCredentialsError, InvalidPayloadError, InvalidProviderConfigError, InvalidProviderError, ServiceUnavailableError, UnexpectedResponseError, isDirectusError, } from '@directus/errors';
2
+ import { ErrorCode, InvalidCredentialsError, InvalidPayloadError, InvalidProviderConfigError, InvalidProviderError, isDirectusError, ServiceUnavailableError, UnexpectedResponseError, } from '@directus/errors';
3
3
  import { Router } from 'express';
4
4
  import Joi from 'joi';
5
- import ldap from 'ldapjs';
5
+ import { Client, InappropriateAuthError, InsufficientAccessError, InvalidCredentialsError as LdapInvalidCredentialsError, } from 'ldapts';
6
6
  import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
7
7
  import getDatabase from '../../database/index.js';
8
8
  import emitter from '../../emitter.js';
@@ -12,8 +12,8 @@ import { createDefaultAccountability } from '../../permissions/utils/create-defa
12
12
  import { AuthenticationService } from '../../services/authentication.js';
13
13
  import asyncHandler from '../../utils/async-handler.js';
14
14
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
15
- import { AuthDriver } from '../auth.js';
16
15
  import { getSchema } from '../../utils/get-schema.js';
16
+ import { AuthDriver } from '../auth.js';
17
17
  // 0x2: ACCOUNTDISABLE
18
18
  // 0x10: LOCKOUT
19
19
  // 0x800000: PASSWORD_EXPIRED
@@ -34,127 +34,101 @@ export class LDAPAuthDriver extends AuthDriver {
34
34
  throw new InvalidProviderConfigError({ provider });
35
35
  }
36
36
  const clientConfig = typeof config['client'] === 'object' ? config['client'] : {};
37
- this.bindClient = ldap.createClient({ url: clientUrl, reconnect: true, ...clientConfig });
38
- this.bindClient.on('error', (err) => {
39
- logger.warn(err);
37
+ this.bindClient = new Client({
38
+ url: clientUrl,
39
+ ...clientConfig,
40
40
  });
41
41
  this.config = config;
42
42
  }
43
43
  async validateBindClient() {
44
44
  const logger = useLogger();
45
45
  const { bindDn, bindPassword, provider } = this.config;
46
- return new Promise((resolve, reject) => {
47
- // Healthcheck bind user
48
- this.bindClient.search(bindDn, {}, (err, res) => {
49
- if (err) {
50
- reject(handleError(err));
51
- return;
52
- }
53
- res.on('searchEntry', () => {
54
- resolve();
55
- });
56
- res.on('error', () => {
57
- // Attempt to rebind on search error
58
- this.bindClient.bind(bindDn, bindPassword, (err) => {
59
- if (err) {
60
- const error = handleError(err);
61
- if (isDirectusError(error, ErrorCode.InvalidCredentials)) {
62
- logger.warn('Invalid bind user');
63
- reject(new InvalidProviderConfigError({ provider }));
64
- }
65
- else {
66
- reject(error);
67
- }
68
- }
69
- else {
70
- resolve();
71
- }
72
- });
73
- });
74
- res.on('end', (result) => {
75
- // Handle edge case where authenticated bind user cannot read their own DN
76
- // Status `0` is success
77
- if (result?.status !== 0) {
78
- logger.warn('[LDAP] Failed to find bind user record');
79
- reject(new UnexpectedResponseError());
80
- }
81
- });
46
+ try {
47
+ // Attempt to bind with the configured credentials
48
+ await this.bindClient.bind(bindDn, bindPassword);
49
+ // Healthcheck: verify bind user can read their own DN
50
+ const { searchEntries } = await this.bindClient.search(bindDn, {
51
+ scope: 'base',
82
52
  });
83
- });
53
+ if (searchEntries.length === 0) {
54
+ logger.warn('[LDAP] Failed to find bind user record');
55
+ throw new UnexpectedResponseError();
56
+ }
57
+ }
58
+ catch (err) {
59
+ const error = handleError(err);
60
+ if (isDirectusError(error, ErrorCode.InvalidCredentials)) {
61
+ logger.warn('Invalid bind user');
62
+ throw new InvalidProviderConfigError({ provider });
63
+ }
64
+ throw error;
65
+ }
84
66
  }
85
67
  async fetchUserInfo(baseDn, filter, scope) {
86
68
  let { firstNameAttribute, lastNameAttribute, mailAttribute } = this.config;
87
69
  firstNameAttribute ??= 'givenName';
88
70
  lastNameAttribute ??= 'sn';
89
71
  mailAttribute ??= 'mail';
90
- return new Promise((resolve, reject) => {
91
- // Search for the user in LDAP by filter
92
- this.bindClient.search(baseDn, {
93
- filter,
94
- scope,
72
+ try {
73
+ const searchOptions = {
95
74
  attributes: ['uid', firstNameAttribute, lastNameAttribute, mailAttribute, 'userAccountControl'],
96
- }, (err, res) => {
97
- if (err) {
98
- reject(handleError(err));
99
- return;
100
- }
101
- res.on('searchEntry', ({ object }) => {
102
- const user = {
103
- dn: object['dn'],
104
- userAccountControl: Number(getEntryValue(object['userAccountControl']) ?? 0),
105
- };
106
- const firstName = getEntryValue(object[firstNameAttribute]);
107
- if (firstName)
108
- user.firstName = firstName;
109
- const lastName = getEntryValue(object[lastNameAttribute]);
110
- if (lastName)
111
- user.lastName = lastName;
112
- const email = getEntryValue(object[mailAttribute]);
113
- if (email)
114
- user.email = email;
115
- const uid = getEntryValue(object['uid']);
116
- if (uid)
117
- user.uid = uid;
118
- resolve(user);
119
- });
120
- res.on('error', (err) => {
121
- reject(handleError(err));
122
- });
123
- res.on('end', () => {
124
- resolve(undefined);
125
- });
126
- });
127
- });
75
+ };
76
+ if (filter !== undefined)
77
+ searchOptions.filter = filter;
78
+ if (scope !== undefined)
79
+ searchOptions.scope = scope;
80
+ const { searchEntries } = await this.bindClient.search(baseDn, searchOptions);
81
+ if (searchEntries.length === 0) {
82
+ return undefined;
83
+ }
84
+ const entry = searchEntries[0];
85
+ const user = {
86
+ dn: entry['dn'],
87
+ userAccountControl: Number(getEntryValue(entry['userAccountControl']) ?? 0),
88
+ };
89
+ const firstName = getEntryValue(entry[firstNameAttribute]);
90
+ if (firstName)
91
+ user.firstName = firstName;
92
+ const lastName = getEntryValue(entry[lastNameAttribute]);
93
+ if (lastName)
94
+ user.lastName = lastName;
95
+ const email = getEntryValue(entry[mailAttribute]);
96
+ if (email)
97
+ user.email = email;
98
+ const uid = getEntryValue(entry['uid']);
99
+ if (uid)
100
+ user.uid = uid;
101
+ return user;
102
+ }
103
+ catch (err) {
104
+ throw handleError(err);
105
+ }
128
106
  }
129
107
  async fetchUserGroups(baseDn, filter, scope) {
130
- return new Promise((resolve, reject) => {
131
- let userGroups = [];
132
- // Search for the user info in LDAP by group attribute
133
- this.bindClient.search(baseDn, {
134
- filter,
135
- scope,
108
+ try {
109
+ const searchOptions = {
136
110
  attributes: ['cn'],
137
- }, (err, res) => {
138
- if (err) {
139
- reject(handleError(err));
140
- return;
111
+ };
112
+ if (filter !== undefined)
113
+ searchOptions.filter = filter;
114
+ if (scope !== undefined)
115
+ searchOptions.scope = scope;
116
+ const { searchEntries } = await this.bindClient.search(baseDn, searchOptions);
117
+ const userGroups = [];
118
+ for (const entry of searchEntries) {
119
+ const cn = entry['cn'];
120
+ if (Array.isArray(cn)) {
121
+ userGroups.push(...cn.map((v) => String(v)));
141
122
  }
142
- res.on('searchEntry', ({ object }) => {
143
- if (typeof object['cn'] === 'object') {
144
- userGroups = [...userGroups, ...object['cn']];
145
- }
146
- else if (object['cn']) {
147
- userGroups.push(object['cn']);
148
- }
149
- });
150
- res.on('error', (err) => {
151
- reject(handleError(err));
152
- });
153
- res.on('end', () => {
154
- resolve(userGroups);
155
- });
156
- });
157
- });
123
+ else if (cn) {
124
+ userGroups.push(String(cn));
125
+ }
126
+ }
127
+ return userGroups;
128
+ }
129
+ catch (err) {
130
+ throw handleError(err);
131
+ }
158
132
  }
159
133
  async fetchUserId(userDn) {
160
134
  const user = await this.knex
@@ -171,19 +145,15 @@ export class LDAPAuthDriver extends AuthDriver {
171
145
  const logger = useLogger();
172
146
  await this.validateBindClient();
173
147
  const { userDn, userScope, userAttribute, groupDn, groupScope, groupAttribute, defaultRoleId, syncUserInfo } = this.config;
174
- const userInfo = await this.fetchUserInfo(userDn, new ldap.EqualityFilter({
175
- attribute: userAttribute ?? 'cn',
176
- value: payload['identifier'],
177
- }), userScope ?? 'one');
148
+ const userInfo = await this.fetchUserInfo(userDn, `(${validateLDAPAttribute(userAttribute ?? 'cn')}=${escapeFilterValue(payload['identifier'])})`, userScope ?? 'one');
178
149
  if (!userInfo?.dn) {
179
150
  throw new InvalidCredentialsError();
180
151
  }
181
152
  let userRole;
182
153
  if (groupDn) {
183
- const userGroups = await this.fetchUserGroups(groupDn, new ldap.EqualityFilter({
184
- attribute: groupAttribute ?? 'member',
185
- value: groupAttribute?.toLowerCase() === 'memberuid' && userInfo.uid ? userInfo.uid : userInfo.dn,
186
- }), groupScope ?? 'one');
154
+ const groupAttr = groupAttribute ?? 'member';
155
+ const memberValue = groupAttr.toLowerCase() === 'memberuid' && userInfo.uid ? userInfo.uid : userInfo.dn;
156
+ const userGroups = await this.fetchUserGroups(groupDn, `(${validateLDAPAttribute(groupAttr)}=${escapeFilterValue(memberValue)})`, groupScope ?? 'one');
187
157
  if (userGroups.length) {
188
158
  userRole = await this.knex
189
159
  .select('id')
@@ -253,51 +223,86 @@ export class LDAPAuthDriver extends AuthDriver {
253
223
  if (!user.external_identifier || !password) {
254
224
  throw new InvalidCredentialsError();
255
225
  }
256
- return new Promise((resolve, reject) => {
257
- const clientConfig = typeof this.config['client'] === 'object' ? this.config['client'] : {};
258
- const client = ldap.createClient({
259
- url: this.config['clientUrl'],
260
- ...clientConfig,
261
- reconnect: false,
262
- });
263
- client.on('error', (err) => {
264
- reject(handleError(err));
265
- });
266
- client.bind(user.external_identifier, password, (err) => {
267
- if (err) {
268
- reject(handleError(err));
269
- }
270
- else {
271
- resolve();
272
- }
273
- client.destroy();
274
- });
226
+ const clientConfig = typeof this.config['client'] === 'object' ? this.config['client'] : {};
227
+ const client = new Client({
228
+ url: this.config['clientUrl'],
229
+ ...clientConfig,
275
230
  });
231
+ try {
232
+ await client.bind(user.external_identifier, password);
233
+ }
234
+ catch (err) {
235
+ throw handleError(err);
236
+ }
237
+ finally {
238
+ await client.unbind().catch(() => {
239
+ // Ignore unbind errors
240
+ });
241
+ }
276
242
  }
277
243
  async login(user, payload) {
278
244
  await this.verify(user, payload['password']);
279
245
  }
280
246
  async refresh(user) {
281
247
  await this.validateBindClient();
282
- const userInfo = await this.fetchUserInfo(user.external_identifier);
248
+ // Use scope 'base' to search the specific DN entry
249
+ const userInfo = await this.fetchUserInfo(user.external_identifier, undefined, 'base');
283
250
  if (userInfo?.userAccountControl && userInfo.userAccountControl & INVALID_ACCOUNT_FLAGS) {
284
251
  throw new InvalidCredentialsError();
285
252
  }
286
253
  }
287
254
  }
288
255
  const handleError = (e) => {
289
- if (e instanceof ldap.InappropriateAuthenticationError ||
290
- e instanceof ldap.InvalidCredentialsError ||
291
- e instanceof ldap.InsufficientAccessRightsError) {
256
+ if (e instanceof InappropriateAuthError ||
257
+ e instanceof LdapInvalidCredentialsError ||
258
+ e instanceof InsufficientAccessError) {
292
259
  return new InvalidCredentialsError();
293
260
  }
261
+ if (e instanceof Error) {
262
+ return new ServiceUnavailableError({
263
+ service: 'ldap',
264
+ reason: `Service returned unexpected error: ${e.message}`,
265
+ });
266
+ }
294
267
  return new ServiceUnavailableError({
295
268
  service: 'ldap',
296
- reason: `Service returned unexpected error: ${e.message}`,
269
+ reason: 'Service returned unexpected error',
297
270
  });
298
271
  };
299
272
  const getEntryValue = (value) => {
300
- return typeof value === 'object' ? value[0] : value;
273
+ if (value === undefined)
274
+ return undefined;
275
+ if (Buffer.isBuffer(value)) {
276
+ return value.toString();
277
+ }
278
+ if (Array.isArray(value)) {
279
+ const first = value[0];
280
+ if (Buffer.isBuffer(first)) {
281
+ return first.toString();
282
+ }
283
+ return first;
284
+ }
285
+ return value;
286
+ };
287
+ /**
288
+ * Escape special characters in LDAP filter values according to RFC 4515
289
+ */
290
+ const escapeFilterValue = (value) => {
291
+ return value
292
+ .replace(/\\/g, '\\5c')
293
+ .replace(/\*/g, '\\2a')
294
+ .replace(/\(/g, '\\28')
295
+ .replace(/\)/g, '\\29')
296
+ .replace(/\0/g, '\\00');
297
+ };
298
+ /**
299
+ * Validate LDAP attribute name according to RFC 4512
300
+ */
301
+ const validateLDAPAttribute = (name) => {
302
+ if (/^[a-zA-Z][a-zA-Z0-9-]*$/.test(name) === false) {
303
+ throw new Error(`Invalid LDAP attribute name: "${name}"`);
304
+ }
305
+ return name;
301
306
  };
302
307
  export function createLDAPAuthRouter(provider) {
303
308
  const router = Router();
@@ -1,9 +1,9 @@
1
+ import { performance } from 'perf_hooks';
1
2
  import { useEnv } from '@directus/env';
2
3
  import { InvalidCredentialsError, InvalidPayloadError } from '@directus/errors';
3
4
  import argon2 from 'argon2';
4
5
  import { Router } from 'express';
5
6
  import Joi from 'joi';
6
- import { performance } from 'perf_hooks';
7
7
  import { REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS } from '../../constants.js';
8
8
  import { respond } from '../../middleware/respond.js';
9
9
  import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
@@ -5,12 +5,11 @@ import type { RoleMap } from '../../types/rolemap.js';
5
5
  import { LocalAuthDriver } from './local.js';
6
6
  export declare class OAuth2AuthDriver extends LocalAuthDriver {
7
7
  client: Client;
8
- redirectUrl: string;
9
8
  config: Record<string, any>;
10
9
  roleMap: RoleMap;
11
10
  constructor(options: AuthDriverOptions, config: Record<string, any>);
12
11
  generateCodeVerifier(): string;
13
- generateAuthUrl(codeVerifier: string, prompt?: boolean): string;
12
+ generateAuthUrl(codeVerifier: string, prompt?: boolean, callbackUrl?: string): string;
14
13
  private fetchUserId;
15
14
  getUserID(payload: Record<string, any>): Promise<string>;
16
15
  login(user: User): Promise<void>;
@@ -16,40 +16,37 @@ import { AuthenticationService } from '../../services/authentication.js';
16
16
  import asyncHandler from '../../utils/async-handler.js';
17
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
18
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
19
+ import { getSchema } from '../../utils/get-schema.js';
19
20
  import { getSecret } from '../../utils/get-secret.js';
20
- import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
21
21
  import { verifyJWT } from '../../utils/jwt.js';
22
22
  import { Url } from '../../utils/url.js';
23
+ import { generateCallbackUrl } from '../utils/generate-callback-url.js';
24
+ import { isLoginRedirectAllowed } from '../utils/is-login-redirect-allowed.js';
23
25
  import { LocalAuthDriver } from './local.js';
24
- import { getSchema } from '../../utils/get-schema.js';
25
26
  export class OAuth2AuthDriver extends LocalAuthDriver {
26
27
  client;
27
- redirectUrl;
28
28
  config;
29
29
  roleMap;
30
30
  constructor(options, config) {
31
31
  super(options, config);
32
- const env = useEnv();
33
32
  const logger = useLogger();
34
33
  const { authorizeUrl, accessUrl, profileUrl, clientId, clientSecret, ...additionalConfig } = config;
35
34
  if (!authorizeUrl || !accessUrl || !profileUrl || !clientId || !clientSecret || !additionalConfig['provider']) {
36
35
  logger.error('Invalid provider config');
37
36
  throw new InvalidProviderConfigError({ provider: additionalConfig['provider'] });
38
37
  }
39
- const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', additionalConfig['provider'], 'callback');
40
- this.redirectUrl = redirectUrl.toString();
41
38
  this.config = additionalConfig;
42
39
  this.roleMap = {};
43
40
  const roleMapping = this.config['roleMapping'];
44
- if (roleMapping) {
45
- this.roleMap = roleMapping;
46
- }
47
41
  // role mapping will fail on login if AUTH_<provider>_ROLE_MAPPING is an array instead of an object.
48
42
  // This happens if the 'json:' prefix is missing from the variable declaration. To save the user from exhaustive debugging, we'll try to fail early here.
49
43
  if (roleMapping instanceof Array) {
50
44
  logger.error("[OAuth2] Expected a JSON-Object as role mapping, got an Array instead. Make sure you declare the variable with 'json:' prefix.");
51
45
  throw new InvalidProviderError();
52
46
  }
47
+ if (roleMapping) {
48
+ this.roleMap = roleMapping;
49
+ }
53
50
  const issuer = new Issuer({
54
51
  authorization_endpoint: authorizeUrl,
55
52
  token_endpoint: accessUrl,
@@ -67,7 +64,6 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
67
64
  this.client = new issuer.Client({
68
65
  client_id: clientId,
69
66
  client_secret: clientSecret,
70
- redirect_uris: [this.redirectUrl],
71
67
  response_types: ['code'],
72
68
  ...clientOptionsOverrides,
73
69
  });
@@ -75,7 +71,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
75
71
  generateCodeVerifier() {
76
72
  return generators.codeVerifier();
77
73
  }
78
- generateAuthUrl(codeVerifier, prompt = false) {
74
+ generateAuthUrl(codeVerifier, prompt = false, callbackUrl) {
79
75
  const { plainCodeChallenge } = this.config;
80
76
  try {
81
77
  const codeChallenge = plainCodeChallenge ? codeVerifier : generators.codeChallenge(codeVerifier);
@@ -89,6 +85,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
89
85
  code_challenge_method: plainCodeChallenge ? 'plain' : 'S256',
90
86
  // Some providers require state even with PKCE
91
87
  state: codeChallenge,
88
+ redirect_uri: callbackUrl,
92
89
  });
93
90
  }
94
91
  catch (e) {
@@ -116,7 +113,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
116
113
  const codeChallenge = plainCodeChallenge
117
114
  ? payload['codeVerifier']
118
115
  : generators.codeChallenge(payload['codeVerifier']);
119
- tokenSet = await this.client.oauthCallback(this.redirectUrl, { code: payload['code'], state: payload['state'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge });
116
+ tokenSet = await this.client.oauthCallback(payload['callbackUrl'], { code: payload['code'], state: payload['state'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge });
120
117
  userInfo = await this.client.userinfo(tokenSet.access_token);
121
118
  }
122
119
  catch (e) {
@@ -275,12 +272,19 @@ export function createOAuth2AuthRouter(providerName) {
275
272
  const provider = getAuthProvider(providerName);
276
273
  const codeVerifier = provider.generateCodeVerifier();
277
274
  const prompt = !!req.query['prompt'];
278
- const redirect = req.query['redirect'];
279
275
  const otp = req.query['otp'];
280
- if (isLoginRedirectAllowed(redirect, providerName) === false) {
276
+ const redirect = req.query['redirect'];
277
+ if (!isLoginRedirectAllowed(providerName, redirect)) {
281
278
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
282
279
  }
283
- const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
280
+ const callbackUrl = generateCallbackUrl(providerName, `${req.protocol}://${req.get('host')}`);
281
+ const token = jwt.sign({
282
+ verifier: codeVerifier,
283
+ redirect,
284
+ prompt,
285
+ otp,
286
+ callbackUrl,
287
+ }, getSecret(), {
284
288
  expiresIn: '5m',
285
289
  issuer: 'directus',
286
290
  });
@@ -288,7 +292,7 @@ export function createOAuth2AuthRouter(providerName) {
288
292
  httpOnly: true,
289
293
  sameSite: 'lax',
290
294
  });
291
- return res.redirect(provider.generateAuthUrl(codeVerifier, prompt));
295
+ return res.redirect(provider.generateAuthUrl(codeVerifier, prompt, callbackUrl));
292
296
  }, respond);
293
297
  router.post('/callback', express.urlencoded({ extended: false }), (req, res) => {
294
298
  res.redirect(303, `./callback?${new URLSearchParams(req.body)}`);
@@ -303,7 +307,7 @@ export function createOAuth2AuthRouter(providerName) {
303
307
  logger.warn(e, `[OAuth2] Couldn't verify OAuth2 cookie`);
304
308
  throw new InvalidCredentialsError();
305
309
  }
306
- const { verifier, prompt, otp } = tokenData;
310
+ const { verifier, prompt, otp, callbackUrl } = tokenData;
307
311
  let { redirect } = tokenData;
308
312
  const accountability = createDefaultAccountability({
309
313
  ip: getIPFromReq(req),
@@ -326,6 +330,7 @@ export function createOAuth2AuthRouter(providerName) {
326
330
  code: req.query['code'],
327
331
  codeVerifier: verifier,
328
332
  state: req.query['state'],
333
+ callbackUrl,
329
334
  }, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
330
335
  }
331
336
  catch (error) {
@@ -5,13 +5,12 @@ import type { RoleMap } from '../../types/rolemap.js';
5
5
  import { LocalAuthDriver } from './local.js';
6
6
  export declare class OpenIDAuthDriver extends LocalAuthDriver {
7
7
  client: null | Client;
8
- redirectUrl: string;
9
8
  config: Record<string, any>;
10
9
  roleMap: RoleMap;
11
10
  constructor(options: AuthDriverOptions, config: Record<string, any>);
12
11
  private getClient;
13
12
  generateCodeVerifier(): string;
14
- generateAuthUrl(codeVerifier: string, prompt?: boolean): Promise<string>;
13
+ generateAuthUrl(codeVerifier: string, prompt?: boolean, callbackUrl?: string): Promise<string>;
15
14
  private fetchUserId;
16
15
  getUserID(payload: Record<string, any>): Promise<string>;
17
16
  login(user: User): Promise<void>;
@@ -16,20 +16,19 @@ import { AuthenticationService } from '../../services/authentication.js';
16
16
  import asyncHandler from '../../utils/async-handler.js';
17
17
  import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
18
18
  import { getIPFromReq } from '../../utils/get-ip-from-req.js';
19
+ import { getSchema } from '../../utils/get-schema.js';
19
20
  import { getSecret } from '../../utils/get-secret.js';
20
- import { isLoginRedirectAllowed } from '../../utils/is-login-redirect-allowed.js';
21
21
  import { verifyJWT } from '../../utils/jwt.js';
22
22
  import { Url } from '../../utils/url.js';
23
+ import { generateCallbackUrl } from '../utils/generate-callback-url.js';
24
+ import { isLoginRedirectAllowed } from '../utils/is-login-redirect-allowed.js';
23
25
  import { LocalAuthDriver } from './local.js';
24
- import { getSchema } from '../../utils/get-schema.js';
25
26
  export class OpenIDAuthDriver extends LocalAuthDriver {
26
27
  client;
27
- redirectUrl;
28
28
  config;
29
29
  roleMap;
30
30
  constructor(options, config) {
31
31
  super(options, config);
32
- const env = useEnv();
33
32
  const logger = useLogger();
34
33
  const { issuerUrl, clientId, clientSecret, clientPrivateKeys, clientTokenEndpointAuthMethod, provider, issuerDiscoveryMustSucceed, } = config;
35
34
  const isPrivateKeyJwtAuthMethod = clientTokenEndpointAuthMethod === 'private_key_jwt';
@@ -37,8 +36,6 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
37
36
  logger.error('Invalid provider config');
38
37
  throw new InvalidProviderConfigError({ provider });
39
38
  }
40
- const redirectUrl = new Url(env['PUBLIC_URL']).addPath('auth', 'login', provider, 'callback');
41
- this.redirectUrl = redirectUrl.toString();
42
39
  this.config = config;
43
40
  this.roleMap = {};
44
41
  const roleMapping = this.config['roleMapping'];
@@ -98,7 +95,6 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
98
95
  const client = new issuer.Client({
99
96
  client_id: clientId,
100
97
  ...(!isPrivateKeyJwtAuthMethod && { client_secret: clientSecret }),
101
- redirect_uris: [this.redirectUrl],
102
98
  response_types: ['code'],
103
99
  ...clientOptionsOverrides,
104
100
  }, isPrivateKeyJwtAuthMethod ? { keys: clientPrivateKeys } : undefined);
@@ -116,7 +112,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
116
112
  generateCodeVerifier() {
117
113
  return generators.codeVerifier();
118
114
  }
119
- async generateAuthUrl(codeVerifier, prompt = false) {
115
+ async generateAuthUrl(codeVerifier, prompt = false, callbackUrl) {
120
116
  const { plainCodeChallenge } = this.config;
121
117
  try {
122
118
  const client = await this.getClient();
@@ -132,6 +128,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
132
128
  // Some providers require state even with PKCE
133
129
  state: codeChallenge,
134
130
  nonce: codeChallenge,
131
+ redirect_uri: callbackUrl,
135
132
  });
136
133
  }
137
134
  catch (e) {
@@ -160,7 +157,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
160
157
  const codeChallenge = plainCodeChallenge
161
158
  ? payload['codeVerifier']
162
159
  : generators.codeChallenge(payload['codeVerifier']);
163
- tokenSet = await client.callback(this.redirectUrl, { code: payload['code'], state: payload['state'], iss: payload['iss'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge, nonce: codeChallenge });
160
+ tokenSet = await client.callback(payload['callbackUrl'], { code: payload['code'], state: payload['state'], iss: payload['iss'] }, { code_verifier: payload['codeVerifier'], state: codeChallenge, nonce: codeChallenge });
164
161
  userInfo = tokenSet.claims();
165
162
  if (client.issuer.metadata['userinfo_endpoint']) {
166
163
  userInfo = {
@@ -329,10 +326,17 @@ export function createOpenIDAuthRouter(providerName) {
329
326
  const prompt = !!req.query['prompt'];
330
327
  const redirect = req.query['redirect'];
331
328
  const otp = req.query['otp'];
332
- if (isLoginRedirectAllowed(redirect, providerName) === false) {
329
+ if (!isLoginRedirectAllowed(providerName, redirect)) {
333
330
  throw new InvalidPayloadError({ reason: `URL "${redirect}" can't be used to redirect after login` });
334
331
  }
335
- const token = jwt.sign({ verifier: codeVerifier, redirect, prompt, otp }, getSecret(), {
332
+ const callbackUrl = generateCallbackUrl(providerName, `${req.protocol}://${req.get('host')}`);
333
+ const token = jwt.sign({
334
+ verifier: codeVerifier,
335
+ redirect,
336
+ prompt,
337
+ otp,
338
+ callbackUrl,
339
+ }, getSecret(), {
336
340
  expiresIn: (env[`AUTH_${providerName.toUpperCase()}_LOGIN_TIMEOUT`] ?? '5m'),
337
341
  issuer: 'directus',
338
342
  });
@@ -341,7 +345,7 @@ export function createOpenIDAuthRouter(providerName) {
341
345
  sameSite: 'lax',
342
346
  });
343
347
  try {
344
- return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt));
348
+ return res.redirect(await provider.generateAuthUrl(codeVerifier, prompt, callbackUrl));
345
349
  }
346
350
  catch {
347
351
  return res.redirect(new Url(env['PUBLIC_URL'])
@@ -365,7 +369,7 @@ export function createOpenIDAuthRouter(providerName) {
365
369
  const url = new Url(env['PUBLIC_URL']).addPath('admin', 'login');
366
370
  return res.redirect(`${url.toString()}?reason=${ErrorCode.InvalidCredentials}`);
367
371
  }
368
- const { verifier, prompt, otp } = tokenData;
372
+ const { verifier, prompt, otp, callbackUrl } = tokenData;
369
373
  let { redirect } = tokenData;
370
374
  const accountability = createDefaultAccountability({ ip: getIPFromReq(req) });
371
375
  const userAgent = req.get('user-agent')?.substring(0, 1024);
@@ -387,6 +391,7 @@ export function createOpenIDAuthRouter(providerName) {
387
391
  codeVerifier: verifier,
388
392
  state: req.query['state'],
389
393
  iss: req.query['iss'],
394
+ callbackUrl,
390
395
  }, { session: authMode === 'session', ...(otp ? { otp: String(otp) } : {}) });
391
396
  }
392
397
  catch (error) {