Skip to content

Commit dc5a492

Browse files
committed
Support DPoP
1 parent 740d08f commit dc5a492

File tree

6 files changed

+121
-140
lines changed

6 files changed

+121
-140
lines changed

.prettierrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"printWidth": 120,
2+
"printWidth": 80,
33
"tabWidth": 4,
44
"useTabs": false,
55
"semi": true,

package.json

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,26 @@
1414
},
1515
"author": "u/garronej",
1616
"license": "MIT",
17-
"keywords": ["todo", "todos", "rest", "api"],
17+
"keywords": [
18+
"todo",
19+
"todos",
20+
"rest",
21+
"api"
22+
],
1823
"dependencies": {
1924
"@hono/node-server": "^1.11.1",
2025
"@hono/zod-openapi": "^0.13.0",
21-
"hono": "^4.3.2",
22-
"tsafe": "^1.7.2",
26+
"hono": "^4.11.1",
27+
"oidc-spa": "^8.7.0",
28+
"tsafe": "^1.8.12",
2329
"url-join": "^5.0.0",
24-
"zod": "^3.23.8",
25-
"oidc-spa": "^8.6.11"
30+
"zod": "^3.23.8"
2631
},
2732
"devDependencies": {
2833
"@types/node": "^20.12.12",
2934
"@vercel/ncc": "^0.38.1",
3035
"dotenv-cli": "^7.4.1",
31-
"typescript": "^5.4.5",
32-
"tsx": "^4.3.0"
36+
"tsx": "^4.3.0",
37+
"typescript": "^5.4.5"
3338
}
3439
}

src/auth.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { oidcSpa } from "oidc-spa/server";
2+
import { z } from "zod";
3+
import { HTTPException } from "hono/http-exception";
4+
import type { HonoRequest } from "hono";
5+
6+
const { bootstrapAuth, validateAndDecodeAccessToken } = oidcSpa
7+
.withExpectedDecodedAccessTokenShape({
8+
decodedAccessTokenSchema: z.object({
9+
sub: z.string(),
10+
realm_access: z.object({
11+
roles: z.array(z.string())
12+
})
13+
})
14+
})
15+
.createUtils();
16+
17+
export { bootstrapAuth };
18+
19+
export type User = {
20+
id: string;
21+
};
22+
23+
export async function getUser(
24+
req: HonoRequest,
25+
requiredRole?: "realm-admin" | "support-staff"
26+
): Promise<User> {
27+
28+
const { isSuccess, debugErrorMessage, decodedAccessToken } =
29+
await validateAndDecodeAccessToken({
30+
request: {
31+
url: req.url,
32+
method: req.method,
33+
headers: {
34+
Authorization: req.header("Authorization"),
35+
DPoP: req.header("DPoP")
36+
}
37+
}
38+
});
39+
40+
if (!isSuccess) {
41+
console.warn(debugErrorMessage);
42+
throw new HTTPException(401);
43+
}
44+
45+
if (
46+
requiredRole !== undefined &&
47+
!decodedAccessToken.realm_access.roles.includes(requiredRole)
48+
) {
49+
console.warn(`User missing role: ${requiredRole}`);
50+
throw new HTTPException(403);
51+
}
52+
53+
return {
54+
id: decodedAccessToken.sub
55+
};
56+
}

src/main.ts

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
import { z, createRoute, OpenAPIHono } from "@hono/zod-openapi";
22
import { serve } from "@hono/node-server";
3-
import { HTTPException } from "hono/http-exception";
43
import { getUserTodoStore } from "./todo";
54
import { cors } from "hono/cors";
65
import { assert } from "tsafe/assert";
7-
import { createDecodeAccessToken } from "./oidc";
6+
import { getUser, bootstrapAuth } from "./auth";
87

