Skip to content

Commit

Permalink
Merge pull request #2157 from 18F/jgs-lg-241-feature-tests-for-piv-cac
Browse files Browse the repository at this point in the history
[LG-241] Add feature specs for piv/cac
  • Loading branch information
jgsmith-usds authored May 17, 2018
2 parents 90190eb + 8dec540 commit 0cb778a
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 71 deletions.
15 changes: 15 additions & 0 deletions app/controllers/concerns/piv_cac_concern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module PivCacConcern
extend ActiveSupport::Concern

def create_piv_cac_nonce
user_session[:piv_cac_nonce] = SecureRandom.base64(20)
end

def piv_cac_nonce
user_session[:piv_cac_nonce]
end

def clear_piv_cac_nonce
user_session[:piv_cac_nonce] = nil
end
end
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module TwoFactorAuthentication
class PivCacVerificationController < ApplicationController
include TwoFactorAuthenticatable
include PivCacConcern

before_action :confirm_piv_cac_enabled
before_action :reset_attempt_count_if_user_no_longer_locked_out, only: :show
Expand All @@ -9,8 +10,7 @@ def show
if params[:token]
process_token
else
@piv_cac_nonce = SecureRandom.base64(10)
user_session[:piv_cac_nonce] = @piv_cac_nonce
create_piv_cac_nonce
@presenter = presenter_for_two_factor_authentication_method
end
end
Expand All @@ -21,8 +21,11 @@ def process_token
result = piv_cac_verfication_form.submit
analytics.track_event(Analytics::MULTI_FACTOR_AUTH, result.to_h.merge(analytics_properties))
if result.success?
clear_piv_cac_nonce
handle_valid_piv_cac
else
# create new nonce for retry
create_piv_cac_nonce
handle_invalid_otp(type: 'piv_cac')
end
end
Expand All @@ -39,14 +42,16 @@ def piv_cac_view_data
two_factor_authentication_method: two_factor_authentication_method,
user_email: current_user.email,
remember_device_available: false,
totp_enabled: current_user.totp_enabled?,
piv_cac_nonce: piv_cac_nonce,
}.merge(generic_data)
end

def piv_cac_verfication_form
@piv_cac_verification_form ||= UserPivCacVerificationForm.new(
user: current_user,
token: params[:token],
nonce: user_session[:piv_cac_nonce]
nonce: piv_cac_nonce
)
end

Expand Down
10 changes: 6 additions & 4 deletions app/controllers/users/piv_cac_authentication_setup_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Users
class PivCacAuthenticationSetupController < ApplicationController
include UserAuthenticator
include PivCacConcern

before_action :confirm_two_factor_authenticated
before_action :authorize_piv_cac_setup, only: :new
Expand All @@ -12,8 +13,8 @@ def new
else
# add a nonce that we track for the return
analytics.track_event(Analytics::USER_REGISTRATION_PIV_CAC_SETUP_VISIT)
@piv_cac_nonce = SecureRandom.base64(10)
user_session[:piv_cac_nonce] = @piv_cac_nonce
create_piv_cac_nonce
@presenter = PivCacAuthenticationSetupPresenter.new(user_piv_cac_form)
render :new
end
end
Expand Down Expand Up @@ -42,7 +43,7 @@ def user_piv_cac_form
@user_piv_cac_form ||= UserPivCacSetupForm.new(
user: current_user,
token: params[:token],
nonce: user_session[:piv_cac_nonce]
nonce: piv_cac_nonce
)
end

Expand All @@ -52,7 +53,8 @@ def process_valid_submission
end

def process_invalid_submission
@presenter = PivCacAuthenticationSetupPresenter.new(user_piv_cac_form)
create_piv_cac_nonce
@presenter = PivCacAuthenticationSetupErrorPresenter.new(user_piv_cac_form)
render :error
end

Expand Down
22 changes: 22 additions & 0 deletions app/presenters/piv_cac_authentication_setup_base_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class PivCacAuthenticationSetupBasePresenter
include Rails.application.routes.url_helpers
include ActionView::Helpers::TranslationHelper

