vault: session-gated notifications WS + lifecycle cleanup
- token-store: shared "give me a valid Vaultwarden access token" (proactive refresh) used by both the HTTP proxy and the WS; router refactored onto it. - notifications WS: validates the platform session in `open` (deferred, owner only), injects the stored Vaultwarden token into the upstream, and buffers client frames during the async setup so the SignalR handshake isn't dropped. The device connects with its platform JWT (?access_token=), never a vault one. - lifecycle: logout drops the vault token set (keeps the protector); distress (/auth/revoke) and panic wipe both token set and protector, forcing a one-time master-password re-setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,20 @@
|
|||||||
import type { Handler } from 'hono';
|
import type { Handler } from 'hono';
|
||||||
|
import { clearVaultTokens, clearVaultUnlockKey } from 'officerdb';
|
||||||
import { triggerLockdown } from './panic';
|
import { triggerLockdown } from './panic';
|
||||||
import { clientIp } from './client-ip';
|
import { clientIp } from './client-ip';
|
||||||
|
|
||||||
// Trigger the panic lockdown. Authenticated (Bearer token) — no password in the body. Once tripped,
|
// Trigger the panic lockdown. Authenticated (Bearer token) — no password in the body. Once tripped,
|
||||||
// all logins and existing sessions are refused until the server is manually restarted.
|
// all logins and existing sessions are refused until the server is manually restarted.
|
||||||
export const panicHandler: Handler = async (ctx) => {
|
export const panicHandler: Handler = async (ctx) => {
|
||||||
const user = ctx.get('user') as { email?: string } | undefined;
|
const user = ctx.get('user') as { id?: number; email?: string } | undefined;
|
||||||
const origin = ctx.get('origin') as string | undefined;
|
const origin = ctx.get('origin') as string | undefined;
|
||||||
triggerLockdown(`/panic (user=${user?.email || '?'}, origin=${origin || '-'}, ip=${clientIp(ctx)})`);
|
triggerLockdown(`/panic (user=${user?.email || '?'}, origin=${origin || '-'}, ip=${clientIp(ctx)})`);
|
||||||
|
|
||||||
|
// Panic wipes the vault session AND the protector key (like distress).
|
||||||
|
if (user?.id) {
|
||||||
|
clearVaultTokens(user.id).catch(() => {});
|
||||||
|
clearVaultUnlockKey(user.id).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.json({ ok: true });
|
return ctx.json({ ok: true });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { Handler } from 'hono';
|
import type { Handler } from 'hono';
|
||||||
import { blacklistToken, cleanupExpiredTokens } from 'officerdb';
|
import { blacklistToken, cleanupExpiredTokens, clearVaultTokens, clearVaultUnlockKey } from 'officerdb';
|
||||||
import { clientIp } from './client-ip';
|
import { clientIp } from './client-ip';
|
||||||
|
|
||||||
// Distress: blacklist the current token (same as signout) but log it as a security event.
|
// Distress: blacklist the current token (same as signout) but log it as a security event.
|
||||||
export const revokeHandler: Handler = async (ctx) => {
|
export const revokeHandler: Handler = async (ctx) => {
|
||||||
const user = ctx.get('user') as { jti: string; exp: number; email?: string };
|
const user = ctx.get('user') as { id: number; jti: string; exp: number; email?: string };
|
||||||
const origin = ctx.get('origin') as string | undefined;
|
const origin = ctx.get('origin') as string | undefined;
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -12,6 +12,12 @@ export const revokeHandler: Handler = async (ctx) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await blacklistToken(user.jti, user.exp);
|
await blacklistToken(user.jti, user.exp);
|
||||||
|
|
||||||
|
// Distress wipes the vault session AND the stored protector key — forces a one-time master-password
|
||||||
|
// re-setup, the correct "someone got in" behavior.
|
||||||
|
clearVaultTokens(user.id).catch(() => {});
|
||||||
|
clearVaultUnlockKey(user.id).catch(() => {});
|
||||||
|
|
||||||
cleanupExpiredTokens().catch(() => {});
|
cleanupExpiredTokens().catch(() => {});
|
||||||
|
|
||||||
return ctx.json({ ok: true });
|
return ctx.json({ ok: true });
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import type { Handler } from 'hono';
|
import type { Handler } from 'hono';
|
||||||
import { blacklistToken, cleanupExpiredTokens } from 'officerdb';
|
import { blacklistToken, cleanupExpiredTokens, clearVaultTokens } from 'officerdb';
|
||||||
|
|
||||||
export const signoutHandler: Handler = async (ctx) => {
|
export const signoutHandler: Handler = async (ctx) => {
|
||||||
const user = ctx.get('user') as { jti: string; exp: number };
|
const user = ctx.get('user') as { id: number; jti: string; exp: number };
|
||||||
|
|
||||||
await blacklistToken(user.jti, user.exp);
|
await blacklistToken(user.jti, user.exp);
|
||||||
|
|
||||||
|
// Drop the brokered vault session on logout (the protector key stays, so re-login is frictionless).
|
||||||
|
clearVaultTokens(user.id).catch(() => {});
|
||||||
|
|
||||||
// Opportunistic cleanup of expired tokens (non-blocking)
|
// Opportunistic cleanup of expired tokens (non-blocking)
|
||||||
cleanupExpiredTokens().catch(() => {});
|
cleanupExpiredTokens().catch(() => {});
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ import { isSuperAdmin } from '../../super-admin';
|
|||||||
import * as errors from '../../custom-errors';
|
import * as errors from '../../custom-errors';
|
||||||
import { getVaultServerUrl } from './sidecar-server';
|
import { getVaultServerUrl } from './sidecar-server';
|
||||||
import { stripHopByHop, redactPath } from './proxy-util';
|
import { stripHopByHop, redactPath } from './proxy-util';
|
||||||
import { passwordGrant, refreshGrant, pick, expiryFrom, type PasswordGrantInput } from './broker';
|
import { passwordGrant, pick, expiryFrom, type PasswordGrantInput } from './broker';
|
||||||
import {
|
import { getValidAccessToken, refreshAccess } from './token-store';
|
||||||
getVaultTokens,
|
import { getVaultTokens, setVaultTokens, getVaultUnlockKey, setVaultUnlockKey } from 'officerdb';
|
||||||
setVaultTokens,
|
|
||||||
updateVaultAccess,
|
|
||||||
getVaultUnlockKey,
|
|
||||||
setVaultUnlockKey,
|
|
||||||
type VaultTokenSet,
|
|
||||||
} from 'officerdb';
|
|
||||||
|
|
||||||
// Platform side of Officer Vault (VAULT_AUTH_SPEC.md). The device holds NO Vaultwarden token; a valid
|
// Platform side of Officer Vault (VAULT_AUTH_SPEC.md). The device holds NO Vaultwarden token; a valid
|
||||||
// platform session authorizes vault access. This router:
|
// platform session authorizes vault access. This router:
|
||||||
@@ -26,7 +20,6 @@ import {
|
|||||||
export const vaultRouter = createRouter();
|
export const vaultRouter = createRouter();
|
||||||
|
|
||||||
const PREFIX = '/api/vault';
|
const PREFIX = '/api/vault';
|
||||||
const REFRESH_SKEW_MS = 60_000; // refresh a bit before expiry to avoid racing a 401
|
|
||||||
|
|
||||||
// Owner-only. The global backstop already blocks non-owner tokens from /api/vault; this is explicit
|
// Owner-only. The global backstop already blocks non-owner tokens from /api/vault; this is explicit
|
||||||
// defense-in-depth and gives a clear error.
|
// defense-in-depth and gives a clear error.
|
||||||
@@ -39,22 +32,6 @@ vaultRouter.use(originMiddleware); // set ctx 'origin' for userMiddleware's orig
|
|||||||
vaultRouter.use(userMiddleware); // valid platform session (Authorization: Bearer <platform JWT> or ?token=)
|
vaultRouter.use(userMiddleware); // valid platform session (Authorization: Bearer <platform JWT> or ?token=)
|
||||||
vaultRouter.use(ownerGate);
|
vaultRouter.use(ownerGate);
|
||||||
|
|
||||||
// Renew the access token from the stored refresh token; persist and return the new access token, or null.
|
|
||||||
async function refreshAccess(userId: number, ts: VaultTokenSet): Promise<string | null> {
|
|
||||||
const r = await refreshGrant(ts.refreshToken, ts.clientId ?? 'mobile');
|
|
||||||
if (!r.ok) return null;
|
|
||||||
const access = pick(r.data, 'access_token');
|
|
||||||
const refresh = pick(r.data, 'refresh_token');
|
|
||||||
if (typeof access !== 'string') return null;
|
|
||||||
await updateVaultAccess(
|
|
||||||
userId,
|
|
||||||
access,
|
|
||||||
typeof refresh === 'string' ? refresh : ts.refreshToken,
|
|
||||||
expiryFrom(r.data, Date.now()),
|
|
||||||
);
|
|
||||||
return access;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Native endpoints (registered before the catch-all) ──
|
// ── Native endpoints (registered before the catch-all) ──
|
||||||
|
|
||||||
// Reachability probe (owner-gated): forwards to the sidecar's /_health, which probes Vaultwarden's /alive.
|
// Reachability probe (owner-gated): forwards to the sidecar's /_health, which probes Vaultwarden's /alive.
|
||||||
@@ -149,13 +126,9 @@ vaultRouter.all('/*', async (ctx) => {
|
|||||||
const hasBody = method !== 'GET' && method !== 'HEAD';
|
const hasBody = method !== 'GET' && method !== 'HEAD';
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
|
|
||||||
// Resolve the Vaultwarden token to inject (proactively refreshed if near expiry). No token yet →
|
// Inject a valid Vaultwarden token (proactively refreshed near expiry). No token yet → forward without
|
||||||
// forward without one: unauthenticated endpoints (prelogin) work; authed ones get Vaultwarden's 401.
|
// one: unauthenticated endpoints (prelogin) work; authed ones get Vaultwarden's 401.
|
||||||
const tokenSet = await getVaultTokens(user.id);
|
const accessToken = await getValidAccessToken(user.id);
|
||||||
let accessToken = tokenSet?.accessToken ?? null;
|
|
||||||
if (tokenSet?.expiresAt && tokenSet.expiresAt.getTime() < Date.now() + REFRESH_SKEW_MS) {
|
|
||||||
accessToken = (await refreshAccess(user.id, tokenSet)) ?? accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = stripHopByHop(ctx.req.raw.headers);
|
const headers = stripHopByHop(ctx.req.raw.headers);
|
||||||
headers.delete('authorization'); // drop the platform JWT
|
headers.delete('authorization'); // drop the platform JWT
|
||||||
@@ -176,8 +149,9 @@ vaultRouter.all('/*', async (ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A 401 on a replayable (no-body) request → token likely just expired/revoked: refresh once and retry.
|
// A 401 on a replayable (no-body) request → token likely just expired/revoked: refresh once and retry.
|
||||||
if (upstream.status === 401 && !hasBody && tokenSet) {
|
if (upstream.status === 401 && !hasBody) {
|
||||||
const fresh = await refreshAccess(user.id, tokenSet);
|
const ts = await getVaultTokens(user.id);
|
||||||
|
const fresh = ts ? await refreshAccess(user.id, ts) : null;
|
||||||
if (fresh) {
|
if (fresh) {
|
||||||
headers.set('authorization', `Bearer ${fresh}`);
|
headers.set('authorization', `Bearer ${fresh}`);
|
||||||
upstream = await fetch(target, { method, headers, redirect: 'manual' }).catch(() => upstream);
|
upstream = await fetch(target, { method, headers, redirect: 'manual' }).catch(() => upstream);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { getVaultTokens, updateVaultAccess, type VaultTokenSet } from 'officerdb';
|
||||||
|
import { refreshGrant, pick, expiryFrom } from './broker';
|
||||||
|
|
||||||
|
// Read-side of the brokered Vaultwarden token: hand out a currently-valid access token, refreshing it
|
||||||
|
// server-side (with the stored refresh token) when it's near expiry. Shared by the HTTP proxy and the
|
||||||
|
// notifications-WS proxy so both inject a fresh token. The device never sees any of this.
|
||||||
|
|
||||||
|
const REFRESH_SKEW_MS = 60_000; // refresh a bit before expiry to avoid racing a 401
|
||||||
|
|
||||||
|
/** Renew the access token from the stored refresh token; persist and return it, or null on failure. */
|
||||||
|
export async function refreshAccess(userId: number, ts: VaultTokenSet): Promise<string | null> {
|
||||||
|
const r = await refreshGrant(ts.refreshToken, ts.clientId ?? 'mobile');
|
||||||
|
if (!r.ok) return null;
|
||||||
|
const access = pick(r.data, 'access_token');
|
||||||
|
const refresh = pick(r.data, 'refresh_token');
|
||||||
|
if (typeof access !== 'string') return null;
|
||||||
|
await updateVaultAccess(
|
||||||
|
userId,
|
||||||
|
access,
|
||||||
|
typeof refresh === 'string' ? refresh : ts.refreshToken,
|
||||||
|
expiryFrom(r.data, Date.now()),
|
||||||
|
);
|
||||||
|
return access;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A currently-valid Vaultwarden access token for the owner, or null if none is stored. */
|
||||||
|
export async function getValidAccessToken(userId: number): Promise<string | null> {
|
||||||
|
const ts = await getVaultTokens(userId);
|
||||||
|
if (!ts) return null;
|
||||||
|
if (ts.expiresAt && ts.expiresAt.getTime() < Date.now() + REFRESH_SKEW_MS) {
|
||||||
|
return (await refreshAccess(userId, ts)) ?? ts.accessToken;
|
||||||
|
}
|
||||||
|
return ts.accessToken;
|
||||||
|
}
|
||||||
@@ -1,48 +1,91 @@
|
|||||||
import type { ServerWebSocket } from 'bun';
|
import type { ServerWebSocket } from 'bun';
|
||||||
import { getVaultServerWsUrl } from './sidecar-server';
|
import { verify } from '../../jwt';
|
||||||
|
import { isTokenBlacklisted } from 'officerdb';
|
||||||
|
import { isSuperAdmin } from '../../super-admin';
|
||||||
import { isOriginAllowed } from '../../_middlewares';
|
import { isOriginAllowed } from '../../_middlewares';
|
||||||
|
import { getVaultServerWsUrl } from './sidecar-server';
|
||||||
|
import { getValidAccessToken } from './token-store';
|
||||||
|
|
||||||
// Platform side of the Bitwarden notifications WebSocket: origin-gate the client upgrade, then pipe it to
|
// Platform side of the Bitwarden notifications WebSocket. The device connects with its platform JWT (via
|
||||||
// the officer-vault sidecar (which in turn pipes to Vaultwarden). Dumb pipe: text + binary frames both
|
// ?access_token=, how SignalR carries the token); we validate that session in `open`, then pipe the socket
|
||||||
// ways, no inspection. The platform holds NO Vaultwarden knowledge — only the sidecar's loopback address.
|
// to the officer-vault sidecar with the stored Vaultwarden token injected — the device never holds it.
|
||||||
// SignalR's HTTP long-poll fallback does NOT arrive here — it rides the HTTP proxy (vaultRouter).
|
// Dumb pipe: text + binary frames both ways, no inspection. SignalR's HTTP long-poll fallback rides the
|
||||||
|
// HTTP proxy (vaultRouter) instead.
|
||||||
|
|
||||||
export type VaultWSData = {
|
export type VaultWSData = {
|
||||||
provider: 'vault';
|
provider: 'vault';
|
||||||
userId: number;
|
platformToken: string; // the platform JWT the device presented on the upgrade
|
||||||
email: string;
|
vaultWsPath: string; // path + query after /api/vault, e.g. /notifications/hub?access_token=<jwt>
|
||||||
username: string;
|
|
||||||
vaultWsPath: string; // subpath + query after /api/vault, e.g. /notifications/hub?access_token=…
|
|
||||||
vaultWsProtocol?: string; // requested Sec-WebSocket-Protocol, forwarded to the sidecar
|
vaultWsProtocol?: string; // requested Sec-WebSocket-Protocol, forwarded to the sidecar
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpstreamState = { ws: WebSocket; queue: (string | Uint8Array<ArrayBuffer>)[]; ready: boolean };
|
type UpstreamState = {
|
||||||
|
ws: WebSocket | null;
|
||||||
|
queue: (string | Uint8Array<ArrayBuffer>)[];
|
||||||
|
ready: boolean;
|
||||||
|
closed: boolean;
|
||||||
|
};
|
||||||
const upstreams = new Map<ServerWebSocket<VaultWSData>, UpstreamState>();
|
const upstreams = new Map<ServerWebSocket<VaultWSData>, UpstreamState>();
|
||||||
|
|
||||||
// Bun hands frames over as `string | Buffer`; a Buffer is a Uint8Array at runtime, so forward as-is.
|
// Bun hands frames over as `string | Buffer`; a Buffer is a Uint8Array at runtime, so forward as-is.
|
||||||
const asPayload = (raw: string | Buffer): string | Uint8Array<ArrayBuffer> =>
|
const asPayload = (raw: string | Buffer): string | Uint8Array<ArrayBuffer> =>
|
||||||
typeof raw === 'string' ? raw : (raw as Uint8Array<ArrayBuffer>);
|
typeof raw === 'string' ? raw : (raw as Uint8Array<ArrayBuffer>);
|
||||||
|
|
||||||
|
function closeClient(ws: ServerWebSocket<VaultWSData>, state: UpstreamState, code: number, reason: string) {
|
||||||
|
state.closed = true;
|
||||||
|
upstreams.delete(ws);
|
||||||
|
try {
|
||||||
|
ws.close(code, reason);
|
||||||
|
} catch {
|
||||||
|
/* already closed */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap the platform JWT in the path for the Vaultwarden access token before dialing upstream.
|
||||||
|
function injectToken(wsBase: string, vaultWsPath: string, vaultToken: string): string {
|
||||||
|
const qIdx = vaultWsPath.indexOf('?');
|
||||||
|
const path = qIdx >= 0 ? vaultWsPath.slice(0, qIdx) : vaultWsPath;
|
||||||
|
const params = new URLSearchParams(qIdx >= 0 ? vaultWsPath.slice(qIdx + 1) : '');
|
||||||
|
params.delete('token');
|
||||||
|
params.set('access_token', vaultToken);
|
||||||
|
return `${wsBase}${path}?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
export const vaultWebsocket = {
|
export const vaultWebsocket = {
|
||||||
open(ws: ServerWebSocket<VaultWSData>) {
|
async open(ws: ServerWebSocket<VaultWSData>) {
|
||||||
|
// Register state synchronously so frames sent during the async setup below are buffered, not dropped.
|
||||||
|
const state: UpstreamState = { ws: null, queue: [], ready: false, closed: false };
|
||||||
|
upstreams.set(ws, state);
|
||||||
|
|
||||||
|
// Deferred session validation (Bun requires the upgrade itself to be synchronous).
|
||||||
|
let userId: number;
|
||||||
|
try {
|
||||||
|
const payload = await verify(ws.data.platformToken);
|
||||||
|
if (!payload?.id) return closeClient(ws, state, 4001, 'Unauthorized');
|
||||||
|
if (payload.jti && (await isTokenBlacklisted(payload.jti))) return closeClient(ws, state, 4001, 'Unauthorized');
|
||||||
|
if (!(await isSuperAdmin(payload))) return closeClient(ws, state, 4001, 'Forbidden');
|
||||||
|
userId = payload.id;
|
||||||
|
} catch {
|
||||||
|
return closeClient(ws, state, 4001, 'Unauthorized');
|
||||||
|
}
|
||||||
|
if (state.closed) return;
|
||||||
|
|
||||||
|
const vaultToken = await getValidAccessToken(userId);
|
||||||
|
if (!vaultToken) return closeClient(ws, state, 4001, 'No vault session');
|
||||||
const wsBase = getVaultServerWsUrl();
|
const wsBase = getVaultServerWsUrl();
|
||||||
if (!wsBase) {
|
if (!wsBase) return closeClient(ws, state, 1011, 'Vault sidecar not available');
|
||||||
ws.close(1011, 'Vault sidecar not available');
|
if (state.closed) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${wsBase}${ws.data.vaultWsPath}`;
|
|
||||||
const protocols = ws.data.vaultWsProtocol
|
const protocols = ws.data.vaultWsProtocol
|
||||||
? ws.data.vaultWsProtocol
|
? ws.data.vaultWsProtocol
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const url = injectToken(wsBase, ws.data.vaultWsPath, vaultToken);
|
||||||
const upstream = protocols?.length ? new WebSocket(url, protocols) : new WebSocket(url);
|
const upstream = protocols?.length ? new WebSocket(url, protocols) : new WebSocket(url);
|
||||||
upstream.binaryType = 'arraybuffer';
|
upstream.binaryType = 'arraybuffer';
|
||||||
|
state.ws = upstream;
|
||||||
const state: UpstreamState = { ws: upstream, queue: [], ready: false };
|
|
||||||
upstreams.set(ws, state);
|
|
||||||
|
|
||||||
upstream.addEventListener('open', () => {
|
upstream.addEventListener('open', () => {
|
||||||
state.ready = true;
|
state.ready = true;
|
||||||
@@ -77,14 +120,15 @@ export const vaultWebsocket = {
|
|||||||
const state = upstreams.get(ws);
|
const state = upstreams.get(ws);
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
const payload = asPayload(raw);
|
const payload = asPayload(raw);
|
||||||
if (state.ready) state.ws.send(payload);
|
if (state.ready && state.ws) state.ws.send(payload);
|
||||||
else state.queue.push(payload); // buffer until the sidecar socket opens
|
else state.queue.push(payload); // buffer until the upstream socket opens
|
||||||
},
|
},
|
||||||
close(ws: ServerWebSocket<VaultWSData>) {
|
close(ws: ServerWebSocket<VaultWSData>) {
|
||||||
const state = upstreams.get(ws);
|
const state = upstreams.get(ws);
|
||||||
if (state) {
|
if (state) {
|
||||||
|
state.closed = true;
|
||||||
try {
|
try {
|
||||||
state.ws.close();
|
state.ws?.close();
|
||||||
} catch {
|
} catch {
|
||||||
/* already closed */
|
/* already closed */
|
||||||
}
|
}
|
||||||
@@ -96,21 +140,21 @@ export const vaultWebsocket = {
|
|||||||
|
|
||||||
const PREFIX = '/api/vault';
|
const PREFIX = '/api/vault';
|
||||||
|
|
||||||
// Serve-level upgrade for /api/vault/notifications/* WebSockets. Origin-gated: only an allow-listed origin
|
// Serve-level upgrade for /api/vault/notifications/* WebSockets. Origin-gated; the platform JWT rides the
|
||||||
// (the OffVault app in prod; anything in dev) may connect — mirroring the HTTP origin gate. The Bitwarden
|
// query (?access_token= for SignalR, or ?token=). The session is validated in `open` (deferred). The
|
||||||
// access_token rides the query string and is forwarded untouched; we do NOT verify it as a platform JWT
|
// device never sends a Vaultwarden token — we inject the stored one upstream.
|
||||||
// (it isn't one), matching the "don't inject platform session auth" caveat.
|
|
||||||
export function upgradeVaultWs(req: Request, server: any): Response | undefined {
|
export function upgradeVaultWs(req: Request, server: any): Response | undefined {
|
||||||
const origin = req.headers.get('origin') ?? undefined;
|
const origin = req.headers.get('origin') ?? undefined;
|
||||||
const host = req.headers.get('host') ?? undefined;
|
const host = req.headers.get('host') ?? undefined;
|
||||||
if (!isOriginAllowed(origin, host)) return new Response('Forbidden', { status: 403 });
|
if (!isOriginAllowed(origin, host)) return new Response('Forbidden', { status: 403 });
|
||||||
|
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
const platformToken = url.searchParams.get('access_token') || url.searchParams.get('token') || '';
|
||||||
|
if (!platformToken) return new Response('Unauthorized', { status: 401 });
|
||||||
|
|
||||||
const data: VaultWSData = {
|
const data: VaultWSData = {
|
||||||
provider: 'vault',
|
provider: 'vault',
|
||||||
userId: 0,
|
platformToken,
|
||||||
email: '',
|
|
||||||
username: '',
|
|
||||||
vaultWsPath: url.pathname.slice(PREFIX.length) + url.search,
|
vaultWsPath: url.pathname.slice(PREFIX.length) + url.search,
|
||||||
vaultWsProtocol: req.headers.get('sec-websocket-protocol') ?? undefined,
|
vaultWsProtocol: req.headers.get('sec-websocket-protocol') ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user