Skip to content

Commit 62cd952

Browse files
committed
Authorization flow handoff
1 parent 54f643c commit 62cd952

File tree

4 files changed

+56
-15
lines changed

4 files changed

+56
-15
lines changed

src/server/auth/crypto.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import crypto from "node:crypto";
2+
3+
export function generateToken(): string {
4+
return crypto.randomBytes(32).toString("hex");
5+
}

src/server/auth/handlers/authorize.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
import { RequestHandler } from "express";
22
import { z } from "zod";
3-
import { OAuthRegisteredClientsStore } from "../clients.js";
43
import { isValidUrl } from "../validation.js";
4+
import { OAuthServerProvider } from "../provider.js";
55

66
export type AuthorizationHandlerOptions = {
7-
/**
8-
* A store used to read information about registered OAuth clients.
9-
*/
10-
store: OAuthRegisteredClientsStore;
7+
provider: OAuthServerProvider;
118
};
129

10+
// Parameters that must be validated in order to issue redirects.
1311
const ClientAuthorizationParamsSchema = z.object({
1412
client_id: z.string(),
1513
redirect_uri: z.string().optional().refine((value) => value === undefined || isValidUrl(value), { message: "redirect_uri must be a valid URL" }),
1614
});
1715

16+
// Parameters that must be validated for a successful authorization request. Failure can be reported to the redirect URI.
1817
const RequestAuthorizationParamsSchema = z.object({
1918
response_type: z.literal("code"),
2019
code_challenge: z.string(),
@@ -23,7 +22,7 @@ const RequestAuthorizationParamsSchema = z.object({
2322
state: z.string().optional(),
2423
});
2524

26-
export function authorizationHandler({ store }: AuthorizationHandlerOptions): RequestHandler {
25+
export function authorizationHandler({ provider }: AuthorizationHandlerOptions): RequestHandler {
2726
return async (req, res) => {
2827
if (req.method !== "GET" && req.method !== "POST") {
2928
res.status(405).end("Method Not Allowed");
@@ -38,7 +37,7 @@ export function authorizationHandler({ store }: AuthorizationHandlerOptions): Re
3837
return;
3938
}
4039

41-
const client = await store.getClient(client_id);
40+
const client = await provider.clientsStore.getClient(client_id);
4241
if (!client) {
4342
res.status(400).end("Bad Request: invalid client_id");
4443
return;
@@ -67,8 +66,9 @@ export function authorizationHandler({ store }: AuthorizationHandlerOptions): Re
6766
return;
6867
}
6968

69+
let requestedScopes: string[] = [];
7070
if (params.scope !== undefined && client.scope !== undefined) {
71-
const requestedScopes = params.scope.split(" ");
71+
requestedScopes = params.scope.split(" ");
7272
const allowedScopes = new Set(client.scope.split(" "));
7373

7474
// If any requested scope is not in the client's registered scopes, error out
@@ -83,8 +83,12 @@ export function authorizationHandler({ store }: AuthorizationHandlerOptions): Re
8383
}
8484
}
8585

86-
// TODO: Store code challenge
87-
// TODO: Generate authorization code
88-
// TODO: Redirect to redirect_uri (handle in calling code)
86+
await provider.authorize({
87+
client,
88+
state: params.state,
89+
scopes: requestedScopes,
90+
redirectUri: redirect_uri,
91+
codeChallenge: params.code_challenge,
92+
}, res);
8993
};
9094
}

src/server/auth/handlers/register.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type ClientRegistrationHandlerOptions = {
88
/**
99
* A store used to save information about dynamically registered OAuth clients.
1010
*/
11-
store: OAuthRegisteredClientsStore;
11+
clientsStore: OAuthRegisteredClientsStore;
1212

1313
/**
1414
* 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 = {
2020

2121
const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
2222

23-
export function clientRegistrationHandler({ store, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS }: ClientRegistrationHandlerOptions): RequestHandler {
24-
if (!store.registerClient) {
23+
export function clientRegistrationHandler({ clientsStore, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS }: ClientRegistrationHandlerOptions): RequestHandler {
24+
if (!clientsStore.registerClient) {
2525
throw new Error("Client registration store does not support registering clients");
2626
}
2727

@@ -55,7 +55,7 @@ export function clientRegistrationHandler({ store, clientSecretExpirySeconds = D
5555
client_secret_expires_at: clientSecretExpirySeconds > 0 ? clientIdIssuedAt + clientSecretExpirySeconds : 0
5656
};
5757

58-
clientInfo = await store.registerClient!(clientInfo);
58+
clientInfo = await clientsStore.registerClient!(clientInfo);
5959
return clientInfo;
6060
}
6161

src/server/auth/provider.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Response } from "express";
2+
import { OAuthRegisteredClientsStore } from "./clients.js";
3+
import { OAuthClientInformationFull } from "../../shared/auth.js";
4+
5+
export type AuthorizationParams = {
6+
client: OAuthClientInformationFull;
7+
state?: string;
8+
scopes?: string[];
9+
codeChallenge: string;
10+
redirectUri: string;
11+
};
12+
13+
/**
14+
* Implements an end-to-end OAuth server.
15+
*/
16+
export interface OAuthServerProvider {
17+
/**
18+
* A store used to read information about registered OAuth clients.
19+
*/
20+
get clientsStore(): OAuthRegisteredClientsStore;
21+
22+
/**
23+
* Begins the authorization flow, which can either be implemented by this server itself or via redirection to a separate authorization server.
24+
*
25+
* An authorization code can be generated using the `generateToken` function.
26+
*
27+
* This server must eventually issue a redirect with an authorization response or an error response to the given redirect URI. Per OAuth 2.1:
28+
* - In the successful case, the redirect MUST include the `code` and `state` (if present) query parameters.
29+
* - In the error case, the redirect MUST include the `error` query parameter, and MAY include an optional `error_description` query parameter.
30+
*/
31+
authorize(params: AuthorizationParams, res: Response): Promise<void>;
32+
}

0 commit comments

Comments
 (0)