@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.
- package/dist/ai/chat/controllers/chat.post.js +19 -4
- package/dist/ai/chat/lib/create-ui-stream.d.ts +8 -7
- package/dist/ai/chat/lib/create-ui-stream.js +28 -25
- package/dist/ai/chat/middleware/load-settings.js +31 -7
- package/dist/ai/chat/models/chat-request.d.ts +135 -2
- package/dist/ai/chat/models/chat-request.js +56 -2
- package/dist/ai/chat/models/providers.d.ts +16 -2
- package/dist/ai/chat/models/providers.js +16 -2
- package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +3 -4
- package/dist/ai/chat/utils/format-context.d.ts +5 -0
- package/dist/ai/chat/utils/format-context.js +122 -0
- package/dist/ai/mcp/server.d.ts +27 -1
- package/dist/ai/providers/index.d.ts +3 -0
- package/dist/ai/providers/index.js +3 -0
- package/dist/ai/providers/options.d.ts +14 -0
- package/dist/ai/providers/options.js +26 -0
- package/dist/ai/providers/registry.d.ts +6 -0
- package/dist/ai/providers/registry.js +65 -0
- package/dist/ai/providers/types.d.ts +34 -0
- package/dist/ai/providers/types.js +1 -0
- package/dist/ai/tools/assets/index.js +1 -1
- package/dist/ai/tools/collections/index.js +2 -2
- package/dist/ai/tools/fields/index.js +2 -2
- package/dist/ai/tools/files/index.js +1 -1
- package/dist/ai/tools/flows/index.js +1 -1
- package/dist/ai/tools/folders/index.js +1 -1
- package/dist/ai/tools/items/index.js +6 -3
- package/dist/ai/tools/items/prompt.md +7 -9
- package/dist/ai/tools/relations/index.js +1 -1
- package/dist/ai/tools/schema.js +1 -1
- package/dist/ai/tools/trigger-flow/index.js +1 -1
- package/dist/app.js +12 -8
- package/dist/auth/drivers/ldap.d.ts +1 -1
- package/dist/auth/drivers/ldap.js +144 -139
- package/dist/auth/drivers/local.js +1 -1
- package/dist/auth/drivers/oauth2.d.ts +1 -2
- package/dist/auth/drivers/oauth2.js +22 -17
- package/dist/auth/drivers/openid.d.ts +1 -2
- package/dist/auth/drivers/openid.js +18 -13
- package/dist/auth/drivers/saml.js +3 -3
- package/dist/auth/utils/generate-callback-url.d.ts +11 -0
- package/dist/auth/utils/generate-callback-url.js +40 -0
- package/dist/auth/utils/is-login-redirect-allowed.d.ts +7 -0
- package/dist/{utils → auth/utils}/is-login-redirect-allowed.js +12 -9
- package/dist/cache.d.ts +12 -0
- package/dist/cache.js +27 -3
- package/dist/cli/commands/bootstrap/index.js +2 -2
- package/dist/cli/commands/database/install.js +1 -1
- package/dist/cli/commands/database/migrate.js +1 -1
- package/dist/cli/commands/init/index.js +2 -2
- package/dist/cli/commands/roles/create.js +4 -4
- package/dist/cli/commands/schema/apply.js +3 -3
- package/dist/cli/commands/schema/snapshot.js +1 -1
- package/dist/cli/utils/create-db-connection.d.ts +1 -1
- package/dist/cli/utils/create-db-connection.js +1 -1
- package/dist/cli/utils/create-env/env-stub.liquid +3 -0
- package/dist/cli/utils/create-env/index.js +1 -1
- package/dist/constants.d.ts +7 -3
- package/dist/constants.js +7 -3
- package/dist/controllers/access.js +1 -1
- package/dist/controllers/assets.js +1 -1
- package/dist/controllers/deployment.js +481 -0
- package/dist/controllers/extensions.js +1 -1
- package/dist/controllers/fields.js +8 -6
- package/dist/controllers/files.js +1 -1
- package/dist/controllers/items.js +1 -1
- package/dist/controllers/not-found.js +1 -1
- package/dist/controllers/relations.js +1 -1
- package/dist/database/errors/dialects/mysql.d.ts +1 -1
- package/dist/database/errors/dialects/postgres.d.ts +1 -1
- package/dist/database/errors/dialects/sqlite.d.ts +1 -1
- package/dist/database/errors/translate.d.ts +1 -1
- package/dist/database/errors/translate.js +1 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +2 -2
- package/dist/database/helpers/date/dialects/mssql.js +1 -1
- package/dist/database/helpers/date/dialects/mysql.js +1 -1
- package/dist/database/helpers/date/types.js +1 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.js +24 -1
- package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +16 -3
- package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
- package/dist/database/helpers/schema/types.d.ts +13 -0
- package/dist/database/helpers/schema/types.js +24 -0
- package/dist/database/index.js +4 -4
- package/dist/database/migrations/20220429A-add-flows.js +1 -1
- package/dist/database/migrations/20230526A-migrate-translation-strings.js +1 -1
- package/dist/database/migrations/20231009A-update-csv-fields-to-text.js +1 -1
- package/dist/database/migrations/20240204A-marketplace.js +9 -7
- package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +15 -0
- package/dist/database/migrations/20240311A-deprecate-webhooks.js +1 -1
- package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
- package/dist/database/migrations/20240924A-migrate-legacy-comments.js +1 -1
- package/dist/database/migrations/20251014A-add-project-owner.js +1 -1
- package/dist/database/migrations/20251224A-remove-webhooks.d.ts +3 -0
- package/dist/database/migrations/20251224A-remove-webhooks.js +19 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.d.ts +3 -0
- package/dist/database/migrations/20260110A-add-ai-provider-settings.js +35 -0
- package/dist/database/migrations/20260113A-add-revisions-index.d.ts +3 -0
- package/dist/database/migrations/20260113A-add-revisions-index.js +41 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.d.ts +3 -0
- package/dist/database/migrations/20260128A-add-collaborative-editing.js +10 -0
- package/dist/database/migrations/20260204A-add-deployment.d.ts +3 -0
- package/dist/database/migrations/20260204A-add-deployment.js +32 -0
- package/dist/database/migrations/run.js +3 -3
- package/dist/database/run-ast/lib/apply-query/add-join.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/index.js +1 -1
- package/dist/database/run-ast/lib/apply-query/filter/operator.js +1 -1
- package/dist/database/run-ast/lib/apply-query/sort.js +1 -1
- package/dist/database/run-ast/utils/get-column-pre-processor.js +2 -2
- package/dist/database/run-ast/utils/get-column.js +1 -1
- package/dist/database/seeds/run.js +3 -3
- package/dist/deployment/deployment.d.ts +94 -0
- package/dist/deployment/deployment.js +29 -0
- package/dist/deployment/drivers/index.d.ts +1 -0
- package/dist/deployment/drivers/index.js +1 -0
- package/dist/deployment/drivers/vercel.d.ts +32 -0
- package/dist/deployment/drivers/vercel.js +208 -0
- package/dist/deployment/index.d.ts +2 -0
- package/dist/deployment/index.js +2 -0
- package/dist/deployment.d.ts +24 -0
- package/dist/deployment.js +39 -0
- package/dist/extensions/lib/get-extensions-path.js +1 -1
- package/dist/extensions/lib/get-extensions-settings.js +1 -1
- package/dist/extensions/lib/get-extensions.js +1 -1
- package/dist/extensions/lib/get-shared-deps-mapping.js +3 -3
- package/dist/extensions/lib/installation/manager.js +3 -3
- package/dist/extensions/lib/sandbox/register/route.d.ts +1 -1
- package/dist/extensions/lib/sync/status.js +1 -1
- package/dist/extensions/lib/sync/sync.js +7 -7
- package/dist/extensions/lib/sync/utils.js +2 -2
- package/dist/extensions/manager.d.ts +1 -1
- package/dist/extensions/manager.js +8 -8
- package/dist/flows.d.ts +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/logger/logs-stream.d.ts +1 -1
- package/dist/logger/logs-stream.js +1 -1
- package/dist/mailer.js +1 -1
- package/dist/metrics/lib/create-metrics.js +2 -2
- package/dist/middleware/authenticate.js +3 -3
- package/dist/middleware/collection-exists.js +1 -1
- package/dist/middleware/extract-token.js +1 -1
- package/dist/middleware/graphql.js +2 -2
- package/dist/middleware/respond.js +27 -14
- package/dist/middleware/validate-batch.js +1 -1
- package/dist/operations/exec/index.js +2 -1
- package/dist/operations/mail/index.js +1 -1
- package/dist/operations/mail/rate-limiter.js +2 -2
- package/dist/permissions/cache.js +5 -0
- package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +1 -1
- package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +1 -1
- package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +2 -2
- package/dist/permissions/modules/process-ast/lib/inject-cases.js +1 -1
- package/dist/permissions/modules/process-ast/process-ast.js +1 -1
- package/dist/permissions/modules/process-ast/utils/find-related-collection.js +1 -1
- package/dist/permissions/modules/process-payload/process-payload.js +1 -1
- package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +14 -2
- package/dist/permissions/modules/validate-access/lib/validate-item-access.js +72 -13
- package/dist/permissions/modules/validate-access/validate-access.js +3 -2
- package/dist/rate-limiter.js +1 -1
- package/dist/request/is-denied-ip.js +1 -1
- package/dist/schedules/project.js +1 -1
- package/dist/schedules/telemetry.js +1 -1
- package/dist/schedules/tus.js +1 -1
- package/dist/server.js +6 -5
- package/dist/services/assets.d.ts +2 -1
- package/dist/services/assets.js +35 -8
- package/dist/services/authentication.js +2 -2
- package/dist/services/collections.js +1 -1
- package/dist/services/deployment-projects.d.ts +20 -0
- package/dist/services/deployment-projects.js +34 -0
- package/dist/services/deployment-runs.d.ts +13 -0
- package/dist/services/deployment-runs.js +6 -0
- package/dist/services/deployment.d.ts +40 -0
- package/dist/services/deployment.js +202 -0
- package/dist/services/extensions.d.ts +1 -1
- package/dist/services/files/utils/get-metadata.d.ts +1 -1
- package/dist/services/files/utils/get-metadata.js +1 -1
- package/dist/services/files.d.ts +1 -1
- package/dist/services/files.js +4 -4
- package/dist/services/graphql/index.d.ts +1 -1
- package/dist/services/graphql/index.js +1 -1
- package/dist/services/graphql/resolvers/mutation.js +1 -1
- package/dist/services/graphql/resolvers/system-admin.js +2 -3
- package/dist/services/graphql/schema/get-types.d.ts +1 -1
- package/dist/services/graphql/schema/read.js +1 -1
- package/dist/services/graphql/subscription.d.ts +1 -1
- package/dist/services/graphql/types/date.js +1 -1
- package/dist/services/graphql/types/hash.js +1 -1
- package/dist/services/graphql/utils/add-path-to-validation-error.js +1 -1
- package/dist/services/graphql/utils/filter-replace-m2a.js +3 -4
- package/dist/services/import-export.d.ts +1 -1
- package/dist/services/import-export.js +2 -2
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.js +3 -1
- package/dist/services/mail/index.js +2 -2
- package/dist/services/mail/rate-limiter.js +2 -2
- package/dist/services/payload.js +2 -2
- package/dist/services/schema.js +1 -1
- package/dist/services/server.js +13 -4
- package/dist/services/settings.js +2 -2
- package/dist/services/specifications.js +2 -2
- package/dist/services/tfa.js +1 -1
- package/dist/services/translations.js +1 -1
- package/dist/services/tus/data-store.d.ts +1 -3
- package/dist/services/tus/data-store.js +2 -5
- package/dist/services/tus/server.js +6 -6
- package/dist/services/users.js +4 -4
- package/dist/services/versions.js +1 -1
- package/dist/telemetry/lib/get-report.js +2 -0
- package/dist/telemetry/lib/send-report.d.ts +1 -1
- package/dist/telemetry/lib/send-report.js +1 -1
- package/dist/telemetry/lib/track.js +1 -1
- package/dist/telemetry/types/report.d.ts +8 -0
- package/dist/telemetry/utils/get-settings.d.ts +2 -0
- package/dist/telemetry/utils/get-settings.js +5 -0
- package/dist/test-utils/knex.js +1 -1
- package/dist/types/collection.d.ts +1 -1
- package/dist/utils/async-handler.d.ts +1 -1
- package/dist/utils/calculate-field-depth.js +1 -1
- package/dist/utils/compress.js +1 -1
- package/dist/utils/deep-map-response.d.ts +1 -1
- package/dist/utils/deep-map-response.js +2 -2
- package/dist/utils/get-cache-key.js +1 -1
- package/dist/utils/get-column-path.js +1 -1
- package/dist/utils/get-field-system-rows.js +1 -1
- package/dist/utils/get-ip-from-req.d.ts +1 -1
- package/dist/utils/get-ip-from-req.js +1 -1
- package/dist/utils/get-local-type.js +7 -3
- package/dist/utils/get-service.js +7 -3
- package/dist/utils/get-snapshot-diff.js +1 -1
- package/dist/utils/is-field-allowed.d.ts +4 -0
- package/dist/utils/is-field-allowed.js +9 -0
- package/dist/utils/is-url-allowed.js +1 -1
- package/dist/utils/jwt.js +1 -1
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/utils/should-clear-cache.d.ts +1 -1
- package/dist/utils/should-skip-cache.js +2 -2
- package/dist/utils/validate-diff.js +1 -1
- package/dist/utils/validate-snapshot.js +3 -3
- package/dist/utils/validate-storage.js +2 -2
- package/dist/utils/verify-session-jwt.js +1 -1
- package/dist/utils/versioning/handle-version.js +1 -1
- package/dist/websocket/collab/calculate-cache-metadata.d.ts +9 -0
- package/dist/websocket/collab/calculate-cache-metadata.js +121 -0
- package/dist/websocket/collab/collab.d.ts +63 -0
- package/dist/websocket/collab/collab.js +481 -0
- package/dist/websocket/collab/constants.d.ts +1 -0
- package/dist/websocket/collab/constants.js +13 -0
- package/dist/websocket/collab/filter-to-fields.d.ts +2 -0
- package/dist/websocket/collab/filter-to-fields.js +11 -0
- package/dist/websocket/collab/messenger.d.ts +43 -0
- package/dist/websocket/collab/messenger.js +225 -0
- package/dist/websocket/collab/payload-permissions.d.ts +18 -0
- package/dist/websocket/collab/payload-permissions.js +158 -0
- package/dist/websocket/collab/permissions-cache.d.ts +52 -0
- package/dist/websocket/collab/permissions-cache.js +204 -0
- package/dist/websocket/collab/room.d.ts +125 -0
- package/dist/websocket/collab/room.js +593 -0
- package/dist/websocket/collab/store.d.ts +7 -0
- package/dist/websocket/collab/store.js +33 -0
- package/dist/websocket/collab/types.d.ts +21 -0
- package/dist/websocket/collab/types.js +1 -0
- package/dist/websocket/collab/verify-permissions.d.ts +11 -0
- package/dist/websocket/collab/verify-permissions.js +100 -0
- package/dist/websocket/controllers/base.d.ts +2 -2
- package/dist/websocket/controllers/base.js +3 -3
- package/dist/websocket/controllers/graphql.d.ts +1 -1
- package/dist/websocket/controllers/graphql.js +1 -1
- package/dist/websocket/controllers/logs.d.ts +1 -1
- package/dist/websocket/controllers/rest.d.ts +1 -1
- package/dist/websocket/controllers/rest.js +2 -2
- package/dist/websocket/handlers/heartbeat.js +1 -1
- package/dist/websocket/handlers/index.d.ts +2 -0
- package/dist/websocket/handlers/index.js +9 -0
- package/dist/websocket/handlers/items.js +2 -2
- package/dist/websocket/handlers/subscribe.js +1 -1
- package/dist/websocket/types.d.ts +1 -1
- package/dist/websocket/utils/items.d.ts +2 -2
- package/dist/websocket/utils/message.d.ts +1 -1
- package/dist/websocket/utils/message.js +2 -2
- package/dist/websocket/utils/wait-for-message.js +1 -1
- package/package.json +35 -33
- package/dist/controllers/webhooks.js +0 -74
- package/dist/services/webhooks.d.ts +0 -14
- package/dist/services/webhooks.js +0 -32
- package/dist/utils/get-relation-info.d.ts +0 -6
- package/dist/utils/get-relation-info.js +0 -43
- package/dist/utils/get-relation-type.d.ts +0 -6
- package/dist/utils/get-relation-type.js +0 -18
- package/dist/utils/is-login-redirect-allowed.d.ts +0 -4
- package/dist/utils/versioning/deep-map-with-schema.d.ts +0 -23
- package/dist/utils/versioning/deep-map-with-schema.js +0 -81
- /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,
|
|
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
|
|
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 =
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
47
|
-
//
|
|
48
|
-
this.bindClient.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
this.bindClient.search(baseDn, {
|
|
93
|
-
filter,
|
|
94
|
-
scope,
|
|
72
|
+
try {
|
|
73
|
+
const searchOptions = {
|
|
95
74
|
attributes: ['uid', firstNameAttribute, lastNameAttribute, mailAttribute, 'userAccountControl'],
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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,
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
|
290
|
-
e instanceof
|
|
291
|
-
e instanceof
|
|
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:
|
|
269
|
+
reason: 'Service returned unexpected error',
|
|
297
270
|
});
|
|
298
271
|
};
|
|
299
272
|
const getEntryValue = (value) => {
|
|
300
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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) {
|