mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #17399 from overleaf/jpa-hibp-login
[web] check HIBP on login GitOrigin-RevId: e052926e4d970f9a15821f1ea9c8af46bdab90cb
This commit is contained in:
parent
60d38285ea
commit
001af76f15
5 changed files with 60 additions and 25 deletions
|
@ -23,7 +23,10 @@ const AnalyticsRegistrationSourceHelper = require('../Analytics/AnalyticsRegistr
|
|||
const {
|
||||
acceptsJson,
|
||||
} = require('../../infrastructure/RequestContentTypeDetection')
|
||||
const { ParallelLoginError } = require('./AuthenticationErrors')
|
||||
const {
|
||||
ParallelLoginError,
|
||||
PasswordReusedError,
|
||||
} = require('./AuthenticationErrors')
|
||||
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
|
||||
const Modules = require('../../infrastructure/Modules')
|
||||
const { expressify } = require('@overleaf/promise-utils')
|
||||
|
@ -218,6 +221,28 @@ const AuthenticationController = {
|
|||
if (error != null) {
|
||||
if (error instanceof ParallelLoginError) {
|
||||
return done(null, false, { status: 429 })
|
||||
} else if (error instanceof PasswordReusedError) {
|
||||
const text = `${req.i18n
|
||||
.translate(
|
||||
'password_was_detected_on_a_public_list_of_known_compromised_passwords'
|
||||
)
|
||||
.replace('<0>', '')
|
||||
.replace(
|
||||
'</0>',
|
||||
' (https://haveibeenpwned.com)'
|
||||
)}. ${req.i18n
|
||||
.translate('please_reset_your_password_to_login')
|
||||
.replace('<0>', '')
|
||||
.replace(
|
||||
'</0>',
|
||||
` (${Settings.siteUrl}/user/password/reset)`
|
||||
)}.`
|
||||
return done(null, false, {
|
||||
status: 400,
|
||||
type: 'error',
|
||||
key: 'password-compromised',
|
||||
text,
|
||||
})
|
||||
}
|
||||
return done(error)
|
||||
}
|
||||
|
|
|
@ -60,6 +60,19 @@ function _metricsForSuccessfulPasswordMatch(password) {
|
|||
|
||||
const AuthenticationManager = {
|
||||
_checkUserPassword(query, password, callback) {
|
||||
HaveIBeenPwned.checkPasswordForReuse(password, (err, isPasswordReused) => {
|
||||
if (err) {
|
||||
logger.err({ err }, 'cannot check password for re-use')
|
||||
}
|
||||
if (isPasswordReused) return callback(new PasswordReusedError())
|
||||
AuthenticationManager._checkUserPasswordWithOutHIBPCheck(
|
||||
query,
|
||||
password,
|
||||
callback
|
||||
)
|
||||
})
|
||||
},
|
||||
_checkUserPasswordWithOutHIBPCheck(query, password, callback) {
|
||||
// Using Mongoose for legacy reasons here. The returned User instance
|
||||
// gets serialized into the session and there may be subtle differences
|
||||
// between the user returned by Mongoose vs mongodb (such as default values)
|
||||
|
@ -179,7 +192,6 @@ const AuthenticationManager = {
|
|||
return callback(err)
|
||||
}
|
||||
callback(null, user)
|
||||
HaveIBeenPwned.checkPasswordForReuseInBackground(password)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -333,7 +345,7 @@ const AuthenticationManager = {
|
|||
}
|
||||
// check if we can log in with this password. In which case we should reject it,
|
||||
// because it is the same as the existing password.
|
||||
AuthenticationManager._checkUserPassword(
|
||||
AuthenticationManager._checkUserPasswordWithOutHIBPCheck(
|
||||
{ _id: user._id },
|
||||
password,
|
||||
(err, _user, match) => {
|
||||
|
|
|
@ -15,6 +15,10 @@ block content
|
|||
| !{translate('email_or_password_wrong_try_again_or_reset', {}, [{ name: 'a', attrs: { href: '/user/password/reset', 'aria-describedby': 'resetPasswordDescription' } }])}
|
||||
span.sr-only(id='resetPasswordDescription')
|
||||
| #{translate('reset_password_link')}
|
||||
+customValidationMessage('password-compromised')
|
||||
| !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com', rel: 'noopener noreferrer', target: '_blank'}}])}.
|
||||
|
|
||||
| !{translate('please_reset_your_password_to_login', {}, [{name: 'a', attrs: {href: '/user/password/reset', target: '_blank'}}])}.
|
||||
.form-group
|
||||
input.form-control(
|
||||
type='email',
|
||||
|
|
|
@ -1338,6 +1338,7 @@
|
|||
"please_reconfirm_your_affiliation_before_making_this_primary": "Please confirm your affiliation before making this the primary.",
|
||||
"please_refresh": "Please refresh the page to continue.",
|
||||
"please_request_a_new_password_reset_email_and_follow_the_link": "Please request a new password reset email and follow the link",
|
||||
"please_reset_your_password_to_login": "Please reset your password <0>here</0> to login",
|
||||
"please_select_a_file": "Please Select a File",
|
||||
"please_select_a_project": "Please Select a Project",
|
||||
"please_select_an_output_file": "Please Select an Output File",
|
||||
|
|
|
@ -4,17 +4,12 @@ const User = require('./helpers/User').promises
|
|||
const MockHaveIBeenPwnedApiClass = require('./mocks/MockHaveIBeenPwnedApi')
|
||||
const { db } = require('../../../app/src/infrastructure/mongodb')
|
||||
const { getMetric } = require('./helpers/metrics').promises
|
||||
const sleep = require('util').promisify(setTimeout)
|
||||
|
||||
let MockHaveIBeenPwnedApi
|
||||
before(function () {
|
||||
MockHaveIBeenPwnedApi = MockHaveIBeenPwnedApiClass.instance()
|
||||
})
|
||||
|
||||
async function letPasswordCheckRunInBackground() {
|
||||
await sleep(200)
|
||||
}
|
||||
|
||||
async function getMetricReUsed() {
|
||||
return getMetric(
|
||||
line => line.includes('password_re_use') && line.includes('re-used')
|
||||
|
@ -83,14 +78,18 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
})
|
||||
beforeEach('create the user', async function () {
|
||||
await user.ensureUserExists()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
beforeEach('fetch previous count', async function () {
|
||||
previous = await getMetricReUsed()
|
||||
})
|
||||
beforeEach('login', async function () {
|
||||
await user.loginNoUpdate()
|
||||
await letPasswordCheckRunInBackground()
|
||||
try {
|
||||
await user.loginNoUpdate()
|
||||
} catch (e) {
|
||||
expect(e.message).to.include('password-compromised')
|
||||
return
|
||||
}
|
||||
expect.fail('should have failed login with weak password')
|
||||
})
|
||||
it('should track the weak password', async function () {
|
||||
const after = await getMetricReUsed()
|
||||
|
@ -105,14 +104,12 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
})
|
||||
beforeEach('create the user', async function () {
|
||||
await user.ensureUserExists()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
beforeEach('fetch previous count', async function () {
|
||||
previous = await getMetricUnique()
|
||||
})
|
||||
beforeEach('login', async function () {
|
||||
await user.loginNoUpdate()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
it('should track the strong password', async function () {
|
||||
const after = await getMetricUnique()
|
||||
|
@ -127,14 +124,12 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
})
|
||||
beforeEach('create the user', async function () {
|
||||
await user.ensureUserExists()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
beforeEach('fetch previous count', async function () {
|
||||
previous = await getMetricFailure()
|
||||
})
|
||||
beforeEach('login', async function () {
|
||||
await user.loginNoUpdate()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
it('should track the failure to collect a score', async function () {
|
||||
const after = await getMetricFailure()
|
||||
|
@ -152,7 +147,6 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
})
|
||||
beforeEach('create the user', async function () {
|
||||
await user.ensureUserExists()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
beforeEach('fetch previous counts', async function () {
|
||||
previous = {
|
||||
|
@ -166,16 +160,19 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
await user.loginWithEmailPassword(user.email, 'aLeakedPassword42')
|
||||
expect.fail('expected the login request to fail')
|
||||
} catch (err) {
|
||||
expect(err).to.match(/login failed: status=401/)
|
||||
expect(err).to.match(/login failed: status=400/)
|
||||
expect(err.info.body).to.deep.equal({
|
||||
message: { type: 'error', key: 'invalid-password-retry-or-reset' },
|
||||
message: {
|
||||
type: 'error',
|
||||
key: 'password-compromised',
|
||||
text: `This password was detected on a public list of known compromised passwords (https://haveibeenpwned.com). Please reset your password here (${Settings.siteUrl}/user/password/reset) to login.`,
|
||||
},
|
||||
})
|
||||
}
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
it('should not increment any counter', async function () {
|
||||
it('should increment the counter', async function () {
|
||||
expect(previous).to.deep.equal({
|
||||
reUsed: await getMetricReUsed(),
|
||||
reUsed: (await getMetricReUsed()) - 1,
|
||||
unique: await getMetricUnique(),
|
||||
failure: await getMetricFailure(),
|
||||
})
|
||||
|
@ -192,7 +189,6 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
})
|
||||
beforeEach('create the user', async function () {
|
||||
await user.ensureUserExists()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
beforeEach('fetch previous count', async function () {
|
||||
previous = await getMetricReUsed()
|
||||
|
@ -207,7 +203,6 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
},
|
||||
})
|
||||
)
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
it('should track the weak password', async function () {
|
||||
const after = await getMetricReUsed()
|
||||
|
@ -221,7 +216,6 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
})
|
||||
beforeEach('create the user', async function () {
|
||||
await user.ensureUserExists()
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
beforeEach('fetch previous count', async function () {
|
||||
previous = await getMetricUnique()
|
||||
|
@ -229,7 +223,6 @@ describe('HaveIBeenPwnedApi', function () {
|
|||
beforeEach('set password', async function () {
|
||||
const response = await resetPassword('a-strong-new-password')
|
||||
expect(response.statusCode).to.equal(200)
|
||||
await letPasswordCheckRunInBackground()
|
||||
})
|
||||
it('should track the strong password', async function () {
|
||||
const after = await getMetricUnique()
|
||||
|
|
Loading…
Reference in a new issue