Authorization flow handoff

This commit is contained in:
Justin Spahr-Summers 2025-02-17 15:04:02 +00:00
commit 62cd9520da
No known key found for this signature in database
4 changed files with 56 additions and 15 deletions

View file

@ -0,0 +1,5 @@
import crypto from "node:crypto";
export function generateToken(): string {
return crypto.randomBytes(32).toString("hex");
}

View file

@ -1,20 +1,19 @@
import { RequestHandler } from "express";
import { z } from "zod";
import { OAuthRegisteredClientsStore } from "../clients.js";
import { isValidUrl } from "../validation.js";
import { OAuthServerProvider } from "../provider.js";
export type AuthorizationHandlerOptions = {
/**
* A store used to read information about registered OAuth clients.
*/
store: OAuthRegisteredClientsStore;
provider: OAuthServerProvider;
};
// Parameters that must be validated in order to issue redirects.
const ClientAuthorizationParamsSchema = z.object({
client_id: z.string(),
redirect_uri: z.string().optional().refine((value) => value === undefined || isValidUrl(value), { message: "redirect_uri must be a valid URL" }),
});
// Parameters that must be validated for a successful authorization request. Failure can be reported to the redirect URI.
const RequestAuthorizationParamsSchema = z.object({
response_type: z.literal("code"),
code_challenge: z.string(),
@ -23,7 +22,7 @@ const RequestAuthorizationParamsSchema = z.object({
state: z.string().optional(),
});
export function authorizationHandler({ store }: AuthorizationHandlerOptions): RequestHandler {
export function authorizationHandler({ provider }: AuthorizationHandlerOptions): RequestHandler {
return async (req, res) => {
if (req.method !== "GET" && req.method !== "POST") {
res.status(405).end("Method Not Allowed");
@ -38,7 +37,7 @@ export function authorizationHandler({ store }: AuthorizationHandlerOptions): Re
return;
}
const client = await store.getClient(client_id);
const client = await provider.clientsStore.getClient(client_id);
if (!client) {
res.status(400).end("Bad Request: invalid client_id");
return;
@ -67,8 +66,9 @@ export function authorizationHandler({ store }: AuthorizationHandlerOptions): Re
return;
}
let requestedScopes: string[] = [];
if (params.scope !== undefined && client.scope !== undefined) {
const requestedScopes = params.scope.split(" ");
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
@ -83,8 +83,12 @@ export function authorizationHandler({ store }: AuthorizationHandlerOptions): Re
}
}
// TODO: Store code challenge
// TODO: Generate authorization code
// TODO: Redirect to redirect_uri (handle in calling code)
await provider.authorize({
client,
state: params.state,
scopes: requestedScopes,
redirectUri: redirect_uri,
codeChallenge: params.code_challenge,
}, res);
};
}

View file

@ -8,7 +8,7 @@ export type ClientRegistrationHandlerOptions = {
/**
* A store used to save information about dynamically registered OAuth clients.
*/
store: OAuthRegisteredClientsStore;
clientsStore: OAuthRegisteredClientsStore;
/**
* The number of seconds after which to expire issued client secrets, or 0 to prevent expiration of client secrets (not recommended).
@ -20,8 +20,8 @@ export type ClientRegistrationHandlerOptions = {
const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
export function clientRegistrationHandler({ store, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS }: ClientRegistrationHandlerOptions): RequestHandler {
if (!store.registerClient) {
export function clientRegistrationHandler({ clientsStore, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS }: ClientRegistrationHandlerOptions): RequestHandler {
if (!clientsStore.registerClient) {
throw new Error("Client registration store does not support registering clients");
}
@ -55,7 +55,7 @@ export function clientRegistrationHandler({ store, clientSecretExpirySeconds = D
client_secret_expires_at: clientSecretExpirySeconds > 0 ? clientIdIssuedAt + clientSecretExpirySeconds : 0
};
clientInfo = await store.registerClient!(clientInfo);
clientInfo = await clientsStore.registerClient!(clientInfo);
return clientInfo;
}

View file

@ -0,0 +1,32 @@
import { Response } from "express";
import { OAuthRegisteredClientsStore } from "./clients.js";
import { OAuthClientInformationFull } from "../../shared/auth.js";
export type AuthorizationParams = {
client: OAuthClientInformationFull;
state?: string;
scopes?: string[];
codeChallenge: string;
redirectUri: string;
};
/**
* Implements an end-to-end OAuth server.
*/
export interface OAuthServerProvider {
/**
* A store used to read information about registered OAuth clients.
*/
get clientsStore(): OAuthRegisteredClientsStore;
/**
* Begins the authorization flow, which can either be implemented by this server itself or via redirection to a separate authorization server.
*
* An authorization code can be generated using the `generateToken` function.
*
* This server must eventually issue a redirect with an authorization response or an error response to the given redirect URI. Per OAuth 2.1:
* - In the successful case, the redirect MUST include the `code` and `state` (if present) query parameters.
* - In the error case, the redirect MUST include the `error` query parameter, and MAY include an optional `error_description` query parameter.
*/
authorize(params: AuthorizationParams, res: Response): Promise<void>;
}