diff --git a/.changeset/brave-walls-scream.md b/.changeset/brave-walls-scream.md new file mode 100644 index 0000000000..fc70802894 --- /dev/null +++ b/.changeset/brave-walls-scream.md @@ -0,0 +1,5 @@ +--- +"@directus/api": patch +--- + +Added WebSocket Session Authentication diff --git a/api/package.json b/api/package.json index 4664f0986d..4e0b4b04d6 100644 --- a/api/package.json +++ b/api/package.json @@ -92,6 +92,7 @@ "@rollup/plugin-alias": "5.1.0", "@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-virtual": "3.0.2", + "@types/cookie": "0.6.0", "argon2": "0.40.1", "async": "3.2.5", "axios": "1.6.7", @@ -102,6 +103,7 @@ "chokidar": "3.6.0", "commander": "12.0.0", "content-disposition": "0.5.4", + "cookie": "0.6.0", "cookie-parser": "1.4.6", "cors": "2.8.5", "cron-parser": "4.9.0", diff --git a/api/src/websocket/controllers/base.ts b/api/src/websocket/controllers/base.ts index 5fbdf14f09..3798549fdc 100644 --- a/api/src/websocket/controllers/base.ts +++ b/api/src/websocket/controllers/base.ts @@ -4,7 +4,6 @@ import type { Accountability } from '@directus/types'; import { parseJSON, toBoolean } from '@directus/utils'; import type { IncomingMessage, Server as httpServer } from 'http'; import { randomUUID } from 'node:crypto'; -import type { ParsedUrlQuery } from 'querystring'; import type { RateLimiterAbstract } from 'rate-limiter-flexible'; import type internal from 'stream'; import { parse } from 'url'; @@ -22,6 +21,7 @@ import { getExpiresAtForToken } from '../utils/get-expires-at-for-token.js'; import { getMessageType } from '../utils/message.js'; import { waitForAnyMessage, waitForMessageType } from '../utils/wait-for-message.js'; import { registerWebSocketEvents } from './hooks.js'; +import cookie from 'cookie'; const TOKEN_CHECK_INTERVAL = 15 * 60 * 1000; // 15 minutes @@ -132,10 +132,20 @@ export default abstract class SocketController { return; } + const env = useEnv(); + const cookies = request.headers.cookie ? cookie.parse(request.headers.cookie) : {}; const context: UpgradeContext = { request, socket, head }; + const sessionCookieName = env['SESSION_COOKIE_NAME'] as string; + + if (cookies[sessionCookieName]) { + const token = cookies[sessionCookieName] as string; + await this.handleTokenUpgrade(context, token); + return; + } if (this.authentication.mode === 'strict') { - await this.handleStrictUpgrade(context, query); + const token = query['access_token'] as string; + await this.handleTokenUpgrade(context, token); return; } @@ -151,11 +161,10 @@ export default abstract class SocketController { }); } - protected async handleStrictUpgrade({ request, socket, head }: UpgradeContext, query: ParsedUrlQuery) { + protected async handleTokenUpgrade({ request, socket, head }: UpgradeContext, token: string) { let accountability: Accountability | null, expires_at: number | null; try { - const token = query['access_token'] as string; accountability = await getAccountabilityForToken(token); expires_at = getExpiresAtForToken(token); } catch { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1625133d14..9e8b0e6a9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: '@rollup/plugin-virtual': specifier: 3.0.2 version: 3.0.2(rollup@4.12.0) + '@types/cookie': + specifier: 0.6.0 + version: 0.6.0 argon2: specifier: 0.40.1 version: 0.40.1 @@ -149,6 +152,9 @@ importers: content-disposition: specifier: 0.5.4 version: 0.5.4 + cookie: + specifier: 0.6.0 + version: 0.6.0 cookie-parser: specifier: 1.4.6 version: 1.4.6 @@ -7962,6 +7968,10 @@ packages: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: false + /@types/cookiejar@2.1.5: resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} dev: true @@ -10622,6 +10632,11 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + /cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true @@ -18575,6 +18590,9 @@ packages: /sqlite3@5.1.7: resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} requiresBuild: true + peerDependenciesMeta: + node-gyp: + optional: true dependencies: bindings: 1.5.0 node-addon-api: 7.0.0