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.
This commit is contained in:
Jose Olarte III
2026-07-10 19:06:40 +08:00
parent f2f0732ef5
commit 2343cece5c
2 changed files with 57 additions and 15 deletions
+29 -6
View File
@@ -132,15 +132,38 @@ export async function requireEndorserAuth(
return; return;
} }
const endorsed = await checkAuth(jwt); const result = await checkAuth(jwt);
if (!endorsed) { if (result.ok) {
console.log("[Auth] Endorser verification failed"); next();
res.status(401).json({ 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, success: false,
message: "Unauthorized", message:
"Authentication service unavailable. See server logs at " + errorTime,
}); });
return; 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,
});
} }
+28 -9
View File
@@ -3,6 +3,10 @@ import { errorMessage } from "../util/formatElapsed.js";
const RATE_LIMITS_PATH = "/api/report/rateLimits"; const RATE_LIMITS_PATH = "/api/report/rateLimits";
export type EndorserAuthResult =
| { ok: true }
| { ok: false; reason: "unauthorized" | "unavailable" };
function rateLimitsUrl(): string { function rateLimitsUrl(): string {
const base = ENDORSER_URL.replace(/\/+$/, ""); const base = ENDORSER_URL.replace(/\/+$/, "");
return `${base}${RATE_LIMITS_PATH}`; return `${base}${RATE_LIMITS_PATH}`;
@@ -12,13 +16,15 @@ function rateLimitsUrl(): string {
* Confirms a JWT is accepted by the Endorser server. * Confirms a JWT is accepted by the Endorser server.
* *
* Calls GET /api/report/rateLimits with the JWT as a Bearer token. * 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. * Does not expose HTTP status or response bodies to callers.
*/ */
export async function checkAuth(jwt: string): Promise<boolean> { export async function checkAuth(jwt: string): Promise<EndorserAuthResult> {
const url = rateLimitsUrl();
let response: Response; let response: Response;
try { try {
response = await fetch(rateLimitsUrl(), { response = await fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${jwt}`, Authorization: `Bearer ${jwt}`,
@@ -26,19 +32,32 @@ export async function checkAuth(jwt: string): Promise<boolean> {
}); });
} catch (err) { } catch (err) {
console.error( console.error(
"[Endorser] Auth check request failed:", "[Endorser] Auth check request failed for",
url + ":",
errorMessage(err) 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( console.error(
"[Endorser] Auth check failed with status", "[Endorser] Auth check unavailable for",
url + ", status",
response.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" };
} }