From e3f068bb508d6106cefe9d5bd3d0dcba79c9d0aa Mon Sep 17 00:00:00 2001 From: "aleksandar.kovacic@t-systems.com" Date: Thu, 12 Feb 2026 13:17:11 +0100 Subject: [PATCH] fix: return attempted security scheme error --- src/middlewares/openapi.security.ts | 13 ++++++++++--- test/resources/security.yaml | 11 +++++++++++ test/security.handlers.spec.ts | 25 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/middlewares/openapi.security.ts b/src/middlewares/openapi.security.ts index 68c28fb3..e30e7f95 100644 --- a/src/middlewares/openapi.security.ts +++ b/src/middlewares/openapi.security.ts @@ -22,6 +22,7 @@ interface SecurityHandlerResult { success: boolean; status?: number; error?: string; + attempted?: boolean; // true if credentials were provided and handler was called } function extractErrorsFromResults(results: (SecurityHandlerResult | SecurityHandlerResult[])[]) { @@ -87,7 +88,10 @@ export function security( next(); } else { const errors = extractErrorsFromResults(results); - throw errors[0]; + // Prioritize errors where authentication was actually attempted + const attemptedErrors = errors.filter(e => e.attempted); + const errorToThrow = attemptedErrors.length > 0 ? attemptedErrors[0] : errors[0]; + throw errorToThrow; } } catch (e) { const message = e?.error?.message || 'unauthorized'; @@ -138,7 +142,7 @@ class SecuritySchemes { } return Promise.all( Object.keys(s).map(async (securityKey) => { - + let validatorPassed = false; try { const scheme = this.securitySchemes[securityKey]; const handler = this.securityHandlers?.[securityKey] ?? fallbackHandler; @@ -157,6 +161,8 @@ class SecuritySchemes { throw new InternalServerError({ message }); } new AuthValidator(req, scheme, scopes).validate(); + // If we reach here, validation passed and credentials were provided + validatorPassed = true; // expected handler results are: // - throw exception, // - return true, @@ -167,7 +173,7 @@ class SecuritySchemes { const securityScheme = scheme; const success = await handler(req, scopes, securityScheme); if (success === true) { - return { success }; + return { success, attempted: true }; } else { throw Error(); } @@ -176,6 +182,7 @@ class SecuritySchemes { success: false, status: e.status ?? 401, error: e, + attempted: validatorPassed, }; } }), diff --git a/test/resources/security.yaml b/test/resources/security.yaml index a482783f..6d796243 100644 --- a/test/resources/security.yaml +++ b/test/resources/security.yaml @@ -19,6 +19,17 @@ paths: description: OK "401": description: unauthorized + + /bearer_or_apikey: + get: + security: + - BearerAuth: [] + - ApiKeyAuth: [] + responses: + "200": + description: OK + "401": + description: unauthorized /no_security: get: diff --git a/test/security.handlers.spec.ts b/test/security.handlers.spec.ts index bc64838a..660100a4 100644 --- a/test/security.handlers.spec.ts +++ b/test/security.handlers.spec.ts @@ -64,6 +64,7 @@ describe('security.handlers', () => { .get(`/cookie_auth`, (req, res) => {res.json({ logged_in: true })}) .get(`/oauth2`, (req, res) => {res.json({ logged_in: true })}) .get(`/openid`, (req, res) => {res.json({ logged_in: true })}) + .get(`/bearer_or_apikey`, (req, res) => {res.json({ logged_in: true })}) .get(`/api_key_or_anonymous`, (req, res) =>{ res.json({ logged_in: true }) }) @@ -412,6 +413,30 @@ describe('security.handlers', () => { return request(app).get(`${basePath}/api_key_or_anonymous`).expect(200); }); + it('should return error from attempted security scheme, not first defined scheme', async () => { + // This test verifies the fix for the issue where when multiple security schemes + // are defined (e.g., BearerAuth OR ApiKeyAuth) and only one is attempted (ApiKeyAuth), + // the error from the attempted scheme should be returned, not the error from the + // first defined scheme (BearerAuth). + const validateSecurity = eovConf.validateSecurity as ValidateSecurityOpts; + (validateSecurity.handlers! as any).BearerAuth = (req, scopes, schema) => true; + (validateSecurity.handlers! as any).ApiKeyAuth = (req, scopes, schema) => { + throw new Error('Invalid API key provided'); + }; + + return request(app) + .get(`${basePath}/bearer_or_apikey`) + .set('X-API-Key', 'wrong-key') // Provide API key but not Bearer token + .expect(401) + .then((r) => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + // Should return the API key error, not the Bearer token error + expect(body.errors[0].message).to.equals('Invalid API key provided'); + }); + }); + it('should return 200 if api_key or anonymous and api key is supplied', async () => { const validateSecurity = eovConf.validateSecurity as ValidateSecurityOpts; (validateSecurity.handlers! as any).ApiKeyAuth = (req, scopes, schema) => true;