98
(async function main() {
10-
const { decodeAccessToken } = await createDecodeAccessToken({
11-
issuerUri: (() => {
12-
const value = process.env.OIDC_ISSUER_URI;
139

14-
assert(value !== undefined, "OIDC_ISSUER_URI must be defined");
10+
const issuerUri = (() => {
11+
const value = process.env.OIDC_ISSUER_URI;
1512

16-
return value;
17-
})(),
18-
audience: (() => {
19-
const value = process.env.OIDC_AUDIENCE;
13+
assert(value !== undefined, "OIDC_ISSUER_URI must be defined");
2014

21-
assert(value !== undefined, "OIDC_AUDIENCE must be defined");
15+
return value;
16+
})();
2217

23-
return value;
24-
})()
18+
const audience = (() => {
19+
const value = process.env.OIDC_AUDIENCE;
20+
21+
assert(value !== undefined, "OIDC_AUDIENCE must be defined");
22+
23+
return value;
24+
})();
25+
26+
bootstrapAuth({
27+
implementation: "real",
28+
issuerUri,
29+
expectedAudience: audience
2530
});
2631

2732
const app = new OpenAPIHono();
@@ -83,16 +88,17 @@ import { createDecodeAccessToken } from "./oidc";
8388
});
8489

8590
app.openapi(route, async c => {
86-
const decodedAccessToken = await decodeAccessToken({
87-
authorizationHeaderValue: c.req.header("Authorization")
88-
});
91+
92+
const user = await getUser(c.req);
8993

9094
const { id } = c.req.valid("param");
9195
const { text, isDone } = c.req.valid("json");
9296

93-
const todoStore = getUserTodoStore(decodedAccessToken.sub);
97+
const todoStore = getUserTodoStore(user.id);
9498

95-
const todo = todoStore.getAll().find(({ id: todoId }) => todoId === id);
99+
const todo = todoStore
100+
.getAll()
101+
.find(({ id: todoId }) => todoId === id);
96102

97103
assert(todo !== undefined);
98104

@@ -139,28 +145,24 @@ import { createDecodeAccessToken } from "./oidc";
139145
schema: z.object({
140146
id: z.string().openapi({
141147
example: "123",
142-
description: "The id of the newly created todo item"
148+
description:
149+
"The id of the newly created todo item"
143150
})
144151
})
145152
}
146153
},
147-
description: "Create a new todo item returns the newly created todo item's id"
154+
description:
155+
"Create a new todo item returns the newly created todo item's id"
148156
}
149157
}
150158
});
151159

152160
app.openapi(route, async c => {
153-
const decodedAccessToken = await decodeAccessToken({
154-
authorizationHeaderValue: c.req.header("Authorization")
155-
});
156-
157-
if (decodedAccessToken === undefined) {
158-
throw new HTTPException(401);
159-
}
161+
const user = await getUser(c.req);
160162

161163
const { text } = c.req.valid("json");
162164

163-
const todoStore = getUserTodoStore(decodedAccessToken.sub);
165+
const todoStore = getUserTodoStore(user.id);
164166

165167
const id = Math.random().toString();
166168

@@ -205,15 +207,9 @@ import { createDecodeAccessToken } from "./oidc";
205207
});
206208

207209
app.openapi(route, async c => {
208-
const decodedAccessToken = await decodeAccessToken({
209-
authorizationHeaderValue: c.req.header("Authorization")
210-
});
211-
212-
if (decodedAccessToken === undefined) {
213-
throw new HTTPException(401);
214-
}
210+
const user = await getUser(c.req);
215211

216-
const todos = getUserTodoStore(decodedAccessToken.sub).getAll();
212+
const todos = getUserTodoStore(user.id).getAll();
217213

218214
return c.json(todos);
219215
});
@@ -245,17 +241,11 @@ import { createDecodeAccessToken } from "./oidc";
245241
});
246242

247243
app.openapi(route, async c => {
248-
const decodedAccessToken = await decodeAccessToken({
249-
authorizationHeaderValue: c.req.header("Authorization")
250-
});
244+
const user = await getUser(c.req);
251245

252246
const { id } = c.req.valid("param");
253247

254-
if (decodedAccessToken === undefined) {
255-
throw new HTTPException(401);
256-
}
257-
258-
getUserTodoStore(decodedAccessToken.sub).remove(id);
248+
getUserTodoStore(user.id).remove(id);
259249

260250
return c.json({
261251
message: "Todo item deleted"
@@ -284,5 +274,7 @@ import { createDecodeAccessToken } from "./oidc";
284274
port
285275
});
286276

287-
console.log(`\nServer running. OpenAPI documentation available at http://localhost:${port}/doc`);
277+
console.log(
278+
`\nServer running. OpenAPI documentation available at http://localhost:${port}/doc`
279+
);
288280
})();

src/oidc.ts

Lines changed: 0 additions & 72 deletions
This file was deleted.

yarn.lock

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,10 @@ get-tsconfig@^4.7.5:
230230
dependencies:
231231
resolve-pkg-maps "^1.0.0"
232232

233-
hono@^4.3.2:
234-
version "4.3.9"
235-
resolved "https://registry.yarnpkg.com/hono/-/hono-4.3.9.tgz#3c866d527241dc4d423ef6a4e1fb1f8ed032278e"
236-
integrity sha512-6c5LVE23HnIS8iBhY+XPmYJlPeeClznOi7mBNsAsJCgxo8Ciz75LTjqRUf5wv4RYq8kL+1KPLUZHCtKmbZssNg==
233+
hono@^4.11.1:
234+
version "4.11.1"
235+
resolved "https://registry.yarnpkg.com/hono/-/hono-4.11.1.tgz#cb1b0c045fc74a96c693927234c95a45fb46ab0b"
236+
integrity sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==
237237

238238
isexe@^2.0.0:
239239
version "2.0.0"
@@ -245,10 +245,10 @@ minimist@^1.2.6:
245245
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
246246
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
247247

248-
oidc-spa@^8.6.11:
249-
version "8.6.11"
250-
resolved "https://registry.yarnpkg.com/oidc-spa/-/oidc-spa-8.6.11.tgz#8214e722def94d83e116096909b01be7acd08446"
251-
integrity sha512-0K4vq4fkm+BXXOLHtd1+1wH+BidPnN25CrX9PY6pjSUmIuFbnWVuPAl1zzEM6GfmjnU4oVf16ww3fCjbK1M/cA==
248+
oidc-spa@^8.7.0:
249+
version "8.7.0"
250+
resolved "https://registry.yarnpkg.com/oidc-spa/-/oidc-spa-8.7.0.tgz#8d690ecd2aeec2ad044f9e81d71cb79ab40ee5d7"
251+
integrity sha512-3EJXwofO07+c7kzsJvE0hqKWG72Y/+yiEov3802dHcPFwPR9ro3tGpk1ICsC8HNSI0ZbKSfszrVW6Ng4G9v1gg==
252252

253253
openapi3-ts@^4.1.2:
254254
version "4.3.1"
@@ -279,10 +279,10 @@ shebang-regex@^3.0.0:
279279
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
280280
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
281281

282-
tsafe@^1.7.2:
283-
version "1.7.2"
284-
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.7.2.tgz#0f63d414876287ad01b135f832722f93e22da374"
285-
integrity sha512-dAPfQLhCfCRre5qs+Z5Q2a7s2CV7RxffZUmvj7puGaePYjECzWREJFd3w4XSFe/T5tbxgowfItA/JSSZ6Ma3dA==
282+
tsafe@^1.8.12:
283+
version "1.8.12"
284+
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.8.12.tgz#68a410b4b7687ef497b1c85904b1215532335c3e"
285+
integrity sha512-nFRqW0ttu/2o6XTXsHiVZWJBCOaxhVqZLg7dgs3coZNsCMPXPfwz+zPHAQA+70fNnVJLAPg1EgGIqK9Q84tvAw==
286286

287287
tsx@^4.3.0:
288288
version "4.17.0"
@@ -322,6 +322,6 @@ yaml@^2.4.1:
322322
integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==
323323

324324
zod@^3.23.8:
325-
version "3.23.8"
326-
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
327-
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
325+
version "3.25.76"
326+
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
327+
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==

0 commit comments

Comments
 (0)