From bf2a59d003cca05cacaefe72372e9c8af4623977 Mon Sep 17 00:00:00 2001 From: Rune Botten Date: Fri, 18 Jul 2025 16:09:04 -0700 Subject: [PATCH] fix: implement RFC 6750 Section 3.1 compliance for missing authentication The requireBearerAuth middleware was incorrectly treating missing Authorization headers as invalid tokens, violating RFC 6750 Section 3.1 which states: "If the request lacks any authentication information, the resource server SHOULD NOT include an error code or other error information." Changes: - Add MissingAuthenticationError class for missing authentication cases - Update bearerAuth middleware to throw MissingAuthenticationError when no Authorization header is present - Handle MissingAuthenticationError by returning WWW-Authenticate header with only realm parameter (no error codes) - Update tests to expect RFC 6750 compliant behavior for missing authentication - Preserve existing behavior for invalid/malformed tokens Before (non-compliant): WWW-Authenticate: Bearer error="invalid_token", error_description="Missing Authorization header" After (RFC 6750 compliant): WWW-Authenticate: Bearer realm="protected" Fixes RFC 6750 compliance issue while maintaining backward compatibility for all other authentication error scenarios. --- src/server/auth/errors.ts | 8 ++++++++ src/server/auth/middleware/bearerAuth.test.ts | 10 +++++----- src/server/auth/middleware/bearerAuth.ts | 13 ++++++++++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/server/auth/errors.ts b/src/server/auth/errors.ts index 791b3b86..2b82b22a 100644 --- a/src/server/auth/errors.ts +++ b/src/server/auth/errors.ts @@ -163,6 +163,13 @@ export class InsufficientScopeError extends OAuthError { static errorCode = "insufficient_scope"; } +/** + * Missing authentication error - The request lack required authentication information. + */ +export class MissingAuthenticationError extends OAuthError { + static errorCode = "missing_authentication"; +} + /** * A utility class for defining one-off error codes */ @@ -196,4 +203,5 @@ export const OAUTH_ERRORS = { [TooManyRequestsError.errorCode]: TooManyRequestsError, [InvalidClientMetadataError.errorCode]: InvalidClientMetadataError, [InsufficientScopeError.errorCode]: InsufficientScopeError, + [MissingAuthenticationError.errorCode]: MissingAuthenticationError, } as const; diff --git a/src/server/auth/middleware/bearerAuth.test.ts b/src/server/auth/middleware/bearerAuth.test.ts index 38639b1d..6bbf04bf 100644 --- a/src/server/auth/middleware/bearerAuth.test.ts +++ b/src/server/auth/middleware/bearerAuth.test.ts @@ -23,6 +23,7 @@ describe("requireBearerAuth middleware", () => { status: jest.fn().mockReturnThis(), json: jest.fn(), set: jest.fn().mockReturnThis(), + send: jest.fn(), }; nextFunction = jest.fn(); jest.spyOn(console, 'error').mockImplementation(() => {}); @@ -207,14 +208,12 @@ describe("requireBearerAuth middleware", () => { expect(mockResponse.status).toHaveBeenCalledWith(401); expect(mockResponse.set).toHaveBeenCalledWith( "WWW-Authenticate", - expect.stringContaining('Bearer error="invalid_token"') - ); - expect(mockResponse.json).toHaveBeenCalledWith( - expect.objectContaining({ error: "invalid_token", error_description: "Missing Authorization header" }) + 'Bearer realm="protected"' ); expect(nextFunction).not.toHaveBeenCalled(); }); + it("should return 401 when Authorization header format is invalid", async () => { mockRequest.headers = { authorization: "InvalidFormat", @@ -336,6 +335,7 @@ describe("requireBearerAuth middleware", () => { expect(nextFunction).not.toHaveBeenCalled(); }); + describe("with resourceMetadataUrl", () => { const resourceMetadataUrl = "https://api.example.com/.well-known/oauth-protected-resource"; @@ -348,7 +348,7 @@ describe("requireBearerAuth middleware", () => { expect(mockResponse.status).toHaveBeenCalledWith(401); expect(mockResponse.set).toHaveBeenCalledWith( "WWW-Authenticate", - `Bearer error="invalid_token", error_description="Missing Authorization header", resource_metadata="${resourceMetadataUrl}"` + `Bearer realm="protected", resource_metadata="${resourceMetadataUrl}"` ); expect(nextFunction).not.toHaveBeenCalled(); }); diff --git a/src/server/auth/middleware/bearerAuth.ts b/src/server/auth/middleware/bearerAuth.ts index 7b6d8f61..a66877e9 100644 --- a/src/server/auth/middleware/bearerAuth.ts +++ b/src/server/auth/middleware/bearerAuth.ts @@ -1,5 +1,5 @@ import { RequestHandler } from "express"; -import { InsufficientScopeError, InvalidTokenError, OAuthError, ServerError } from "../errors.js"; +import { InsufficientScopeError, InvalidTokenError, MissingAuthenticationError, OAuthError, ServerError } from "../errors.js"; import { OAuthTokenVerifier } from "../provider.js"; import { AuthInfo } from "../types.js"; @@ -42,7 +42,7 @@ export function requireBearerAuth({ verifier, requiredScopes = [], resourceMetad try { const authHeader = req.headers.authorization; if (!authHeader) { - throw new InvalidTokenError("Missing Authorization header"); + throw new MissingAuthenticationError("Missing Authorization header"); } const [type, token] = authHeader.split(' '); @@ -73,7 +73,14 @@ export function requireBearerAuth({ verifier, requiredScopes = [], resourceMetad req.auth = authInfo; next(); } catch (error) { - if (error instanceof InvalidTokenError) { + if (error instanceof MissingAuthenticationError) { + // RFC 6750 Section 3.1: Missing authentication should not include error codes + const wwwAuthValue = resourceMetadataUrl + ? `Bearer realm="protected", resource_metadata="${resourceMetadataUrl}"` + : `Bearer realm="protected"`; + res.set("WWW-Authenticate", wwwAuthValue); + res.status(401).send(); + } else if (error instanceof InvalidTokenError) { const wwwAuthValue = resourceMetadataUrl ? `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"` : `Bearer error="${error.errorCode}", error_description="${error.message}"`;