Extract client authentication into reusable middleware
This commit is contained in:
parent
f4002e559e
commit
19f2ac7018
4 changed files with 73 additions and 23 deletions
|
|
@ -12,7 +12,7 @@ export interface OAuthRegisteredClientsStore {
|
|||
/**
|
||||
* Registers a new client with the server. The client ID and secret will be automatically generated by the library. A modified version of the client information can be returned to reflect specific values enforced by the server.
|
||||
*
|
||||
* NOTE: Implements must ensure that client secrets, if present, are expired in accordance with the `client_secret_expires_at` field.
|
||||
* NOTE: Implementations must ensure that client secrets, if present, are expired in accordance with the `client_secret_expires_at` field.
|
||||
*
|
||||
* If unimplemented, dynamic client registration is unsupported.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ import express, { RequestHandler } from "express";
|
|||
import { OAuthServerProvider } from "../provider.js";
|
||||
import cors from "cors";
|
||||
import { verifyChallenge } from "pkce-challenge";
|
||||
import { authenticateClient } from "../middleware/clientAuth.js";
|
||||
|
||||
export type TokenHandlerOptions = {
|
||||
provider: OAuthServerProvider;
|
||||
};
|
||||
|
||||
const TokenRequestSchema = z.object({
|
||||
client_id: z.string(),
|
||||
client_secret: z.string().optional(),
|
||||
grant_type: z.string(),
|
||||
});
|
||||
|
||||
|
|
@ -32,10 +31,13 @@ export function tokenHandler({ provider }: TokenHandlerOptions): RequestHandler
|
|||
// Configure CORS to allow any origin, to make accessible to web-based MCP clients
|
||||
router.use(cors());
|
||||
|
||||
// Authenticate and extract client details
|
||||
router.use(authenticateClient({ clientsStore: provider.clientsStore }));
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
let client_id, client_secret, grant_type;
|
||||
let grant_type;
|
||||
try {
|
||||
({ client_id, client_secret, grant_type } = TokenRequestSchema.parse(req.body));
|
||||
({ grant_type } = TokenRequestSchema.parse(req.body));
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
error: "invalid_request",
|
||||
|
|
@ -44,24 +46,6 @@ export function tokenHandler({ provider }: TokenHandlerOptions): RequestHandler
|
|||
return;
|
||||
}
|
||||
|
||||
const client = await provider.clientsStore.getClient(client_id);
|
||||
if (!client) {
|
||||
// TODO: Return 401 with WWW-Authenticate if Authorization header was used
|
||||
res.status(400).json({
|
||||
error: "invalid_client",
|
||||
error_description: "Invalid client_id",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.client_secret !== client_secret) {
|
||||
res.status(400).json({
|
||||
error: "invalid_client",
|
||||
error_description: "Invalid client_secret",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (grant_type) {
|
||||
case "authorization_code": {
|
||||
let grant;
|
||||
|
|
|
|||
65
src/server/auth/middleware/clientAuth.ts
Normal file
65
src/server/auth/middleware/clientAuth.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { z } from "zod";
|
||||
import { RequestHandler } from "express";
|
||||
import { OAuthRegisteredClientsStore } from "../clients.js";
|
||||
import { OAuthClientInformationFull } from "../../../shared/auth.js";
|
||||
|
||||
export type ClientAuthenticationMiddlewareOptions = {
|
||||
/**
|
||||
* A store used to read information about registered OAuth clients.
|
||||
*/
|
||||
clientsStore: OAuthRegisteredClientsStore;
|
||||
}
|
||||
|
||||
const ClientAuthenticatedRequestSchema = z.object({
|
||||
client_id: z.string(),
|
||||
client_secret: z.string().optional(),
|
||||
})
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
/**
|
||||
* The authenticated client for this request, if the `authenticateClient` middleware was used.
|
||||
*/
|
||||
client?: OAuthClientInformationFull;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function authenticateClient({ clientsStore }: ClientAuthenticationMiddlewareOptions): RequestHandler {
|
||||
return async (req, res, next) => {
|
||||
let client_id, client_secret;
|
||||
try {
|
||||
const result = ClientAuthenticatedRequestSchema.parse(req.body);
|
||||
client_id = result.client_id;
|
||||
client_secret = result.client_secret;
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
error: "invalid_request",
|
||||
error_description: String(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await clientsStore.getClient(client_id);
|
||||
if (!client) {
|
||||
// TODO: Return 401 with WWW-Authenticate if Authorization header was used
|
||||
res.status(400).json({
|
||||
error: "invalid_client",
|
||||
error_description: "Invalid client_id",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client.client_secret !== client_secret) {
|
||||
res.status(400).json({
|
||||
error: "invalid_client",
|
||||
error_description: "Invalid client_secret",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
req.client = client;
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ export interface OAuthServerProvider {
|
|||
/**
|
||||
* Returns the `codeChallenge` that was used when the indicated authorization began.
|
||||
*/
|
||||
// TODO: Add `client` to this and below methods
|
||||
challengeForAuthorizationCode(authorizationCode: string): Promise<string>;
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue