refactor(plugin): 🚚 Move JWKS well-known endpoint to OpenID plugin

This commit is contained in:
Jesse Wierzbinski 2024-10-07 12:52:22 +02:00
parent 2e827814de
commit 0557d52afe
No known key found for this signature in database
7 changed files with 118 additions and 83 deletions

View file

@ -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,
);
}),
);

View file

@ -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;

View file

@ -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";

View file

@ -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();
});
});

View file

@ -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,
);
},
),
);
};

View file

@ -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);

View file

@ -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);