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}"`;