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
1 change: 1 addition & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def resolved_authn_context_result
@resolved_authn_context_result = Vot::Parser::Result.no_sp_result
else
@resolved_authn_context_result = AuthnContextResolver.new(
user: current_user,
service_provider: service_provider,
vtr: sp_session[:vtr],
acr_values: sp_session[:acr_values],
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/openid_connect/authorization_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def block_biometric_requests_in_production
end

def biometric_comparison_requested?
@authorize_form.parsed_vector_of_trust&.biometric_comparison?
@authorize_form.biometric_comparison_requested?
end

def check_sp_active
Expand Down
18 changes: 9 additions & 9 deletions app/forms/openid_connect_authorize_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,16 @@ def requested_aal_value
Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF
end

def biometric_comparison_required?
parsed_vector_of_trust&.biometric_comparison?
def biometric_comparison_requested?
!!parsed_vectors_of_trust&.any?(&:biometric_comparison?)
end

def parsed_vector_of_trust
return @parsed_vector_of_trust if defined?(@parsed_vector_of_trust)
def parsed_vectors_of_trust
return @parsed_vectors_of_trust if defined?(@parsed_vectors_of_trust)

@parsed_vector_of_trust = begin
@parsed_vectors_of_trust = begin
if vtr.is_a?(Array) && !vtr.empty?
Vot::Parser.new(vector_of_trust: vtr.first).parse
vtr.map { |vot| Vot::Parser.new(vector_of_trust: vot).parse }
end
rescue Vot::Parser::ParseException
nil
Expand Down Expand Up @@ -192,7 +192,7 @@ def validate_acr_values

def validate_vtr
return if vtr.blank?
return if parsed_vector_of_trust.present?
return if parsed_vectors_of_trust.present?
errors.add(
:vtr, t('openid_connect.authorization.errors.no_valid_vtr'),
type: :no_valid_vtr
Expand Down Expand Up @@ -319,8 +319,8 @@ def sp_defaults_to_identity_proofing?
end

def identity_proofing_requested?
if parsed_vector_of_trust.present?
parsed_vector_of_trust.identity_proofing?
if parsed_vectors_of_trust.present?
parsed_vectors_of_trust.any?(&:identity_proofing?)
else
Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL[ial_values.sort.max] == 2
end
Expand Down
8 changes: 8 additions & 0 deletions app/models/anonymous_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,12 @@ def confirmed_at
def locked_out?
second_factor_locked_at.present? && !lockout_period_expired?
end

def identity_verified_with_biometric_comparison?
false
end

def identity_verified?
false
end
end
10 changes: 7 additions & 3 deletions app/presenters/openid_connect_user_info_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,12 @@ def url_options
private

def vot_values
vot = JSON.parse(identity.vtr).first
parsed_vot = Vot::Parser.new(vector_of_trust: vot).parse
parsed_vot.expanded_component_values
AuthnContextResolver.new(
user: identity.user,
vtr: JSON.parse(identity.vtr),
service_provider: identity&.service_provider_record,
acr_values: nil,
).resolve.expanded_component_values
end

def uuid_from_sp_identity(identity)
Expand Down Expand Up @@ -140,6 +143,7 @@ def identity_proofing_requested_for_verified_user?

def resolved_authn_context_result
@resolved_authn_context_result ||= AuthnContextResolver.new(
user: identity.user,
service_provider: identity&.service_provider_record,
vtr: identity.vtr.presence && JSON.parse(identity.vtr),
acr_values: identity.acr_values,
Expand Down
1 change: 1 addition & 0 deletions app/services/analytics.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def resolved_authn_context_result
service_provider = ServiceProvider.find_by(issuer: sp)

@resolved_authn_context_result = AuthnContextResolver.new(
user: user,
service_provider:,
vtr: session[:sp][:vtr],
acr_values: session[:sp][:acr_values],
Expand Down
1 change: 1 addition & 0 deletions app/services/attribute_asserter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def resolved_authn_context_result
@resolved_authn_context_result ||= begin
saml = FederatedProtocols::Saml.new(authn_request)
AuthnContextResolver.new(
user: user,
service_provider: service_provider,
vtr: saml.vtr,
acr_values: saml.acr_values,
Expand Down
54 changes: 43 additions & 11 deletions app/services/authn_context_resolver.rb
Original file line number Diff line number Diff line change
@@ -1,39 +1,71 @@
# frozen_string_literal: true

class AuthnContextResolver
attr_reader :service_provider, :vtr, :acr_values
attr_reader :user, :service_provider, :vtr, :acr_values

def initialize(service_provider:, vtr:, acr_values:)
def initialize(user:, service_provider:, vtr:, acr_values:)
@user = user
@service_provider = service_provider
@vtr = vtr
@acr_values = acr_values
end

def resolve
if vtr.present?
vot_parser_result
selected_vtr_parser_result_from_vtr_list
else
acr_result_with_sp_defaults
end
end

private

def vot_parser_result
@vot_result = Vot::Parser.new(
vector_of_trust: vtr&.first,
acr_values: acr_values,
).parse
def selected_vtr_parser_result_from_vtr_list
if biometric_proofing_vot.present? && user&.identity_verified_with_biometric_comparison?
biometric_proofing_vot
elsif non_biometric_identity_proofing_vot.present? && user&.identity_verified?
non_biometric_identity_proofing_vot
elsif no_identity_proofing_vot.present?
no_identity_proofing_vot
else
parsed_vectors_of_trust.first
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am testing this in combination with 18F/identity-oidc-sinatra#163 and it looks like this is always picking C1.P1 from the vtr ["C1.P1","C1"]. Is this correct? If we are trying to replicate ialmax, I would expect C1 to be used in those cases (this is assuming I understand what ialmax is supposed to do)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, reading a little more I think i am confused

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, there is a little bug in here. Let me fix it up. I tested with C1,C1.P1 when I should be testing with C1.P1,C1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

476fc5a has the fix

end
end

def parsed_vectors_of_trust
@parsed_vectors_of_trust ||= vtr.map do |vot|
Vot::Parser.new(vector_of_trust: vot).parse
end
end

def biometric_proofing_vot
parsed_vectors_of_trust.find(&:biometric_comparison?)
end

def non_biometric_identity_proofing_vot
parsed_vectors_of_trust.find do |vot_parser_result|
vot_parser_result.identity_proofing? && !vot_parser_result.biometric_comparison?
end
end

def no_identity_proofing_vot
parsed_vectors_of_trust.find do |vot_parser_result|
!vot_parser_result.identity_proofing?
end
end

def acr_result_with_sp_defaults
result_with_sp_aal_defaults(
result_with_sp_ial_defaults(
vot_parser_result,
acr_result_without_sp_defaults,
),
)
end

def acr_result_without_sp_defaults
@acr_result_without_sp_defaults ||= Vot::Parser.new(acr_values: acr_values).parse
end

def result_with_sp_aal_defaults(result)
if acr_aal_component_values.any?
result
Expand All @@ -57,14 +89,14 @@ def result_with_sp_ial_defaults(result)
end

def acr_aal_component_values
vot_parser_result.component_values.filter do |component_value|
acr_result_without_sp_defaults.component_values.filter do |component_value|
component_value.name.include?('aal') ||
component_value.name == Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF
end
end

def acr_ial_component_values
vot_parser_result.component_values.filter do |component_value|
acr_result_without_sp_defaults.component_values.filter do |component_value|
component_value.name.include?('ial') || component_value.name.include?('loa')
end
end
Expand Down
1 change: 1 addition & 0 deletions app/services/id_token_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def determine_ial_max_acr

def resolved_authn_context_result
@resolved_authn_context_result ||= AuthnContextResolver.new(
user: identity.user,
service_provider: identity.service_provider_record,
vtr: parsed_vtr_value,
acr_values: identity.acr_values,
Expand Down
8 changes: 5 additions & 3 deletions spec/controllers/concerns/remember_device_concern_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
RSpec.describe RememberDeviceConcern do
let(:sp) { nil }
let(:raw_session) { {} }
let(:current_user) { build(:user) }

subject(:test_controller) do
test_controller_class =
Class.new(ApplicationController) do
include(RememberDeviceConcern)

attr_reader :sp, :raw_session, :request
attr_reader :sp, :raw_session, :request, :current_user
alias :sp_from_sp_session :sp
alias :sp_session :raw_session

def initialize(sp, raw_session, request)
def initialize(sp, raw_session, request, current_user)
@sp = sp
@raw_session = raw_session
@request = request
@current_user = current_user
end
end

Expand All @@ -27,7 +29,7 @@ def initialize(sp, raw_session, request)
filtered_parameters: {},
)

test_controller_class.new(sp, raw_session, test_request)
test_controller_class.new(sp, raw_session, test_request, current_user)
end

describe '#mfa_expiration_interval' do
Expand Down
15 changes: 15 additions & 0 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,21 @@
end
end

trait :proofed_with_selfie do
fully_registered

after :build do |user|
create(
:profile,
:active,
:verified,
:with_pii,
idv_level: :unsupervised_with_selfie,
user: user,
)
end
end

trait :with_pending_in_person_enrollment do
after :build do |user|
profile = create(:profile, :with_pii, :in_person_verification_pending, user: user)
Expand Down
123 changes: 123 additions & 0 deletions spec/features/sign_in/multiple_vot_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
require 'rails_helper'

RSpec.feature 'Sign in with multiple vectors of trust', allowed_extra_analytics: [:*] do
include OidcAuthHelper
include IdvHelper
include DocAuthHelper

before do
allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture_enabled).and_return(true)
end

context 'biometric and non-biometric proofing is acceptable' do
scenario 'identity proofing is not required if user is proofed with biometric' do
user = create(:user, :proofed_with_selfie)

visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1'])
sign_in_live_with_2fa(user)

expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue

user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info

expect(user_info[:given_name]).to be_present
expect(user_info[:vot]).to eq('C1.C2.P1.Pb')
end

scenario 'identity proofing is not required if user is proofed without biometric' do
user = create(:user, :proofed)

visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1'])
sign_in_live_with_2fa(user)

expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue

user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info

expect(user_info[:given_name]).to be_present
expect(user_info[:vot]).to eq('C1.C2.P1')
end

scenario 'identity proofing with biometric is required if user is not proofed', :js do
user = create(:user, :fully_registered)

visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1'])
sign_in_live_with_2fa(user)

expect(current_path).to eq(idv_welcome_path)
complete_all_doc_auth_steps_before_password_step(with_selfie: true)
fill_in 'Password', with: user.password
click_continue
acknowledge_and_confirm_personal_key

expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue

user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info

expect(user_info[:given_name]).to be_present
expect(user_info[:vot]).to eq('C1.C2.P1.Pb')
end
end

context 'proofing or no proofing is acceptable (IALMAX)' do
scenario 'identity proofing is not required if the user is not proofed' do
user = create(:user, :fully_registered)

visit_idp_from_oidc_sp_with_vtr(
vtr: ['C1.C2.P1', 'C1.C2'],
scope: 'openid email profile:name',
)
sign_in_live_with_2fa(user)

expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue

user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info

expect(user_info[:given_name]).to_not be_present
expect(user_info[:vot]).to eq('C1.C2')
end

scenario 'attributes are shared if the user is proofed' do
user = create(:user, :proofed)

visit_idp_from_oidc_sp_with_vtr(
vtr: ['C1.C2.P1', 'C1.C2'],
scope: 'openid email profile:name',
)
sign_in_live_with_2fa(user)

expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue

user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info

expect(user_info[:given_name]).to be_present
expect(user_info[:vot]).to eq('C1.C2.P1')
end

scenario 'identity proofing is not required if proofed user resets password' do
user = create(:user, :proofed)

visit_idp_from_oidc_sp_with_vtr(
vtr: ['C1.C2.P1', 'C1.C2'],
scope: 'openid email profile:name',
)
trigger_reset_password_and_click_email_link(user.email)
reset_password(user, 'new even better password')
user.password = 'new even better password'
sign_in_live_with_2fa(user)

expect(current_path).to eq(sign_up_completed_path)
click_agree_and_continue

user_info = OpenidConnectUserInfoPresenter.new(user.identities.last).user_info

expect(user_info[:given_name]).to_not be_present
expect(user_info[:vot]).to eq('C1.C2')
end
end
end
Loading