From 9dc4a4f6a91052def706eba888f6ec072351d6bb Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 17 Feb 2025 14:37:10 +0000 Subject: [PATCH] WIP /authorize endpoint --- src/server/auth/handlers/authorize.ts | 89 +++++++++++++++++++++++++++ src/server/auth/handlers/register.ts | 2 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/server/auth/handlers/authorize.ts diff --git a/src/server/auth/handlers/authorize.ts b/src/server/auth/handlers/authorize.ts new file mode 100644 index 00000000..1f29b0bb --- /dev/null +++ b/src/server/auth/handlers/authorize.ts @@ -0,0 +1,89 @@ +import { RequestHandler } from "express"; +import { z } from "zod"; +import { OAuthRegisteredClientsStore } from "../clients.js"; + +export type AuthorizationHandlerOptions = { + /** + * A store used to read information about registered OAuth clients. + */ + store: OAuthRegisteredClientsStore; +}; + +const ClientAuthorizationParamsSchema = z.object({ + client_id: z.string(), + redirect_uri: z.string().optional(), +}); + +const RequestAuthorizationParamsSchema = z.object({ + response_type: z.literal("code"), + code_challenge: z.string(), + code_challenge_method: z.literal("S256"), + scope: z.string().optional(), + state: z.string().optional(), +}); + +export function authorizationHandler({ store }: AuthorizationHandlerOptions): RequestHandler { + return async (req, res) => { + if (req.method !== "GET" && req.method !== "POST") { + res.status(405).end("Method Not Allowed"); + return; + } + + let client_id, redirect_uri; + try { + ({ client_id, redirect_uri } = ClientAuthorizationParamsSchema.parse(req.query)); + } catch (error) { + res.status(400).end(`Bad Request: ${error}`); + return; + } + + const client = await store.getClient(client_id); + if (!client) { + res.status(400).end("Bad Request: invalid client_id"); + return; + } + + if (redirect_uri !== undefined) { + if (!client.redirect_uris.includes(redirect_uri)) { + res.status(400).end("Bad Request: invalid redirect_uri"); + return; + } + } else if (client.redirect_uris.length === 1) { + redirect_uri = client.redirect_uris[0]; + } else { + res.status(400).end("Bad Request: missing redirect_uri"); + return; + } + + let params; + try { + params = RequestAuthorizationParamsSchema.parse(req.query); + } catch (error) { + const errorUrl = new URL(redirect_uri); + errorUrl.searchParams.set("error", "invalid_request"); + errorUrl.searchParams.set("error_description", String(error)); + res.redirect(302, errorUrl.href); + return; + } + + if (params.scope !== undefined && client.scope !== undefined) { + const requestedScopes = params.scope.split(" "); + const allowedScopes = new Set(client.scope.split(" ")); + + // If any requested scope is not in the client's registered scopes, error out + for (const scope of requestedScopes) { + if (!allowedScopes.has(scope)) { + const errorUrl = new URL(redirect_uri); + errorUrl.searchParams.set("error", "invalid_scope"); + errorUrl.searchParams.set("error_description", `Client was not registered with scope ${scope}`); + res.redirect(302, errorUrl.href); + return; + } + } + } + + // TODO: Store code challenge + // TODO: Generate authorization code + // TODO: Redirect to redirect_uri (handle in calling code) + }; +} \ No newline at end of file diff --git a/src/server/auth/handlers/register.ts b/src/server/auth/handlers/register.ts index 32933316..bd5b080a 100644 --- a/src/server/auth/handlers/register.ts +++ b/src/server/auth/handlers/register.ts @@ -25,7 +25,7 @@ export function clientRegistrationHandler({ store, clientSecretExpirySeconds = D throw new Error("Client registration store does not support registering clients"); } - // Nested router so we can configure middleware + // Nested router so we can configure middleware and restrict HTTP method const router = express.Router(); router.use(express.json());