WebSocket
Securing a WebSocket connection
Last updated
Was this helpful?
Securing a WebSocket connection
Last updated
Was this helpful?
Was this helpful?
import { Hono } from "hono";
import { createNodeWebSocket } from "@hono/node-ws";
import { serve } from "@hono/node-server";
import { bootstrapAuth, getUser, getUser_ws } from "./auth"; // See below
function startHonoServer() {
bootstrapAuth({
implementation: "real", // or "mock", see: https://docs.oidc-spa.dev/v/v8/integration-guides/backend-token-validation/mock-modes
issuerUri: process.env.OIDC_ISSUER_URI!,
expectedAudience: process.env.OIDC_AUDIENCE
});
const app = new Hono();
app.get("/api/todos", async c => { /* ... */ });
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
app.get(
"/ws",
upgradeWebSocket(async c => {
const user = await getUser_ws({ req: c.req });
return {
onOpen: (_event, ws) => {
ws.send(`Hello ${user.name}`);
},
onMessage(event, ws) {
ws.send(`I'm not very smart, all I can do is repeat: "${event.data}"`);
}
};
})
);
const server = serve({
fetch: app.fetch,
port
});
injectWebSocket(server);
}import { oidcSpa, extractRequestAuthContext } from "oidc-spa/server";
import { z } from "zod";
import { HTTPException } from "hono/http-exception";
import type { HonoRequest } from "hono";
const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
.withExpectedDecodedAccessTokenShape({
decodedAccessTokenSchema: z.object({
sub: z.string(),
name: z.string(),
email: z.string().optional(),
realm_access: z
.object({
roles: z.array(z.string())
})
.optional()
})
})
.createUtils();
export { bootstrapAuth };
export type User = {
id: string;
name: string;
email: string | undefined;
};
export async function getUser(/* ... */): Promise<User> { /* ... */ }
export async function getUser_ws(params: { req: HonoRequest }) {
const { req } = params;
const value = req.header("Sec-WebSocket-Protocol");
if (value === undefined) {
throw new HTTPException(400); // Bad Request
}
const accessToken = value
.split(",")
.map(p => p.trim())
.map(p => {
const match = p.match(/^authorization_bearer_(.+)$/);
if (match === null) {
return undefined;
}
return match[1];
})
.filter(t => t !== undefined)[0];
if (accessToken === undefined) {
throw new HTTPException(400); // Bad Request
}
const { isSuccess, debugErrorMessage, decodedAccessToken } =
await validateAndDecodeAccessToken({
scheme: "Bearer",
accessToken,
// NOTE: WebSocket upgrades are out of scope for DPoP.
// There's no RFC-defined way to send and validate a DPoP proof
// on the WebSocket Upgrade request.
// We accept the access token as bearer-like for the WS upgrade only.
// If you need DPoP-grade guarantees on the socket, add an app-level handshake.
rejectIfAccessTokenDPoPBound: false
});
if (!isSuccess) {
console.warn(debugErrorMessage);
throw new HTTPException(401); // Unauthorized
}
const { sub, name, email } = decodedAccessToken;
const user: User = {
id: sub,
name,
email
};
return user;
}import { Evt, type StatefulReadonlyEvt } from "evt";
import { getOidc } from "~/oidc";
import { assert } from "tsafe";
export type Chat = {
evtMessages: StatefulReadonlyEvt<Chat.Message[]>;
sendMessage: (message: string) => void;
};
export namespace Chat {
export type Message = {
origin: "client" | "server";
message: string;
};
}
function createChat(): Chat {
const evtMessages = Evt.create<Chat.Message[]>([]);
const dSocket = Promise.withResolvers<WebSocket>();
(async () => {
const oidc = await getOidc();
assert(oidc.isUserLoggedIn);
const url = new URL(import.meta.env.VITE_TODOS_API_URL); // ex: https://api.my-company.com
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
url.pathname += "ws";
const socket = new WebSocket(
url.href, // ex: wss://api.my-company.com/ws
// NOTE: This is a common workaround to the fact that the WebSocket API
// does not allow to set custom headers to the UPGRADE request.
// So we use the protocol and on the server read the Sec-WebSocket-Protocol header.
[`authorization_bearer_${await oidc.getAccessToken()}` ]
);
socket.addEventListener("message", event => {
evtMessages.state = [
...evtMessages.state,
{
origin: "server",
message: event.data
}
];
});
socket.addEventListener("error", err => {
console.error("socket error", err);
dSocket.reject(err);
});
socket.addEventListener("open", ()=> {
dSocket.resolve(socket);
});
})();
return {
evtMessages,
sendMessage: async message => {
evtMessages.state = [
...evtMessages.state,
{
origin: "client",
message
}
];
const socket = await dSocket.promise;
socket.send(message);
}
};
}
let chat: Chat | undefined = undefined;
export function getChat() {
if (chat === undefined) {
chat = createChat();
}
return chat;
}