mirror of
https://github.com/directus/directus.git
synced 2026-01-10 06:38:12 -05:00
Fixed websocket authentication handling between websocket controllers (#23691)
* Fixed authentication handling between websocket controllers * catch the error for a token upgrade * Create wicked-ligers-lay.md
This commit is contained in:
5
.changeset/wicked-ligers-lay.md
Normal file
5
.changeset/wicked-ligers-lay.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@directus/api": patch
|
||||
---
|
||||
|
||||
Fixed authentication handling between logs streaming and regular websocket connections
|
||||
@@ -83,7 +83,6 @@ export default abstract class SocketController {
|
||||
authentication: {
|
||||
mode: authMode.data,
|
||||
timeout: authTimeout,
|
||||
requireAdmin: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -176,7 +175,14 @@ export default abstract class SocketController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.meetsAdminRequirement({ socket, accountability })) return;
|
||||
try {
|
||||
this.checkUserRequirements(accountability);
|
||||
} catch {
|
||||
logger.debug('WebSocket upgrade denied - ' + JSON.stringify(accountability || 'invalid'));
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
||||
this.catchInvalidMessages(ws);
|
||||
@@ -195,9 +201,10 @@ export default abstract class SocketController {
|
||||
|
||||
const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
|
||||
|
||||
if (this.meetsAdminRequirement({ client: ws, accountability: state.accountability })) return;
|
||||
this.checkUserRequirements(state.accountability);
|
||||
|
||||
ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
|
||||
|
||||
this.server.emit('connection', ws, state);
|
||||
} catch {
|
||||
logger.debug('WebSocket authentication handshake failed');
|
||||
@@ -306,8 +313,7 @@ export default abstract class SocketController {
|
||||
protected async handleAuthRequest(client: WebSocketClient, message: WebSocketAuthMessage) {
|
||||
try {
|
||||
const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
|
||||
|
||||
if (!this.meetsAdminRequirement({ client, accountability })) return;
|
||||
this.checkUserRequirements(accountability);
|
||||
|
||||
client.accountability = accountability;
|
||||
client.expires_at = expires_at;
|
||||
@@ -335,6 +341,11 @@ export default abstract class SocketController {
|
||||
}
|
||||
}
|
||||
|
||||
protected checkUserRequirements(_accountability: Accountability | null) {
|
||||
// there are no requirements in the abstract class
|
||||
return;
|
||||
}
|
||||
|
||||
setTokenExpireTimer(client: WebSocketClient) {
|
||||
if (client.auth_timer !== null) {
|
||||
// clear up old timeouts if needed
|
||||
@@ -375,30 +386,6 @@ export default abstract class SocketController {
|
||||
}, TOKEN_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
meetsAdminRequirement({
|
||||
socket,
|
||||
client,
|
||||
accountability,
|
||||
}: {
|
||||
socket?: UpgradeContext['socket'];
|
||||
client?: WebSocketClient | WebSocket;
|
||||
accountability: Accountability | null;
|
||||
}) {
|
||||
if (!this.authentication.requireAdmin || accountability?.admin) return true;
|
||||
|
||||
logger.debug('WebSocket connection denied - ' + JSON.stringify(accountability || 'invalid'));
|
||||
|
||||
if (socket) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
} else if (client) {
|
||||
handleWebSocketError(client, new WebSocketError('auth', 'UNAUTHORIZED', 'Unauthorized.'), 'auth');
|
||||
client.close();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
terminate() {
|
||||
if (this.authInterval) clearInterval(this.authInterval);
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ import type { Server as httpServer } from 'http';
|
||||
import type WebSocket from 'ws';
|
||||
import emitter from '../../emitter.js';
|
||||
import { useLogger } from '../../logger/index.js';
|
||||
import { handleWebSocketError } from '../errors.js';
|
||||
import { handleWebSocketError, WebSocketError } from '../errors.js';
|
||||
import { AuthMode, WebSocketMessage } from '../messages.js';
|
||||
import type { AuthenticationState, WebSocketClient } from '../types.js';
|
||||
import SocketController from './base.js';
|
||||
import type { Accountability } from '@directus/types';
|
||||
|
||||
const logger = useLogger();
|
||||
|
||||
@@ -34,11 +35,9 @@ export class LogsController extends SocketController {
|
||||
return {
|
||||
endpoint,
|
||||
maxConnections,
|
||||
// require strict auth
|
||||
authentication: {
|
||||
mode: 'strict' as AuthMode,
|
||||
timeout: 0,
|
||||
requireAdmin: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -63,4 +62,11 @@ export class LogsController extends SocketController {
|
||||
|
||||
emitter.emitAction('websocket.connect', { client });
|
||||
}
|
||||
|
||||
protected override checkUserRequirements(accountability: Accountability | null) {
|
||||
// enforce admin only access for the logs streaming websocket
|
||||
if (!accountability?.admin) {
|
||||
throw new WebSocketError('auth', 'AUTH_FAILED', 'Unauthorized access.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ export type UpgradeRequest = IncomingMessage & AuthenticationState;
|
||||
export type WebSocketAuthentication = {
|
||||
mode: AuthMode;
|
||||
timeout: number;
|
||||
requireAdmin: boolean;
|
||||
};
|
||||
|
||||
export type SubscriptionEvent = 'create' | 'update' | 'delete';
|
||||
|
||||
Reference in New Issue
Block a user