Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ plugins:
channel: rubocop-0-58
scss-lint:
enabled: true
checks:
HexLength:
enabled: false

exclude_patterns:
- 'db/schema.rb'
Expand Down
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Metrics/ClassLength:
- spec/**/*
- app/controllers/application_controller.rb
- app/controllers/openid_connect/authorization_controller.rb
- app/controllers/saml_idp_controller.rb
- app/controllers/users/confirmations_controller.rb
- app/controllers/users/sessions_controller.rb
- app/controllers/users/two_factor_authentication_controller.rb
Expand Down
8 changes: 4 additions & 4 deletions app/assets/stylesheets/variables/_colors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ $red: #e21c3d !default;
$fuchsia: #f012be !default;
$purple: #b10dc9 !default;
$maroon: #85144b !default;
$white: #fff !default;
$white: #ffffff !default;
$silver: #d9dadb !default;
$gray: #5b616a !default;
$gray-light: #ddd !default;
$gray-light: #dddddd !default;
$gray-lighter: #fafafa !default;
$black: #111 !default;
$black: #111111 !default;
$pink: #eb4d67 !default;
$red: #f00 !default;
$red: #ff0000 !default;
$red-lightest: #fff7f8 !default;
4 changes: 2 additions & 2 deletions app/controllers/account_recovery_setup_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ class AccountRecoverySetupController < ApplicationController
before_action :confirm_two_factor_authenticated

def index
return redirect_to account_url unless piv_cac_enabled_but_not_phone_enabled?
return redirect_to account_url unless piv_cac_enabled_but_not_multiple_mfa_enabled?
@two_factor_options_form = TwoFactorOptionsForm.new(current_user)
@presenter = account_recovery_options_presenter
end

private

def account_recovery_options_presenter
AccountRecoveryOptionsPresenter.new
AccountRecoveryOptionsPresenter.new(current_user, current_sp)
end
end
3 changes: 2 additions & 1 deletion app/controllers/account_reset/request_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def show
def create
analytics.track_event(Analytics::ACCOUNT_RESET, analytics_attributes)
AccountReset::CreateRequest.new(current_user).call
flash[:email] = current_user.email_address.email
flash[:email] = current_user.email_addresses.first.email
redirect_to account_reset_confirm_request_url
end

Expand All @@ -40,6 +40,7 @@ def analytics_attributes
sms_phone: TwoFactorAuthentication::PhonePolicy.new(current_user).configured?,
totp: TwoFactorAuthentication::AuthAppPolicy.new(current_user).configured?,
piv_cac: TwoFactorAuthentication::PivCacPolicy.new(current_user).configured?,
email_addresses: current_user.email_addresses.count,
}
end
end
Expand Down
34 changes: 26 additions & 8 deletions app/controllers/analytics_controller.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
class AnalyticsController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :confirm_two_factor_authenticated

def create
unless analytics_saved?
session[:platform_authenticator] = true
analytics.track_event(Analytics::PLATFORM_AUTHENTICATOR, results.to_h)
results.each do |event, result|
next if result.nil?

analytics.track_event(event, result.to_h)
end
head :ok
end

private

def results
FormResponse.new(success: true, errors: {},
extra: { platform_authenticator: params[:available] })
{
Analytics::FRONTEND_BROWSER_CAPABILITIES => platform_authenticator_result,
}
end

def platform_authenticator_result
return unless current_user
return if platform_authenticator_results_saved? || !platform_authenticator_params_valid?

session[:platform_authenticator_analytics_saved] = true
platform_authenticator_available = params[:available] ||
params.dig(:platform_authenticator, :available)
extra = { platform_authenticator: (platform_authenticator_available == 'true') }
FormResponse.new(success: true, errors: {}, extra: extra)
end

def platform_authenticator_params_valid?
result = params[:available] || params.dig(:platform_authenticator, :available)
%w[true false].include?(result)
end

def analytics_saved?
session[:platform_authenticator]
def platform_authenticator_results_saved?
session[:platform_authenticator_analytics_saved] == true ||
session[:platform_authenticator] == true
end
end
4 changes: 2 additions & 2 deletions app/controllers/concerns/account_recoverable.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module AccountRecoverable
# :reek:FeatureEnvy
def piv_cac_enabled_but_not_phone_enabled?
def piv_cac_enabled_but_not_multiple_mfa_enabled?
# we need to change this so it's about having multiple mfa methods defined rather than
# piv/cac + phone. Leaving as-is for now.
TwoFactorAuthentication::PivCacPolicy.new(current_user).enabled? &&
!TwoFactorAuthentication::PhonePolicy.new(current_user).enabled?
!MfaPolicy.new(current_user).multiple_factors_enabled?
end
end
28 changes: 25 additions & 3 deletions app/controllers/concerns/remember_device_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ def save_remember_device_preference
def check_remember_device_preference
return unless authentication_context?
return if remember_device_cookie.nil?
return unless remember_device_cookie.valid_for_user?(current_user)
handle_valid_otp
return unless remember_device_cookie.valid_for_user?(
user: current_user,
expiration_interval: decorated_session.mfa_expiration_interval
)

