From 868d562d96ba768b96bd2d0ec10591646a992fce Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Mon, 21 Jul 2025 11:53:05 +0200 Subject: [PATCH] Support password-fallbackPassword array in requireBasicAuth (#27237) GitOrigin-RevId: 33b15a05996bfa0190041f347772867a9667e2ca --- .../AuthenticationController.js | 17 +- .../AuthenticationControllerTests.js | 327 ++++++++++++++++++ 2 files changed, 343 insertions(+), 1 deletion(-) diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.js b/services/web/app/src/Features/Authentication/AuthenticationController.js index 7a97d2ac9c..99c418df1b 100644 --- a/services/web/app/src/Features/Authentication/AuthenticationController.js +++ b/services/web/app/src/Features/Authentication/AuthenticationController.js @@ -36,7 +36,22 @@ function send401WithChallenge(res) { function checkCredentials(userDetailsMap, user, password) { const expectedPassword = userDetailsMap.get(user) const userExists = userDetailsMap.has(user) && expectedPassword // user exists with a non-null password - const isValid = userExists && tsscmp(expectedPassword, password) + + let isValid = false + if (userExists) { + if (Array.isArray(expectedPassword)) { + const isValidPrimary = Boolean( + expectedPassword[0] && tsscmp(expectedPassword[0], password) + ) + const isValidFallback = Boolean( + expectedPassword[1] && tsscmp(expectedPassword[1], password) + ) + isValid = isValidPrimary || isValidFallback + } else { + isValid = tsscmp(expectedPassword, password) + } + } + if (!isValid) { logger.err({ user }, 'invalid login details') } diff --git a/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js index 0e4f675b1b..1fa3aba6a6 100644 --- a/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js +++ b/services/web/test/unit/src/Authentication/AuthenticationControllerTests.js @@ -1500,4 +1500,331 @@ describe('AuthenticationController', function () { }) }) }) + + describe('checkCredentials', function () { + beforeEach(function () { + this.userDetailsMap = new Map() + this.logger.err = sinon.stub() + this.Metrics.inc = sinon.stub() + }) + + describe('with valid credentials', function () { + describe('single password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', 'correctpassword') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'correctpassword' + ) + }) + + it('should return true', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + + it('should record success metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'pass', + } + ) + }) + }) + + describe('array with primary password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'primary' + ) + }) + + it('should return true', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + + it('should record success metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'pass', + } + ) + }) + }) + + describe('array with fallback password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'fallback' + ) + }) + + it('should return true', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + + it('should record success metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'pass', + } + ) + }) + }) + }) + + describe('with invalid credentials', function () { + describe('unknown user', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', 'correctpassword') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'unknownuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'unknownuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'unknown-user', + status: 'fail', + } + ) + }) + }) + + describe('wrong password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', 'correctpassword') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'wrongpassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'fail', + } + ) + }) + }) + + describe('wrong password with array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'wrongpassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'known-user', + status: 'fail', + } + ) + }) + }) + + describe('null user entry', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', null) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics for unknown user', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'unknown-user', + status: 'fail', + } + ) + }) + }) + + describe('empty primary password in array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['', 'fallback']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'fallback' + ) + }) + + it('should return true with fallback password', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + }) + + describe('empty fallback password in array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['primary', '']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'primary' + ) + }) + + it('should return true with primary password', function () { + this.result.should.equal(true) + }) + + it('should not log an error', function () { + this.logger.err.called.should.equal(false) + }) + }) + + describe('both passwords empty in array', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', ['', '']) + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + }) + + describe('empty single password', function () { + beforeEach(function () { + this.userDetailsMap.set('testuser', '') + this.result = this.AuthenticationController.checkCredentials( + this.userDetailsMap, + 'testuser', + 'anypassword' + ) + }) + + it('should return false', function () { + this.result.should.equal(false) + }) + + it('should log an error', function () { + this.logger.err.should.have.been.calledWith( + { user: 'testuser' }, + 'invalid login details' + ) + }) + + it('should record failure metrics for unknown user', function () { + this.Metrics.inc.should.have.been.calledWith( + 'security.http-auth.check-credentials', + 1, + { + path: 'unknown-user', + status: 'fail', + } + ) + }) + }) + }) + }) })