UX: show error when hitting the rate limit on password reset

This commit is contained in:
Régis Hanol 2018-10-22 19:00:30 +02:00
parent db26fe1527
commit 3e232412e3
5 changed files with 62 additions and 17 deletions

View File

@ -41,8 +41,7 @@ export default Ember.Controller.extend(PasswordValidation, {
second_factor_token: this.get("secondFactor"), second_factor_token: this.get("secondFactor"),
second_factor_method: this.get("secondFactorMethod") second_factor_method: this.get("secondFactorMethod")
} }
}) }).then(result => {
.then(result => {
if (result.success) { if (result.success) {
this.set("successMessage", result.message); this.set("successMessage", result.message);
this.set("redirectTo", result.redirect_to); this.set("redirectTo", result.redirect_to);
@ -83,8 +82,12 @@ export default Ember.Controller.extend(PasswordValidation, {
} }
} }
}) })
.catch(error => { .catch(e => {
throw new Error(error); if (e.jqXHR && e.jqXHR.status === 429) {
this.set("errorMessage", I18n.t("user.second_factor.rate_limit"));
} else {
throw new Error(e);
}
}); });
}, },

View File

@ -18,7 +18,15 @@
<form> <form>
{{#if secondFactorRequired}} {{#if secondFactorRequired}}
{{#second-factor-form secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} {{#second-factor-form secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}}
{{text-field value=secondFactor id="second-factor" autocorrect="off" autocapitalize="off" autofocus="autofocus" secondFactorMethod=secondFactorMethod}} {{text-field
id="second-factor"
value=secondFactor
autocorrect="off"
autocapitalize="off"
autofocus="autofocus"
maxlength="6"
secondFactorMethod=secondFactorMethod
}}
{{/second-factor-form}} {{/second-factor-form}}
{{d-button action="submit" class='btn-primary' label='submit'}} {{d-button action="submit" class='btn-primary' label='submit'}}
{{else}} {{else}}

View File

@ -453,12 +453,11 @@ class UsersController < ApplicationController
token = params[:token] token = params[:token]
if EmailToken.valid_token_format?(token) if EmailToken.valid_token_format?(token)
@user = @user = if request.put?
if request.put? EmailToken.confirm(token)
EmailToken.confirm(token) else
else EmailToken.confirmable(token)&.user
EmailToken.confirmable(token)&.user end
end
if @user if @user
secure_session["password-#{token}"] = @user.id secure_session["password-#{token}"] = @user.id
@ -468,9 +467,15 @@ class UsersController < ApplicationController
end end
end end
totp_enabled = @user&.totp_enabled? second_factor_token = params[:second_factor_token]
second_factor_method = params[:second_factor_method].to_i
if !totp_enabled || @user.authenticate_second_factor(params[:second_factor_token], params[:second_factor_method].to_i) if second_factor_token.present? && second_factor_token[/\d{6}/] && UserSecondFactor.methods[second_factor_method]
RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed!
second_factor_authenticated = @user&.authenticate_second_factor(second_factor_token, second_factor_method)
end
if second_factor_authenticated || !@user&.totp_enabled?
secure_session["second-factor-#{token}"] = "true" secure_session["second-factor-#{token}"] = "true"
end end
@ -479,13 +484,10 @@ class UsersController < ApplicationController
if !@user if !@user
@error = I18n.t('password_reset.no_token') @error = I18n.t('password_reset.no_token')
elsif request.put? elsif request.put?
@invalid_password = params[:password].blank? || params[:password].length > User.max_password_length
if !valid_second_factor if !valid_second_factor
RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed!
@user.errors.add(:user_second_factors, :invalid) @user.errors.add(:user_second_factors, :invalid)
@error = I18n.t('login.invalid_second_factor_code') @error = I18n.t('login.invalid_second_factor_code')
elsif @invalid_password elsif @invalid_password = params[:password].blank? || params[:password].size > User.max_password_length
@user.errors.add(:password, :invalid) @user.errors.add(:password, :invalid)
else else
@user.password = params[:password] @user.password = params[:password]
@ -547,6 +549,8 @@ class UsersController < ApplicationController
end end
end end
end end
rescue RateLimiter::LimitExceeded => e
render_rate_limit_error(e)
end end
def confirm_email_token def confirm_email_token

View File

@ -771,6 +771,7 @@ en:
enable: "Enable <a href>two factor authentication</a> for enhanced account security" enable: "Enable <a href>two factor authentication</a> for enhanced account security"
confirm_password_description: "Please confirm your password to continue" confirm_password_description: "Please confirm your password to continue"
label: "Code" label: "Code"
rate_limit: "Please wait before trying another authentication code."
enable_description: | enable_description: |
Scan this QR code in a supported app (<a href="https://www.google.com/search?q=authenticator+apps+for+android" target="_blank">Android</a> <a href="https://www.google.com/search?q=authenticator+apps+for+ios" target="_blank">iOS</a> and enter your authentication code. Scan this QR code in a supported app (<a href="https://www.google.com/search?q=authenticator+apps+for+android" target="_blank">Android</a> <a href="https://www.google.com/search?q=authenticator+apps+for+ios" target="_blank">iOS</a> and enter your authentication code.
disable_description: "Please enter the authentication code from your app" disable_description: "Please enter the authentication code from your app"

View File

@ -250,6 +250,35 @@ describe UsersController do
expect(UserAuthToken.where(id: user_token.id).count).to eq(1) expect(UserAuthToken.where(id: user_token.id).count).to eq(1)
end end
context "rate limiting" do
before { RateLimiter.clear_all!; RateLimiter.enable }
after { RateLimiter.disable }
it "rate limits reset passwords" do
freeze_time
token = user.email_tokens.create!(email: user.email).token
3.times do
put "/u/password-reset/#{token}", params: {
second_factor_token: 123456,
second_factor_method: 1
}
expect(response.status).to eq(200)
end
put "/u/password-reset/#{token}", params: {
second_factor_token: 123456,
second_factor_method: 1
}
expect(response.status).to eq(429)
end
end
context '2 factor authentication required' do context '2 factor authentication required' do
let!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) } let!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) }