mirror of
https://github.com/discourse/discourse.git
synced 2025-06-05 12:44:50 +08:00
FEATURE: Implement 2factor login TOTP
implemented review items. Blocking previous codes - valid 2-factor auth tokens can only be authenticated once/30 seconds. I played with updating the “last used” any time the token was attempted but that seemed to be overkill, and frustrating as to why a token would fail. Translatable texts. Move second factor logic to a helper class. Move second factor specific controller endpoints to its own controller. Move serialization logic for 2-factor details in admin user views. Add a login ember component for de-duplication Fix up code formatting Change verbiage of google authenticator add controller tests: second factor controller tests change email tests change password tests admin login tests add qunit tests - password reset, preferences fix: check for 2factor on change email controller fix: email controller - only show second factor errors on attempt fix: check against 'true' to enable second factor. Add modal for explaining what 2fa with links to Google Authenticator/FreeOTP add two factor to email signin link rate limit if second factor token present add rate limiter test for second factor attempts
This commit is contained in:
@ -24,6 +24,20 @@ acceptance("Password Reset", {
|
||||
return response({success: "OK", message: I18n.t('password_reset.success')});
|
||||
}
|
||||
});
|
||||
|
||||
server.get('/u/confirm-email-token/requiretwofactor.json', () => { //eslint-disable-line
|
||||
return response({success: "OK"});
|
||||
});
|
||||
server.put('/u/password-reset/requiretwofactor.json', request => { //eslint-disable-line
|
||||
const body = parsePostData(request.requestBody);
|
||||
if (body.password === "perf3ctly5ecur3" && body.second_factor_token === "123123") {
|
||||
return response({success: "OK", message: I18n.t('password_reset.success')});
|
||||
} else if (body.second_factor_token === "123123") {
|
||||
return response({success: false, errors: {password: ["invalid"]}});
|
||||
} else {
|
||||
return response({success: false, message: "invalid token", errors: {second_factor: ["invalid token"]}});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -58,4 +72,35 @@ QUnit.test("Password Reset Page", assert => {
|
||||
andThen(() => {
|
||||
assert.ok(!exists(".password-reset form"), "form is gone");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Password Reset Page With Second Factor", assert => {
|
||||
PreloadStore.store('password_reset', {is_developer: false, second_factor_required: true});
|
||||
|
||||
visit("/u/password-reset/requiretwofactor");
|
||||
andThen(() => {
|
||||
assert.notOk(exists("#new-account-password"), "does not show the input");
|
||||
assert.ok(exists("#second-factor"), "shows the second factor prompt");
|
||||
});
|
||||
|
||||
fillIn('#second-factor', '0000');
|
||||
|
||||
click('.password-reset form button');
|
||||
andThen(() => {
|
||||
assert.ok(exists(".alert-error"), "shows 2 factor error");
|
||||
assert.ok(find(".alert-error").html().indexOf("invalid token") > -1, "server validation error message shows");
|
||||
});
|
||||
|
||||
fillIn('#second-factor', '123123');
|
||||
click('.password-reset form button');
|
||||
andThen(() => {
|
||||
assert.notOk(exists(".alert-error"), "hides error");
|
||||
assert.ok(exists("#new-account-password"), "shows the input");
|
||||
});
|
||||
|
||||
fillIn('.password-reset input', 'perf3ctly5ecur3');
|
||||
click('.password-reset form button');
|
||||
andThen(() => {
|
||||
assert.ok(!exists(".password-reset form"), "form is gone");
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,20 @@
|
||||
import { acceptance } from "helpers/qunit-helpers";
|
||||
acceptance("User Preferences", { loggedIn: true });
|
||||
acceptance("User Preferences", {
|
||||
loggedIn: true,
|
||||
beforeEach() {
|
||||
const response = (object) => {
|
||||
return [
|
||||
200,
|
||||
{"Content-Type": "application/json"},
|
||||
object
|
||||
];
|
||||
};
|
||||
|
||||
server.post('/second_factor/create', () => { //eslint-disable-line
|
||||
return response({key: "rcyryaqage3jexfj", qr: '<div id="test-qr">qr-code</div>'});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("update some fields", assert => {
|
||||
visit("/u/eviltrout/preferences");
|
||||
@ -73,3 +88,16 @@ QUnit.test("email", assert => {
|
||||
assert.equal(find('.tip.bad').text().trim(), I18n.t('user.email.invalid'), 'it should display invalid email tip');
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("second factor", assert => {
|
||||
visit("/u/eviltrout/preferences/second-factor");
|
||||
andThen(() => {
|
||||
assert.ok(exists("#password"), "it has a password input");
|
||||
});
|
||||
fillIn('#password', 'secrets');
|
||||
click(".user-content .btn-primary");
|
||||
andThen(() => {
|
||||
assert.ok(exists("#test-qr"), "shows qr code");
|
||||
assert.notOk(exists("#password"), "it hides the password input");
|
||||
});
|
||||
});
|
||||
|
@ -76,6 +76,32 @@ QUnit.test("sign in - not activated - edit email", assert => {
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("second factor", assert => {
|
||||
visit("/");
|
||||
click("header .login-button");
|
||||
andThen(() => {
|
||||
assert.ok(exists('.login-modal'), "it shows the login modal");
|
||||
});
|
||||
|
||||
// Login with username and password only
|
||||
fillIn('#login-account-name', 'eviltrout');
|
||||
fillIn('#login-account-password', 'need-second-factor');
|
||||
click('.modal-footer .btn-primary');
|
||||
andThen(() => {
|
||||
assert.not(exists('#modal-alert:visible'), 'it hides the login error');
|
||||
assert.not(exists('#credentials:visible'), 'it hides the username and password prompt');
|
||||
assert.ok(exists('#second-factor:visible'), 'it displays the second factor prompt');
|
||||
assert.not(exists('.modal-footer .btn-primary:disabled'), "enables the login button");
|
||||
});
|
||||
|
||||
// Login with username, password, and token
|
||||
fillIn('#login-second-factor', '123456');
|
||||
click('.modal-footer .btn-primary');
|
||||
andThen(() => {
|
||||
assert.ok(exists('.modal-footer .btn-primary:disabled'), "disables the login button");
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("create account", assert => {
|
||||
visit("/");
|
||||
click("header .sign-up-button");
|
||||
@ -106,4 +132,4 @@ QUnit.test("create account", assert => {
|
||||
andThen(() => {
|
||||
assert.ok(exists('.modal-footer .btn-primary:disabled'), "create account is disabled");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -227,6 +227,16 @@ export default function() {
|
||||
current_email: 'current@example.com' });
|
||||
}
|
||||
|
||||
if (data.password === 'need-second-factor') {
|
||||
if (data.second_factor_token) {
|
||||
return response({username: 'eviltrout'});
|
||||
}
|
||||
return response({ error: "Invalid Second Factor",
|
||||
reason: "invalid_second_factor",
|
||||
sent_to_email: 'eviltrout@example.com',
|
||||
current_email: 'current@example.com' });
|
||||
}
|
||||
|
||||
return response(400, {error: 'invalid login'});
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user