From 0557d52afedb1e794743c94393fa3f5b17037754 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 7 Oct 2024 12:52:22 +0200 Subject: [PATCH] refactor(plugin): :truck: Move JWKS well-known endpoint to OpenID plugin --- api/well-known/jwks/index.ts | 74 ---------------------- plugins/openid/index.ts | 2 + plugins/openid/routes/authorize.test.ts | 6 +- plugins/openid/routes/jwks.test.ts | 42 ++++++++++++ plugins/openid/routes/jwks.ts | 65 +++++++++++++++++++ plugins/openid/routes/oauth/revoke.test.ts | 6 +- plugins/openid/routes/oauth/token.test.ts | 6 +- 7 files changed, 118 insertions(+), 83 deletions(-) delete mode 100644 api/well-known/jwks/index.ts create mode 100644 plugins/openid/routes/jwks.test.ts create mode 100644 plugins/openid/routes/jwks.ts diff --git a/api/well-known/jwks/index.ts b/api/well-known/jwks/index.ts deleted file mode 100644 index d7946b06..00000000 --- a/api/well-known/jwks/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { apiRoute, applyConfig } from "@/api"; -import { createRoute, z } from "@hono/zod-openapi"; -import { exportJWK } from "jose"; -import { config } from "~/packages/config-manager"; - -export const meta = applyConfig({ - auth: { - required: false, - }, - ratelimits: { - duration: 30, - max: 60, - }, - route: "/.well-known/jwks", -}); - -const route = createRoute({ - method: "get", - path: "/.well-known/jwks", - summary: "JWK Set", - responses: { - 200: { - description: "JWK Set", - content: { - "application/json": { - schema: z.object({ - keys: z.array( - z.object({ - kty: z.string(), - use: z.string(), - alg: z.string(), - kid: z.string(), - crv: z.string().optional(), - x: z.string().optional(), - y: z.string().optional(), - }), - ), - }), - }, - }, - }, - }, -}); - -export default apiRoute((app) => - app.openapi(route, async (context) => { - const publicKey = await crypto.subtle.importKey( - "spki", - Buffer.from(config.oidc.keys?.public ?? "", "base64"), - "Ed25519", - true, - ["verify"], - ); - - const jwk = await exportJWK(publicKey); - - // Remove the private key - jwk.d = undefined; - - return context.json( - { - keys: [ - { - ...jwk, - use: "sig", - alg: "EdDSA", - kid: "1", - }, - ], - }, - 200, - ); - }), -); diff --git a/plugins/openid/index.ts b/plugins/openid/index.ts index f5b128c7..52f9d99c 100644 --- a/plugins/openid/index.ts +++ b/plugins/openid/index.ts @@ -1,6 +1,7 @@ import { Hooks, Plugin } from "@versia/kit"; import { z } from "zod"; import authorizeRoute from "./routes/authorize.ts"; +import jwksRoute from "./routes/jwks.ts"; import tokenRevokeRoute from "./routes/oauth/revoke.ts"; import tokenRoute from "./routes/oauth/token.ts"; import ssoIdRoute from "./routes/sso/:id/index.ts"; @@ -74,6 +75,7 @@ ssoRoute(plugin); ssoIdRoute(plugin); tokenRoute(plugin); tokenRevokeRoute(plugin); +jwksRoute(plugin); export type PluginType = typeof plugin; export default plugin; diff --git a/plugins/openid/routes/authorize.test.ts b/plugins/openid/routes/authorize.test.ts index fff32dbb..94230090 100644 --- a/plugins/openid/routes/authorize.test.ts +++ b/plugins/openid/routes/authorize.test.ts @@ -1,9 +1,9 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { randomString } from "@/math"; -import { eq } from "drizzle-orm"; +import { db } from "@versia/kit/db"; +import { eq } from "@versia/kit/drizzle"; +import { Applications, RolePermissions } from "@versia/kit/tables"; import { SignJWT } from "jose"; -import { db } from "~/drizzle/db"; -import { Applications, RolePermissions } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; import { fakeRequest, getTestUsers } from "~/tests/utils"; diff --git a/plugins/openid/routes/jwks.test.ts b/plugins/openid/routes/jwks.test.ts new file mode 100644 index 00000000..adf42c77 --- /dev/null +++ b/plugins/openid/routes/jwks.test.ts @@ -0,0 +1,42 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { db } from "@versia/kit/db"; +import { eq } from "drizzle-orm"; +import { Applications } from "~/drizzle/schema"; +import { fakeRequest } from "~/tests/utils"; + +const clientId = "test-client-id"; +const redirectUri = "https://example.com/callback"; +const scope = "openid profile email"; +const secret = "test-secret"; + +beforeAll(async () => { + await db.insert(Applications).values({ + clientId, + redirectUri, + scopes: scope, + name: "Test Application", + secret, + }); +}); + +afterAll(async () => { + await db.delete(Applications).where(eq(Applications.clientId, clientId)); +}); + +describe("/.well-known/jwks", () => { + test("should return JWK set with valid inputs", async () => { + const response = await fakeRequest("/.well-known/jwks", { + method: "GET", + }); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.keys).toHaveLength(1); + expect(body.keys[0].kty).toBe("OKP"); + expect(body.keys[0].use).toBe("sig"); + expect(body.keys[0].alg).toBe("EdDSA"); + expect(body.keys[0].kid).toBe("1"); + expect(body.keys[0].crv).toBe("Ed25519"); + expect(body.keys[0].x).toBeString(); + }); +}); diff --git a/plugins/openid/routes/jwks.ts b/plugins/openid/routes/jwks.ts new file mode 100644 index 00000000..d9703895 --- /dev/null +++ b/plugins/openid/routes/jwks.ts @@ -0,0 +1,65 @@ +import { auth } from "@/api"; +import { createRoute, z } from "@hono/zod-openapi"; +import { exportJWK } from "jose"; +import type { PluginType } from "../index.ts"; + +export default (plugin: PluginType) => { + plugin.registerRoute("/.well-known/jwks", (app) => + app.openapi( + createRoute({ + method: "get", + path: "/.well-known/jwks", + summary: "JWK Set", + responses: { + 200: { + description: "JWK Set", + content: { + "application/json": { + schema: z.object({ + keys: z.array( + z.object({ + kty: z.string(), + use: z.string(), + alg: z.string(), + kid: z.string(), + crv: z.string().optional(), + x: z.string().optional(), + y: z.string().optional(), + }), + ), + }), + }, + }, + }, + }, + middleware: [ + auth({ + required: false, + }), + ], + }), + async (context) => { + const jwk = await exportJWK( + context.get("pluginConfig").keys?.public, + ); + + // Remove the private key 💀 + jwk.d = undefined; + + return context.json( + { + keys: [ + { + ...jwk, + use: "sig", + alg: "EdDSA", + kid: "1", + }, + ], + }, + 200, + ); + }, + ), + ); +}; diff --git a/plugins/openid/routes/oauth/revoke.test.ts b/plugins/openid/routes/oauth/revoke.test.ts index 99a1ebf1..2adbe2d3 100644 --- a/plugins/openid/routes/oauth/revoke.test.ts +++ b/plugins/openid/routes/oauth/revoke.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { eq } from "drizzle-orm"; -import { db } from "~/drizzle/db"; -import { Applications, Tokens } from "~/drizzle/schema"; +import { db } from "@versia/kit/db"; +import { eq } from "@versia/kit/drizzle"; +import { Applications, Tokens } from "@versia/kit/tables"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { deleteUsers, users } = await getTestUsers(1); diff --git a/plugins/openid/routes/oauth/token.test.ts b/plugins/openid/routes/oauth/token.test.ts index d05e7c8a..44455cb1 100644 --- a/plugins/openid/routes/oauth/token.test.ts +++ b/plugins/openid/routes/oauth/token.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { eq } from "drizzle-orm"; -import { db } from "~/drizzle/db"; -import { Applications, Tokens } from "~/drizzle/schema"; +import { db } from "@versia/kit/db"; +import { eq } from "@versia/kit/drizzle"; +import { Applications, Tokens } from "@versia/kit/tables"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { deleteUsers, users } = await getTestUsers(1);