From f26d2a59ae3fe2caf98690cf1caea06d1d70c743 Mon Sep 17 00:00:00 2001 From: Fiz <34069775+Hafizzle@users.noreply.github.com> Date: Tue, 15 Jul 2025 09:49:11 -0500 Subject: [PATCH] add InvalidCredentialsError with language-agnostic code (#15472) * add InvalidCredentialsError with language-agnostic code and update backend & web logout logic * error.code in API error responses Updated the error handler to serialize responseErr.code as the JSON error field, falling back to responseErr.name when no code is set. * fix(lint): whitespace and missing def * fix(lint): missed one * add InvalidCredentialsError case for bad token Add test verifying that auth middleware throws InvalidCredentialsError with code "invalid_credentials" and correct translated message when the API token is invalid. * fix(test): user fields implicitly required --------- Co-authored-by: Kalista Payne --- test/api/unit/middlewares/auth.test.js | 15 ++++++++++++++ website/client/src/app.vue | 7 +++---- website/server/libs/errors.js | 24 ++++++++++++++++++++++ website/server/middlewares/auth.js | 3 ++- website/server/middlewares/errorHandler.js | 2 +- 5 files changed, 45 insertions(+), 6 deletions(-) diff --git a/test/api/unit/middlewares/auth.test.js b/test/api/unit/middlewares/auth.test.js index f4a9324189..b75e339d63 100644 --- a/test/api/unit/middlewares/auth.test.js +++ b/test/api/unit/middlewares/auth.test.js @@ -60,5 +60,20 @@ describe('auth middleware', () => { return done(); }); }); + + it('errors with InvalidCredentialsError and code when token is wrong', done => { + const authWithHeaders = authWithHeadersFactory({ userFieldsToExclude: [] }); + + req.headers['x-api-user'] = user._id; + req.headers['x-api-key'] = 'totally-wrong-token'; + + authWithHeaders(req, res, err => { + expect(err).to.exist; + expect(err.name).to.equal('InvalidCredentialsError'); + expect(err.code).to.equal('invalid_credentials'); + expect(err.message).to.equal(res.t('invalidCredentials')); + return done(); + }); + }); }); }); diff --git a/website/client/src/app.vue b/website/client/src/app.vue index 839b297e0a..5b72ea8782 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -223,11 +223,10 @@ export default { const errorData = error.response.data; const errorMessage = errorData.message || errorData; + const errorCode = errorData.error; - // Check for conditions to reset the user auth - // TODO use a specific error like NotificationNotFound instead of checking for the string - const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.']; - if (invalidUserMessage.indexOf(errorMessage) !== -1) { + // If 'invalid_credentials' signaled, force logout + if (error.response.status === 401 && errorCode === 'invalid_credentials') { this.$store.dispatch('auth:logout', { redirectToLogin: true }); return null; } diff --git a/website/server/libs/errors.js b/website/server/libs/errors.js index 2b570dbb7e..0e3a2f0486 100644 --- a/website/server/libs/errors.js +++ b/website/server/libs/errors.js @@ -117,3 +117,27 @@ export class InternalServerError extends CustomError { this.message = customMessage || 'An unexpected error occurred.'; } } + +/** + * @apiDefine InvalidCredentials + * @apiError InvalidCredentials The user’s credentials are no longer valid. + * + * @apiNote + * The 'invalid_credentials' error code is language-agnostic: + * clients should use this code (regardless of locale or translated message) + * to unambiguously trigger a user logout. + * + * @apiErrorExample Error-Response: + * HTTP/1.1 401 Unauthorized + * { + * "error": "invalid_credentials", + * "message": "There is no account that uses those credentials." + * } + */ +export class InvalidCredentialsError extends NotAuthorized { + constructor (message) { + super(message); + this.name = this.constructor.name; + this.code = 'invalid_credentials'; + } +} diff --git a/website/server/middlewares/auth.js b/website/server/middlewares/auth.js index f5d5614847..ff1db6fd5b 100644 --- a/website/server/middlewares/auth.js +++ b/website/server/middlewares/auth.js @@ -2,6 +2,7 @@ import moment from 'moment'; import nconf from 'nconf'; import url from 'url'; import { + InvalidCredentialsError, NotAuthorized, } from '../libs/errors'; import { @@ -81,7 +82,7 @@ export function authWithHeaders (options = {}) { .exec() .then(user => { if (!user || apiToken !== user.apiToken) { - throw new NotAuthorized(res.t('invalidCredentials')); + throw new InvalidCredentialsError(res.t('invalidCredentials')); } if (user.auth.blocked) { diff --git a/website/server/middlewares/errorHandler.js b/website/server/middlewares/errorHandler.js index bd317a2b4a..c3cdf11fab 100644 --- a/website/server/middlewares/errorHandler.js +++ b/website/server/middlewares/errorHandler.js @@ -82,7 +82,7 @@ export default function errorHandler (err, req, res, next) { // eslint-disable-l const jsonRes = { success: false, - error: responseErr.name, + error: responseErr.code || responseErr.name, message: responseErr.message, };