Authorization flow handoff
This commit is contained in:
parent
54f643cee8
commit
62cd9520da
4 changed files with 56 additions and 15 deletions
5
src/server/auth/crypto.ts
Normal file
5
src/server/auth/crypto.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import crypto from "node:crypto";
|
||||
|
||||
export function generateToken(): string {
|
||||
return crypto.randomBytes(32).toString("hex");
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
32
src/server/auth/provider.ts
Normal file
32
src/server/auth/provider.ts
Normal 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>;
|
||||
}
|
||||
Loading…
Reference in a new issue