handle_valid_remember_device_cookie
end

def remember_device_cookie
Expand All @@ -24,9 +28,27 @@ def remember_device_cookie
)
end

def remember_device_expired_for_sp?
return false unless user_session[:mfa_device_remembered]
return true if remember_device_cookie.nil?

!remember_device_cookie.valid_for_user?(
user: current_user,
expiration_interval: decorated_session.mfa_expiration_interval
)
end

private

def handle_valid_remember_device_cookie
user_session[:mfa_device_remembered] = true
mark_user_session_authenticated
bypass_sign_in current_user
redirect_to after_otp_verification_confirmation_url
reset_otp_session_data
end

def remember_device_cookie_expiration
Figaro.env.remember_device_expiration_days.to_i.days.from_now
Figaro.env.remember_device_expiration_hours_aal_1.to_i.hours.from_now
end
end
11 changes: 8 additions & 3 deletions app/controllers/concerns/two_factor_authenticatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ def current_password_required?

def check_already_authenticated
return unless initial_authentication_context?
return unless user_fully_authenticated?
return if remember_device_expired_for_sp?

redirect_to after_otp_verification_confirmation_url if user_fully_authenticated?
redirect_to after_otp_verification_confirmation_url
end

def reset_attempt_count_if_user_no_longer_locked_out
Expand All @@ -76,6 +78,7 @@ def handle_valid_otp
handle_valid_otp_for_confirmation_context
end
save_remember_device_preference
user_session.delete(:mfa_device_remembered)

redirect_to after_otp_verification_confirmation_url
reset_otp_session_data
Expand Down Expand Up @@ -145,7 +148,9 @@ def old_phone

def phone_changed
create_user_event(:phone_changed)
UserMailer.phone_changed(current_user).deliver_later
current_user.confirmed_email_addresses.each do |email_address|
UserMailer.phone_changed(email_address).deliver_later
end
end

def phone_confirmed
Expand Down Expand Up @@ -234,7 +239,7 @@ def account_reset_token
def authenticator_view_data
{
two_factor_authentication_method: two_factor_authentication_method,
user_email: current_user.email_address.email,
user_email: current_user.email_addresses.first.email,
remember_device_available: false,
}.merge(generic_data)
end
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/concerns/unconfirmed_user_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ def validate_token

def process_valid_confirmation_token
@confirmation_token = params[:confirmation_token]
@forbidden_passwords = ForbiddenPasswords.new(@user.email_address.email).call
@forbidden_passwords = @user.email_addresses.flat_map do |email_address|
ForbiddenPasswords.new(email_address.email).call
end
flash.now[:success] = t('devise.confirmations.confirmed_but_must_set_password')
session[:user_confirmation_token] = @confirmation_token
end
Expand Down
10 changes: 8 additions & 2 deletions app/controllers/openid_connect/authorization_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module OpenidConnect
class AuthorizationController < ApplicationController
include AccountRecoverable
include FullyAuthenticatable
include RememberDeviceConcern
include VerifyProfileConcern
include VerifySPAttributesConcern

Expand All @@ -10,18 +11,23 @@ class AuthorizationController < ApplicationController
before_action :force_login_if_prompt_param_is_login_and_request_is_external, only: [:index]
before_action :store_request, only: [:index]
before_action :apply_secure_headers_override, only: [:index]
before_action :confirm_user_is_authenticated_with_fresh_mfa, only: :index

def index
return confirm_two_factor_authenticated(request_id) unless user_fully_authenticated?
link_identity_to_service_provider
return redirect_to account_recovery_setup_url if piv_cac_enabled_but_not_phone_enabled?
return redirect_to account_recovery_setup_url if piv_cac_enabled_but_not_multiple_mfa_enabled?
return redirect_to_account_or_verify_profile_url if profile_or_identity_needs_verification?
return redirect_to(sign_up_completed_url) if needs_sp_attribute_verification?
handle_successful_handoff
end

private

def confirm_user_is_authenticated_with_fresh_mfa
return confirm_two_factor_authenticated(request_id) unless user_fully_authenticated?
redirect_to user_two_factor_authentication_url if remember_device_expired_for_sp?
end

def link_identity_to_service_provider
@authorize_form.link_identity_to_service_provider(current_user, session.id)
end
Expand Down
10 changes: 8 additions & 2 deletions app/controllers/saml_idp_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ class SamlIdpController < ApplicationController
include SamlIdpLogoutConcern
include AccountRecoverable
include FullyAuthenticatable
include RememberDeviceConcern
include VerifyProfileConcern
include VerifySPAttributesConcern

