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 {
|
const {
|
||||||
acceptsJson,
|
acceptsJson,
|
||||||
} = require('../../infrastructure/RequestContentTypeDetection')
|
} = require('../../infrastructure/RequestContentTypeDetection')
|
||||||
const { ParallelLoginError } = require('./AuthenticationErrors')
|
const {
|
||||||
|
ParallelLoginError,
|
||||||
|
PasswordReusedError,
|
||||||
|
} = require('./AuthenticationErrors')
|
||||||
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
|
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
|
||||||
const Modules = require('../../infrastructure/Modules')
|
const Modules = require('../../infrastructure/Modules')
|
||||||
const { expressify } = require('@overleaf/promise-utils')
|
const { expressify } = require('@overleaf/promise-utils')
|
||||||
|
@ -218,6 +221,28 @@ const AuthenticationController = {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
if (error instanceof ParallelLoginError) {
|
if (error instanceof ParallelLoginError) {
|
||||||
return done(null, false, { status: 429 })
|
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)
|
return done(error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,19 @@ function _metricsForSuccessfulPasswordMatch(password) {
|
||||||
|
|
||||||
const AuthenticationManager = {
|
const AuthenticationManager = {
|
||||||
_checkUserPassword(query, password, callback) {
|
_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
|
// Using Mongoose for legacy reasons here. The returned User instance
|
||||||
// gets serialized into the session and there may be subtle differences
|
// gets serialized into the session and there may be subtle differences
|
||||||
// between the user returned by Mongoose vs mongodb (such as default values)
|
// between the user returned by Mongoose vs mongodb (such as default values)
|
||||||
|
@ -179,7 +192,6 @@ const AuthenticationManager = {
|
||||||
return callback(err)
|
return callback(err)
|
||||||
}
|
}
|
||||||
callback(null, user)
|
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,
|
// 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.
|
// because it is the same as the existing password.
|
||||||
AuthenticationManager._checkUserPassword(
|
AuthenticationManager._checkUserPasswordWithOutHIBPCheck(
|
||||||
{ _id: user._id },
|
{ _id: user._id },
|
||||||
password,
|
password,
|
||||||
(err, _user, match) => {
|
(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' } }])}
|
| !{translate('email_or_password_wrong_try_again_or_reset', {}, [{ name: 'a', attrs: { href: '/user/password/reset', 'aria-describedby': 'resetPasswordDescription' } }])}
|
||||||
span.sr-only(id='resetPasswordDescription')
|
span.sr-only(id='resetPasswordDescription')
|
||||||
| #{translate('reset_password_link')}
|
| #{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
|
.form-group
|
||||||
input.form-control(
|
input.form-control(
|
||||||
type='email',
|
type='email',
|
||||||
|
|
|
@ -1338,6 +1338,7 @@
|
||||||
"please_reconfirm_your_affiliation_before_making_this_primary": "Please confirm your affiliation before making this the primary.",
|
"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_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_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_file": "Please Select a File",
|
||||||
"please_select_a_project": "Please Select a Project",
|
"please_select_a_project": "Please Select a Project",
|
||||||
"please_select_an_output_file": "Please Select an Output File",
|
"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 MockHaveIBeenPwnedApiClass = require('./mocks/MockHaveIBeenPwnedApi')
|
||||||
const { db } = require('../../../app/src/infrastructure/mongodb')
|
const { db } = require('../../../app/src/infrastructure/mongodb')
|
||||||
const { getMetric } = require('./helpers/metrics').promises
|
const { getMetric } = require('./helpers/metrics').promises
|
||||||
const sleep = require('util').promisify(setTimeout)
|
|
||||||
|
|
||||||
let MockHaveIBeenPwnedApi
|
let MockHaveIBeenPwnedApi
|
||||||
before(function () {
|
before(function () {
|
||||||
MockHaveIBeenPwnedApi = MockHaveIBeenPwnedApiClass.instance()
|
MockHaveIBeenPwnedApi = MockHaveIBeenPwnedApiClass.instance()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function letPasswordCheckRunInBackground() {
|
|
||||||
await sleep(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMetricReUsed() {
|
async function getMetricReUsed() {
|
||||||
return getMetric(
|
return getMetric(
|
||||||
line => line.includes('password_re_use') && line.includes('re-used')
|
line => line.includes('password_re_use') && line.includes('re-used')
|
||||||
|
@ -83,14 +78,18 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
})
|
})
|
||||||
beforeEach('create the user', async function () {
|
beforeEach('create the user', async function () {
|
||||||
await user.ensureUserExists()
|
await user.ensureUserExists()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
beforeEach('fetch previous count', async function () {
|
beforeEach('fetch previous count', async function () {
|
||||||
previous = await getMetricReUsed()
|
previous = await getMetricReUsed()
|
||||||
})
|
})
|
||||||
beforeEach('login', async function () {
|
beforeEach('login', async function () {
|
||||||
|
try {
|
||||||
await user.loginNoUpdate()
|
await user.loginNoUpdate()
|
||||||
await letPasswordCheckRunInBackground()
|
} 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 () {
|
it('should track the weak password', async function () {
|
||||||
const after = await getMetricReUsed()
|
const after = await getMetricReUsed()
|
||||||
|
@ -105,14 +104,12 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
})
|
})
|
||||||
beforeEach('create the user', async function () {
|
beforeEach('create the user', async function () {
|
||||||
await user.ensureUserExists()
|
await user.ensureUserExists()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
beforeEach('fetch previous count', async function () {
|
beforeEach('fetch previous count', async function () {
|
||||||
previous = await getMetricUnique()
|
previous = await getMetricUnique()
|
||||||
})
|
})
|
||||||
beforeEach('login', async function () {
|
beforeEach('login', async function () {
|
||||||
await user.loginNoUpdate()
|
await user.loginNoUpdate()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
it('should track the strong password', async function () {
|
it('should track the strong password', async function () {
|
||||||
const after = await getMetricUnique()
|
const after = await getMetricUnique()
|
||||||
|
@ -127,14 +124,12 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
})
|
})
|
||||||
beforeEach('create the user', async function () {
|
beforeEach('create the user', async function () {
|
||||||
await user.ensureUserExists()
|
await user.ensureUserExists()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
beforeEach('fetch previous count', async function () {
|
beforeEach('fetch previous count', async function () {
|
||||||
previous = await getMetricFailure()
|
previous = await getMetricFailure()
|
||||||
})
|
})
|
||||||
beforeEach('login', async function () {
|
beforeEach('login', async function () {
|
||||||
await user.loginNoUpdate()
|
await user.loginNoUpdate()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
it('should track the failure to collect a score', async function () {
|
it('should track the failure to collect a score', async function () {
|
||||||
const after = await getMetricFailure()
|
const after = await getMetricFailure()
|
||||||
|
@ -152,7 +147,6 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
})
|
})
|
||||||
beforeEach('create the user', async function () {
|
beforeEach('create the user', async function () {
|
||||||
await user.ensureUserExists()
|
await user.ensureUserExists()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
beforeEach('fetch previous counts', async function () {
|
beforeEach('fetch previous counts', async function () {
|
||||||
previous = {
|
previous = {
|
||||||
|
@ -166,16 +160,19 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
await user.loginWithEmailPassword(user.email, 'aLeakedPassword42')
|
await user.loginWithEmailPassword(user.email, 'aLeakedPassword42')
|
||||||
expect.fail('expected the login request to fail')
|
expect.fail('expected the login request to fail')
|
||||||
} catch (err) {
|
} 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({
|
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({
|
expect(previous).to.deep.equal({
|
||||||
reUsed: await getMetricReUsed(),
|
reUsed: (await getMetricReUsed()) - 1,
|
||||||
unique: await getMetricUnique(),
|
unique: await getMetricUnique(),
|
||||||
failure: await getMetricFailure(),
|
failure: await getMetricFailure(),
|
||||||
})
|
})
|
||||||
|
@ -192,7 +189,6 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
})
|
})
|
||||||
beforeEach('create the user', async function () {
|
beforeEach('create the user', async function () {
|
||||||
await user.ensureUserExists()
|
await user.ensureUserExists()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
beforeEach('fetch previous count', async function () {
|
beforeEach('fetch previous count', async function () {
|
||||||
previous = await getMetricReUsed()
|
previous = await getMetricReUsed()
|
||||||
|
@ -207,7 +203,6 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
it('should track the weak password', async function () {
|
it('should track the weak password', async function () {
|
||||||
const after = await getMetricReUsed()
|
const after = await getMetricReUsed()
|
||||||
|
@ -221,7 +216,6 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
})
|
})
|
||||||
beforeEach('create the user', async function () {
|
beforeEach('create the user', async function () {
|
||||||
await user.ensureUserExists()
|
await user.ensureUserExists()
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
beforeEach('fetch previous count', async function () {
|
beforeEach('fetch previous count', async function () {
|
||||||
previous = await getMetricUnique()
|
previous = await getMetricUnique()
|
||||||
|
@ -229,7 +223,6 @@ describe('HaveIBeenPwnedApi', function () {
|
||||||
beforeEach('set password', async function () {
|
beforeEach('set password', async function () {
|
||||||
const response = await resetPassword('a-strong-new-password')
|
const response = await resetPassword('a-strong-new-password')
|
||||||
expect(response.statusCode).to.equal(200)
|
expect(response.statusCode).to.equal(200)
|
||||||
await letPasswordCheckRunInBackground()
|
|
||||||
})
|
})
|
||||||
it('should track the strong password', async function () {
|
it('should track the strong password', async function () {
|
||||||
const after = await getMetricUnique()
|
const after = await getMetricUnique()
|
||||||
|
|
Loading…
Reference in a new issue