From 2343cece5cc7115f4ca92ce1fa152efc221e4a60 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 10 Jul 2026 19:06:40 +0800 Subject: [PATCH] fix(auth): distinguish Endorser unavailability from auth rejection Return 503 when Endorser cannot be reached and keep 401 for rejected JWTs, with clearer server-side diagnostics and generic client messages. --- src/middleware/auth.ts | 35 ++++++++++++++++++++++++++------ src/services/endorserClient.ts | 37 +++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index c26f902..db38442 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -132,15 +132,38 @@ export async function requireEndorserAuth( return; } - const endorsed = await checkAuth(jwt); - if (!endorsed) { - console.log("[Auth] Endorser verification failed"); - res.status(401).json({ + const result = await checkAuth(jwt); + if (result.ok) { + next(); + return; + } + + const errorTime = new Date().toISOString(); + const did = req.did ?? "(unknown)"; + + if (result.reason === "unavailable") { + console.log("[Auth] Endorser unavailable"); + console.error( + "[Auth] Endorser auth check unavailable at", + errorTime + ", did:", + did + ); + res.status(503).json({ success: false, - message: "Unauthorized", + message: + "Authentication service unavailable. See server logs at " + errorTime, }); return; } - next(); + console.log("[Auth] Endorser verification failed"); + console.error( + "[Auth] Endorser rejected JWT at", + errorTime + ", did:", + did + ); + res.status(401).json({ + success: false, + message: "Unauthorized. See server logs at " + errorTime, + }); } diff --git a/src/services/endorserClient.ts b/src/services/endorserClient.ts index d87ab74..2179bb0 100644 --- a/src/services/endorserClient.ts +++ b/src/services/endorserClient.ts @@ -3,6 +3,10 @@ import { errorMessage } from "../util/formatElapsed.js"; const RATE_LIMITS_PATH = "/api/report/rateLimits"; +export type EndorserAuthResult = + | { ok: true } + | { ok: false; reason: "unauthorized" | "unavailable" }; + function rateLimitsUrl(): string { const base = ENDORSER_URL.replace(/\/+$/, ""); return `${base}${RATE_LIMITS_PATH}`; @@ -12,13 +16,15 @@ function rateLimitsUrl(): string { * Confirms a JWT is accepted by the Endorser server. * * Calls GET /api/report/rateLimits with the JWT as a Bearer token. - * Returns true on success, false on auth or request failure. + * Distinguishes auth rejection from Endorser unavailability. * Does not expose HTTP status or response bodies to callers. */ -export async function checkAuth(jwt: string): Promise { +export async function checkAuth(jwt: string): Promise { + const url = rateLimitsUrl(); + let response: Response; try { - response = await fetch(rateLimitsUrl(), { + response = await fetch(url, { method: "GET", headers: { Authorization: `Bearer ${jwt}`, @@ -26,19 +32,32 @@ export async function checkAuth(jwt: string): Promise { }); } catch (err) { console.error( - "[Endorser] Auth check request failed:", + "[Endorser] Auth check request failed for", + url + ":", errorMessage(err) ); - return false; + return { ok: false, reason: "unavailable" }; } - if (!response.ok) { + if (response.ok) { + return { ok: true }; + } + + // 5xx: Endorser is up but unhealthy; treat as unavailable. + if (response.status >= 500) { console.error( - "[Endorser] Auth check failed with status", + "[Endorser] Auth check unavailable for", + url + ", status", response.status ); - return false; + return { ok: false, reason: "unavailable" }; } - return true; + // 4xx: JWT rejected or user not registered on Endorser. + console.error( + "[Endorser] Auth check rejected for", + url + ", status", + response.status + ); + return { ok: false, reason: "unauthorized" }; }