skip_before_action :verify_authenticity_token
before_action :validate_saml_logout_request, only: :logout
before_action :confirm_user_is_authenticated_with_fresh_mfa, only: :auth

def auth
return confirm_two_factor_authenticated(request_id) unless user_fully_authenticated?
link_identity_from_session_data
capture_analytics
return redirect_to account_recovery_setup_url if piv_cac_enabled_but_not_phone_enabled?
return redirect_to account_recovery_setup_url if piv_cac_enabled_but_not_multiple_mfa_enabled?
return redirect_to_account_or_verify_profile_url if profile_or_identity_needs_verification?
return redirect_to(sign_up_completed_url) if needs_sp_attribute_verification?
handle_successful_handoff
Expand All @@ -40,6 +41,11 @@ def logout

private

def confirm_user_is_authenticated_with_fresh_mfa
return confirm_two_factor_authenticated(request_id) unless user_fully_authenticated?
redirect_to user_two_factor_authentication_url if remember_device_expired_for_sp?
end

def validate_saml_logout_request(raw_saml_request = params[:SAMLRequest])
request_valid = saml_request_valid?(raw_saml_request)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def presenter_for_two_factor_authentication_method

def handle_result(result)
if result.success?
create_user_event(:personal_key_used)
generate_new_personal_key
handle_valid_otp
else
Expand Down Expand Up @@ -69,6 +70,7 @@ def handle_valid_otp
handle_valid_otp_for_authentication_context
redirect_to manage_personal_key_url
reset_otp_session_data
user_session.delete(:mfa_device_remembered)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def handle_valid_piv_cac
handle_valid_otp_for_authentication_context
redirect_to next_step
reset_otp_session_data
user_session.delete(:mfa_device_remembered)
end

def next_step
Expand Down Expand Up @@ -63,7 +64,7 @@ def render_show_after_invalid
def piv_cac_view_data
{
two_factor_authentication_method: two_factor_authentication_method,
user_email: current_user.email_address.email,
user_email: current_user.email_addresses.first.email,
remember_device_available: false,
}.merge(generic_data)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def handle_valid_webauthn
handle_valid_otp_for_authentication_context
redirect_to after_otp_verification_confirmation_url
reset_otp_session_data
user_session.delete(:mfa_device_remembered)
end

def handle_invalid_webauthn
Expand Down
8 changes: 6 additions & 2 deletions app/controllers/users/passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ class PasswordsController < ReauthnRequiredController

def edit
@update_user_password_form = UpdateUserPasswordForm.new(current_user)
@forbidden_passwords = ForbiddenPasswords.new(current_user.email_address.email).call
@forbidden_passwords = current_user.email_addresses.flat_map do |email_address|
ForbiddenPasswords.new(email_address.email).call
end
end

def update
Expand Down Expand Up @@ -41,7 +43,9 @@ def handle_invalid_password
# need to provide our custom forbidden passwords data that zxcvbn needs,
# otherwise the JS will throw an exception and the password strength
# meter will not appear.
@forbidden_passwords = ForbiddenPasswords.new(current_user.email).call
@forbidden_passwords = current_user.email_addresses.flat_map do |email_address|
ForbiddenPasswords.new(email_address.email).call
end
render :edit
end
end
Expand Down
14 changes: 8 additions & 6 deletions app/controllers/users/reset_passwords_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def edit

if result.success?
@reset_password_form = ResetPasswordForm.new(build_user)
@forbidden_passwords = forbidden_passwords(token_user.email_address.email)
@forbidden_passwords = forbidden_passwords(token_user.email_addresses)
else
handle_invalid_or_expired_token(result)
end
Expand All @@ -36,7 +36,6 @@ def edit
# PUT /resource/password
def update
self.resource = user_matching_token(user_params[:reset_password_token])

@reset_password_form = ResetPasswordForm.new(resource)

result = @reset_password_form.submit(user_params)
Expand All @@ -52,8 +51,10 @@ def update

protected

def forbidden_passwords(email_address)
ForbiddenPasswords.new(email_address).call
def forbidden_passwords(email_addresses)
email_addresses.flat_map do |email_address|
ForbiddenPasswords.new(email_address.email).call
end
end

def email_params
Expand Down Expand Up @@ -112,8 +113,9 @@ def handle_successful_password_reset
end

def handle_unsuccessful_password_reset(result)
if result.errors[:reset_password_token].present?
flash[:error] = t('devise.passwords.token_expired')
reset_password_token_errors = result.errors[:reset_password_token]
if reset_password_token_errors.present?
flash[:error] = t("devise.passwords.#{reset_password_token_errors.first}")
redirect_to new_user_password_url
return
end
Expand Down
Loading