mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
fix: Distributed API auth hardening — Bearer tokens and URL normalization
- Extend WS upgrade handler to accept Authorization: Bearer tokens as a fallback to cookie auth. Remote Sencho instances receive proxied WS connections carrying Bearer (no cookie), so the previous cookie-only check caused immediate 401 rejections for all proxied log/terminal streams. - Log token validation failures in authMiddleware (was silently swallowed, violating no-empty-catch directive). - Normalize api_url by stripping trailing slashes in testRemoteConnection, the HTTP proxy target, and the WS proxy target to prevent double-slash URLs.
This commit is contained in:
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
- **Fixed:** Remote node authentication failures by updating middleware to support Bearer tokens in WebSocket upgrade handler (node-to-node WS proxy now authenticates correctly on the receiving instance).
|
||||
- **Fixed:** Node connection testing logic updated to normalize `api_url` trailing slashes before constructing the authenticated HTTP ping URL.
|
||||
- **Fixed:** Memory leak in `GlobalObservabilityView` SSE mode — log array now capped at 10,000 entries (`.slice(-10000)`) to prevent unbounded accumulation across long sessions.
|
||||
- **Fixed:** Infinite re-fetch loop in `NodeContext` — `refreshNodes` useCallback no longer depends on `activeNode` state; replaced with a `useRef` to read current node inside the callback without being a reactive dependency.
|
||||
- **Fixed:** Infinite page reload loop — `apiFetch` was calling `window.location.href = '/'` on every 401, causing a full browser reload before auth could complete. Replaced with a `sencho-unauthorized` custom event that `AuthContext` handles by setting `appStatus` to `notAuthenticated`.
|
||||
|
||||
@@ -123,7 +123,8 @@ const authMiddleware = async (req: Request, res: Response, next: NextFunction):
|
||||
// Accept both user sessions and node proxy tokens
|
||||
req.user = { username: decoded.username || 'node-proxy' };
|
||||
next();
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.error('[Auth] Token validation failed:', (err as Error).message);
|
||||
res.status(401).json({ error: 'Invalid or expired token' });
|
||||
return;
|
||||
}
|
||||
@@ -327,7 +328,7 @@ app.use('/api/', (req: Request, res: Response, next: NextFunction): void => {
|
||||
}
|
||||
|
||||
createProxyMiddleware<Request, Response>({
|
||||
target: node.api_url,
|
||||
target: node.api_url.replace(/\/$/, ''),
|
||||
changeOrigin: true,
|
||||
on: {
|
||||
proxyReq: (proxyReq) => {
|
||||
@@ -362,7 +363,11 @@ server.on('upgrade', async (req, socket, head) => {
|
||||
cookieHeader.split(';').map(c => c.trim().split('=')).filter(([k, v]) => k && v)
|
||||
);
|
||||
|
||||
const token = cookies[COOKIE_NAME];
|
||||
// Accept either cookie auth (browser sessions) or Bearer token auth (node-to-node WS proxy)
|
||||
const cookieToken = cookies[COOKIE_NAME];
|
||||
const authHeader = req.headers['authorization'] as string | undefined;
|
||||
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
const token = cookieToken || bearerToken;
|
||||
|
||||
if (!token) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
@@ -387,7 +392,7 @@ server.on('upgrade', async (req, socket, head) => {
|
||||
|
||||
// Remote Node WebSocket Proxy — forward the entire WS connection to the remote Sencho instance
|
||||
if (node && node.type === 'remote' && node.api_url && node.api_token) {
|
||||
const wsTarget = node.api_url.replace(/^https?/, (m) => m === 'https' ? 'wss' : 'ws');
|
||||
const wsTarget = node.api_url.replace(/\/$/, '').replace(/^https?/, (m) => m === 'https' ? 'wss' : 'ws');
|
||||
req.headers['authorization'] = `Bearer ${node.api_token}`;
|
||||
delete req.headers['x-node-id'];
|
||||
wsProxyServer.ws(req, socket, head, { target: wsTarget });
|
||||
|
||||
@@ -154,7 +154,8 @@ export class NodeRegistry {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${node.api_url}/api/auth/check`, {
|
||||
const baseUrl = node.api_url.replace(/\/$/, '');
|
||||
const response = await axios.get(`${baseUrl}/api/auth/check`, {
|
||||
headers: { Authorization: `Bearer ${node.api_token}` },
|
||||
timeout: 8000,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user