attr_reader :form

def initialize(form)
@form = form
end

def piv_cac_nonce
@form.nonce
end

def piv_cac_capture_text
t('forms.piv_cac_setup.submit')
end

def piv_cac_service_link
PivCacService.piv_cac_service_link(piv_cac_nonce)
end
end
22 changes: 22 additions & 0 deletions app/presenters/piv_cac_authentication_setup_error_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class PivCacAuthenticationSetupErrorPresenter < PivCacAuthenticationSetupBasePresenter
def error
form.error_type
end

def may_select_another_certificate?
error.start_with?('certificate.') && error != 'certificate.none' ||
error == 'token.invalid' || error == 'piv_cac.already_associated'
end

def title
t("titles.piv_cac_setup.#{error}")
end

def heading
t("headings.piv_cac_setup.#{error}")
end

def description
t("forms.piv_cac_setup.#{error}_html")
end
end
24 changes: 4 additions & 20 deletions app/presenters/piv_cac_authentication_setup_presenter.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
class PivCacAuthenticationSetupPresenter
include Rails.application.routes.url_helpers
include ActionView::Helpers::TranslationHelper

def initialize(form)
@form = form
end

def error
@form.error_type
end

def may_select_another_certificate?
error.start_with?('certificate.') && error != 'certificate.none' ||
error == 'token.invalid' || error == 'piv_cac.already_associated'
end

class PivCacAuthenticationSetupPresenter < PivCacAuthenticationSetupBasePresenter
def title
t("titles.piv_cac_setup.#{error}")
t('titles.piv_cac_setup.new')
end

def heading
t("headings.piv_cac_setup.#{error}")
t('headings.piv_cac_setup.new')
end

def description
t("forms.piv_cac_setup.#{error}_html")
t('forms.piv_cac_setup.piv_cac_intro_html')
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def piv_cac_capture_text
def fallback_links
[
otp_fallback_options,
auth_app_fallback,
personal_key_link,
].compact
end
Expand All @@ -33,9 +34,13 @@ def cancel_link
end
end

def piv_cac_service_link
PivCacService.piv_cac_service_link(piv_cac_nonce)
end

private

attr_reader :user_email, :two_factor_authentication_method
attr_reader :user_email, :two_factor_authentication_method, :totp_enabled, :piv_cac_nonce

def otp_fallback_options
t(
Expand All @@ -48,16 +53,25 @@ def otp_fallback_options
def sms_link
view.link_to(
t('devise.two_factor_authentication.totp_fallback.sms_link_text'),
otp_send_path(locale: LinkLocaleResolver.locale, otp_delivery_selection_form:
{ otp_delivery_preference: 'sms' })
login_two_factor_path(locale: LinkLocaleResolver.locale, otp_delivery_preference: 'sms')
)
end

def voice_link
view.link_to(
t('devise.two_factor_authentication.totp_fallback.voice_link_text'),
otp_send_path(locale: LinkLocaleResolver.locale, otp_delivery_selection_form:
{ otp_delivery_preference: 'voice' })
login_two_factor_path(locale: LinkLocaleResolver.locale, otp_delivery_preference: 'voice')
)
end

def auth_app_fallback
safe_join([auth_app_fallback_tag, '.']) if totp_enabled
end

def auth_app_fallback_tag
view.link_to(
t('links.two_factor_authentication.app'),
login_two_factor_authenticator_path(locale: LinkLocaleResolver.locale)
)
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/services/piv_cac_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def token_decoded(token)
end

def decode_token_response(res)
return { 'error' => 'token.bad' } unless res.code == '200'
return { 'error' => 'token.bad' } unless res.code.to_i == 200
result = res.body
JSON.parse(result)
rescue JSON::JSONError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ h1.h3.my0 = @presenter.header
p.mt-tiny.mb3 = @presenter.help_text

= link_to @presenter.piv_cac_capture_text,
PivCacService.piv_cac_service_link(@piv_cac_nonce),
@presenter.piv_cac_service_link,
class: 'btn btn-primary'

= render 'shared/fallback_links', presenter: @presenter
Expand Down
10 changes: 5 additions & 5 deletions app/views/users/piv_cac_authentication_setup/new.html.slim
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
- title t('titles.piv_cac_setup.new')
- title @presenter.title

h1.h3.my0 = t('headings.piv_cac_setup.new')
p.mt-tiny.mb3 = t('forms.piv_cac_setup.piv_cac_intro_html')
h1.h3.my0 = @presenter.heading
p.mt-tiny.mb3 = @presenter.description

= link_to t('forms.piv_cac_setup.submit'),
PivCacService.piv_cac_service_link(@piv_cac_nonce),
= link_to @presenter.piv_cac_capture_text,
@presenter.piv_cac_service_link,
class: 'btn btn-primary'
= render 'shared/cancel', link: account_path

Expand Down
89 changes: 89 additions & 0 deletions spec/features/two_factor_authentication/sign_in_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,95 @@ def submit_prefilled_otp_code
end
end

describe 'when the user is PIV/CAC enabled' do
it 'allows SMS and Voice fallbacks' do
user = user_with_piv_cac
sign_in_before_2fa(user)

click_link t('devise.two_factor_authentication.piv_cac_fallback.link')

expect(current_path).to eq login_two_factor_piv_cac_path

expect(page).not_to have_link(t('links.two_factor_authentication.app'))

click_link t('devise.two_factor_authentication.totp_fallback.sms_link_text')

expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'sms')

