Skip to content

Commit ea344e1

Browse files
committed
Implement websocket example
1 parent 9bb6efb commit ea344e1

4 files changed

Lines changed: 125 additions & 20 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
],
2323
"dependencies": {
2424
"@hono/node-server": "^1.11.1",
25+
"@hono/node-ws": "^1.0.3",
2526
"@hono/zod-openapi": "^0.13.0",
2627
"hono": "^4.11.1",
2728
"oidc-spa": "^8.7.10",

src/auth.ts

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,59 @@ import { z } from "zod";
33
import { HTTPException } from "hono/http-exception";
44
import type { HonoRequest } from "hono";
55

6-
const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
6+
const { bootstrapAuth, validateAndDecodeAccessToken, ofTypeDecodedAccessToken } = oidcSpa
77
.withExpectedDecodedAccessTokenShape({
88
decodedAccessTokenSchema: z.object({
99
sub: z.string(),
10-
realm_access: z.object({
11-
roles: z.array(z.string())
12-
}).optional()
10+
name: z.string(),
11+
email: z.string().optional(),
12+
realm_access: z
13+
.object({
14+
roles: z.array(z.string())
15+
})
16+
.optional()
1317
})
1418
})
1519
.createUtils();
1620

1721
export { bootstrapAuth };
1822

23+
type DecodedAccessToken = typeof ofTypeDecodedAccessToken;
24+
1925
export type User = {
2026
id: string;
27+
name: string;
28+
email: string | undefined;
2129
};
2230

23-
export async function getUser(
24-
req: HonoRequest,
25-
requiredRole?: "realm-admin" | "support-staff"
31+
async function decodedAccessTokenToUser(
32+
decodedAccessToken: DecodedAccessToken
2633
): Promise<User> {
34+
const { sub, name, email } = decodedAccessToken;
35+
36+
// Potentially fetch additional data that represent your user.
37+
38+
const user: User = {
39+
id: sub,
40+
name,
41+
email
42+
};
43+
44+
return user;
45+
}
46+
47+
export async function getUser(params: {
48+
req: HonoRequest;
49+
requiredRole?: "realm-admin" | "support-staff";
50+
}): Promise<User> {
51+
const { req, requiredRole } = params;
52+
2753
const requestAuthContext = extractRequestAuthContext({
2854
request: req,
2955
trustProxy: true
3056
});
3157

32-
if( !requestAuthContext ){
58+
if (!requestAuthContext) {
3359
// Demo shortcut: we return 401 on missing Authorization, but a mixed
3460
// public/private endpoint could instead return undefined here and let
3561
// the caller decide whether to process an anonymous request.
@@ -61,9 +87,55 @@ export async function getUser(
6187
}
6288
}
6389

64-
const user: User = {
65-
id: decodedAccessToken.sub
66-
};
90+
const user = await decodedAccessTokenToUser(decodedAccessToken);
6791

6892
return user;
6993
}
94+
95+
export async function getUser_ws(params: { req: HonoRequest }) {
96+
const { req } = params;
97+
98+
const value = req.header("Sec-WebSocket-Protocol");
99+
100+
if (value === undefined) {
101+
throw new HTTPException(400); // Bad Request
102+
}
103+
104+
const accessToken = value
105+
.split(",")
106+
.map(p => p.trim())
107+
.map(p => {
108+
const match = p.match(/^authorization_bearer_(.+)$/);
109+
110+
if (match === null) {
111+
return undefined;
112+
}
113+
114+
return match[1];
115+
})
116+
.filter(t => t !== undefined)[0];
117+
118+
if (accessToken === undefined) {
119+
throw new HTTPException(400); // Bad Request
120+
}
121+
122+
const { isSuccess, debugErrorMessage, decodedAccessToken } =
123+
await validateAndDecodeAccessToken({
124+
scheme: "Bearer",
125+
accessToken,
126+
// NOTE: The DPoP protocol does not cover WebSocket Upgrade request.
127+
// We chose to accept tokens even if the proof isn't provided.
128+
rejectIfAccessTokenDPoPBound: false
129+
});
130+
131+
if (!isSuccess) {
132+
console.warn(debugErrorMessage);
133+
throw new HTTPException(401); // Unauthorized
134+
}
135+
136+
const user = await decodedAccessTokenToUser(decodedAccessToken);
137+
138+
return user;
139+
}
140+
141+

src/main.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { z, createRoute, OpenAPIHono } from "@hono/zod-openapi";
22
import { serve } from "@hono/node-server";
3+
import { createNodeWebSocket } from "@hono/node-ws";
34
import { getUserTodoStore } from "./todo";
45
import { cors } from "hono/cors";
56
import { assert } from "tsafe/assert";
6-
import { getUser, bootstrapAuth } from "./auth";
7+
import { bootstrapAuth, getUser, getUser_ws } from "./auth";
78

89
(async function main() {
9-
1010
const issuerUri = (() => {
1111
const value = process.env.OIDC_ISSUER_URI;
1212

@@ -33,6 +33,25 @@ import { getUser, bootstrapAuth } from "./auth";
3333

3434
app.use("*", cors());
3535

36+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
37+
38+
app.get(
39+
"/ws",
40+
upgradeWebSocket(async c => {
41+
42+
const user = await getUser_ws({ req: c.req });
43+
44+
return {
45+
onOpen: (_event, ws) => {
46+
ws.send(`Hello ${user.name}`);
47+
},
48+
onMessage(event, ws) {
49+
ws.send(`I'm not very smart, all I can do is repeat what you say: "${event.data}"`);
50+
}
51+
};
52+
})
53+
);
54+
3655
{
3756
const route = createRoute({
3857
method: "put",
@@ -88,8 +107,7 @@ import { getUser, bootstrapAuth } from "./auth";
88107
});
89108

90109
app.openapi(route, async c => {
91-
92-
const user = await getUser(c.req);
110+
const user = await getUser({ req: c.req });
93111

94112
const { id } = c.req.valid("param");
95113
const { text, isDone } = c.req.valid("json");
@@ -158,7 +176,7 @@ import { getUser, bootstrapAuth } from "./auth";
158176
});
159177

160178
app.openapi(route, async c => {
161-
const user = await getUser(c.req);
179+
const user = await getUser({ req: c.req });
162180

163181
const { text } = c.req.valid("json");
164182

@@ -207,7 +225,7 @@ import { getUser, bootstrapAuth } from "./auth";
207225
});
208226

209227
app.openapi(route, async c => {
210-
const user = await getUser(c.req);
228+
const user = await getUser({ req: c.req });
211229

212230
const todos = getUserTodoStore(user.id).getAll();
213231

@@ -241,8 +259,8 @@ import { getUser, bootstrapAuth } from "./auth";
241259
});
242260

243261
app.openapi(route, async c => {
244-
const user = await getUser(c.req);
245-
262+
const user = await getUser({ req: c.req });
263+
246264
const { id } = c.req.valid("param");
247265

248266
getUserTodoStore(user.id).remove(id);
@@ -269,11 +287,13 @@ import { getUser, bootstrapAuth } from "./auth";
269287

270288
const port = parseInt(process.env.PORT);
271289

272-
serve({
290+
const server = serve({
273291
fetch: app.fetch,
274292
port
275293
});
276294

295+
injectWebSocket(server);
296+
277297
console.log(
278298
`\nServer running. OpenAPI documentation available at http://localhost:${port}/doc`
279299
);

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@
134134
resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.11.1.tgz#f4c7bea7f3d52760b1950d6b8aeb900cc59142d3"
135135
integrity sha512-GW1Iomhmm1o4Z+X57xGby8A35Cu9UZLL7pSMdqDBkD99U5cywff8F+8hLk5aBTzNubnsFAvWQ/fZjNwPsEn9lA==
136136

137+
"@hono/node-ws@^1.0.3":
138+
version "1.2.0"
139+
resolved "https://registry.yarnpkg.com/@hono/node-ws/-/node-ws-1.2.0.tgz#93041d045a92aebc0e6e5b98d263ff32d37312cd"
140+
integrity sha512-OBPQ8OSHBw29mj00wT/xGYtB6HY54j0fNSdVZ7gZM3TUeq0So11GXaWtFf1xWxQNfumKIsj0wRuLKWfVsO5GgQ==
141+
dependencies:
142+
ws "^8.17.0"
143+
137144
"@hono/zod-openapi@^0.13.0":
138145
version "0.13.0"
139146
resolved "https://registry.yarnpkg.com/@hono/zod-openapi/-/zod-openapi-0.13.0.tgz#6b9640529d7399302dad17ee8ce5d20d5937eab0"
@@ -316,6 +323,11 @@ which@^2.0.1:
316323
dependencies:
317324
isexe "^2.0.0"
318325

326+
ws@^8.17.0:
327+
version "8.18.3"
328+
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
329+
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
330+
319331
yaml@^2.4.1:
320332
version "2.4.2"
321333
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362"

0 commit comments

Comments
 (0)