visit login_two_factor_piv_cac_path

click_link t('devise.two_factor_authentication.totp_fallback.voice_link_text')

expect(current_path).to eq login_two_factor_path(otp_delivery_preference: 'voice')
end

it 'allows totp fallback when configured' do
user = create(:user, :signed_up, :with_piv_or_cac, otp_secret_key: 'foo')
sign_in_before_2fa(user)

click_link t('devise.two_factor_authentication.piv_cac_fallback.link')

expect(current_path).to eq login_two_factor_piv_cac_path

click_link t('links.two_factor_authentication.app')

expect(current_path).to eq login_two_factor_authenticator_path
end

scenario 'user can cancel PIV/CAC process' do
user = create(:user, :signed_up, :with_piv_or_cac)
sign_in_before_2fa(user)
click_link t('devise.two_factor_authentication.piv_cac_fallback.link')

expect(current_path).to eq login_two_factor_piv_cac_path
click_link t('links.cancel')

expect(current_path).to eq root_path
end

scenario 'user uses PIV/CAC as their second factor' do
stub_piv_cac_service

user = user_with_piv_cac
sign_in_before_2fa(user)

nonce = visit_login_two_factor_piv_cac_and_get_nonce

visit_piv_cac_service(login_two_factor_piv_cac_path, {
uuid: user.x509_dn_uuid,
dn: "C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.1234",
nonce: nonce
})
expect(current_path).to eq account_path
end

scenario 'user uses incorrect PIV/CAC as their second factor' do
stub_piv_cac_service

user = user_with_piv_cac
sign_in_before_2fa(user)

nonce = visit_login_two_factor_piv_cac_and_get_nonce

visit_piv_cac_service(login_two_factor_piv_cac_path, {
uuid: user.x509_dn_uuid + 'X',
dn: "C=US, O=U.S. Government, OU=DoD, OU=PKI, CN=DOE.JOHN.12345",
nonce: nonce
})
expect(current_path).to eq login_two_factor_piv_cac_path
expect(page).to have_content(t("devise.two_factor_authentication.invalid_piv_cac"))
end
end

describe 'when the user is not piv/cac enabled' do
it 'has no link to piv/cac during login' do
user = create(:user, :signed_up)
sign_in_before_2fa(user)

expect(page).not_to have_link(t('devise.two_factor_authentication.piv_cac_fallback.link'))
end
end

describe 'when the user is TOTP enabled' do
it 'allows SMS and Voice fallbacks' do
user = create(:user, :signed_up, otp_secret_key: 'foo')
Expand Down
Loading

0 comments on commit 0cb778a

Please sign in to comment.