diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index d8bc28b5a89..d3d17e42954 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -72,11 +72,11 @@ class OpenidConnectAuthorizeForm validate :validate_verified_within_duration, if: :verified_within_allowed? def initialize(params) - @acr_values = parse_to_values(params[:acr_values], Saml::Idp::Constants::VALID_AUTHN_CONTEXTS) + @acr_values = parse_acr_values(params[:acr_values], Saml::Idp::Constants::VALID_AUTHN_CONTEXTS) @vtr = parse_vtr(params[:vtr]) SIMPLE_ATTRS.each { |key| instance_variable_set(:"@#{key}", params[key]) } @prompt ||= 'select_account' - @scope = parse_to_values(params[:scope], scopes) + @scope = parse_scope(params[:scope], scopes) @unauthorized_scope = check_for_unauthorized_scope(params) if verified_within_allowed? @@ -115,7 +115,7 @@ def link_identity_to_service_provider( nonce: nonce, rails_session_id: rails_session_id, ial: ial, - acr_values: acr_values&.join(' '), + acr_values: Vot::AcrComponentValues.build(acr_values), vtr: vtr, requested_aal_value: requested_aal_value, scope: scope.join(' '), @@ -140,13 +140,14 @@ def aal_values end def requested_aal_value - highest_level_aal(aal_values) || - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF + highest_level_aal || default_aal_acr end private - attr_reader :identity, :success + # @return [ServiceProviderIdentity] + attr_reader :identity + attr_reader :success def code identity&.session_uuid @@ -171,11 +172,15 @@ def parsed_vectors_of_trust end end - def parse_to_values(param_value, possible_values) + def parse_scope(param_value, possible_values) return [] if param_value.blank? param_value.split(' ').compact & possible_values end + def parse_acr_values(param_value, possible_values = Saml::Idp::Constants::VALID_AUTHN_CONTEXTS) + Vot::AcrComponentValues.order_by_priority(param_value) & possible_values + end + def parse_vtr(param_value) return if !IdentityConfig.store.use_vot_in_sp_requests return if param_value.blank? @@ -278,7 +283,7 @@ def extra_analytics_attributes allow_prompt_login: service_provider&.allow_prompt_login, redirect_uri: result_uri, scope: scope&.sort&.join(' '), - acr_values: acr_values&.sort&.join(' '), + acr_values: Vot::AcrComponentValues.build(acr_values), vtr: vtr, unauthorized_scope: @unauthorized_scope, code_digest: code ? Digest::SHA256.hexdigest(code) : nil, @@ -335,12 +340,13 @@ def identity_proofing_requested? 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 + Vot::AcrComponentValues. + includes_requirements?(highest_level_ial, :identity_proofing) end end def identity_proofing_service_provider? - service_provider&.ial.to_i >= 2 + service_provider&.identity_proofing_allowed? end def ialmax_allowed_for_sp? @@ -348,15 +354,19 @@ def ialmax_allowed_for_sp? end def ialmax_requested? - Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL[ial_values.sort.max] == 0 + Vot::AcrComponentValues.includes_requirements?(highest_level_ial, :ialmax) end def biometric_ial_requested? - ial_values.any? { |ial| Saml::Idp::Constants::BIOMETRIC_IAL_CONTEXTS.include? ial } + Vot::AcrComponentValues.includes_requirements?(highest_level_ial, :biometric_comparison) + end + + def highest_level_ial + @highest_level_ial ||= Vot::AcrComponentValues.find_highest_priority(ial_values) end - def highest_level_aal(aal_values) - AALS_BY_PRIORITY.find { |aal| aal_values.include?(aal) } + def highest_level_aal + @highest_level_aal ||= Vot::AcrComponentValues.find_highest_priority(aal_values) end def verified_within_allowed? @@ -366,4 +376,21 @@ def verified_within_allowed? def semantic_authn_contexts_requested? Saml::Idp::Constants::SEMANTIC_ACRS.intersect?(acr_values) end + + def request_authn_context_resolver + @request_authn_context_resolver ||= AuthnContextResolver.new( + service_provider: service_provider, + user: nil, + vtr: nil, + acr_values: acr_values, + ) + end + + def requested_authn_context + @requested_authn_context ||= request_authn_context_resolver.result + end + + def default_aal_acr + request_authn_context_resolver.default_aal_acr + end end diff --git a/app/models/federated_protocols/oidc.rb b/app/models/federated_protocols/oidc.rb index 2486241580b..f7876345efb 100644 --- a/app/models/federated_protocols/oidc.rb +++ b/app/models/federated_protocols/oidc.rb @@ -2,6 +2,7 @@ module FederatedProtocols class Oidc + # @param request [OpenidConnectAuthorizeForm] def initialize(request) @request = request end @@ -36,6 +37,7 @@ def service_provider private + # @return [OpenidConnectAuthorizeForm] attr_reader :request end end diff --git a/app/presenters/openid_connect_user_info_presenter.rb b/app/presenters/openid_connect_user_info_presenter.rb index bc87e20231b..18de5c0c87e 100644 --- a/app/presenters/openid_connect_user_info_presenter.rb +++ b/app/presenters/openid_connect_user_info_presenter.rb @@ -24,8 +24,8 @@ def user_info info.merge!(x509_attributes) if scoper.x509_scopes_requested? info[:verified_at] = verified_at if scoper.verified_at_requested? if identity.vtr.nil? - info[:ial] = authn_context_resolver.asserted_ial_acr - info[:aal] = identity.requested_aal_value + info[:ial] = asserted_ial_value + info[:aal] = asserted_aal_value else info[:vot] = vot_values info[:vtm] = IdentityConfig.store.vtm_url @@ -40,6 +40,14 @@ def url_options private + def asserted_ial_value + authn_context_resolver.asserted_ial_acr + end + + def asserted_aal_value + identity.requested_aal_value.presence || authn_context_resolver.asserted_aal_acr + end + def vot_values AuthnContextResolver.new( user: identity.user, diff --git a/app/services/authn_context_resolver.rb b/app/services/authn_context_resolver.rb index e50fb24268f..71c58f99c43 100644 --- a/app/services/authn_context_resolver.rb +++ b/app/services/authn_context_resolver.rb @@ -3,11 +3,30 @@ class AuthnContextResolver attr_reader :user, :service_provider, :vtr, :acr_values + AALS_BY_PRIORITY = [ + Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::AAL3_HSPD12_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::AAL1_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ].freeze + IALS_BY_PRIORITY = [ + Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::LOA3_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF, + ].freeze + def initialize(user:, service_provider:, vtr:, acr_values:) @user = user @service_provider = service_provider @vtr = vtr - @acr_values = acr_values + @acr_values = Vot::AcrComponentValues.build(acr_values) end def result @@ -32,6 +51,33 @@ def asserted_ial_acr end end + def asserted_aal_acr + return if vtr.present? + if acr_aal_component_values.present? + highest_aal_acr(acr_aal_component_values) || acr_aal_component_values.first.name + # elsif service_provider&.default_aal.to_i >= 3 + # Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF + # elsif service_provider&.default_aal.to_i == 2 + # Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF + # elsif acr_result.identity_proofing_or_ialmax? + # Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF + else + default_aal_acr + end + end + + def default_aal_acr + if service_provider&.default_aal.to_i >= 3 + Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF + elsif service_provider&.default_aal.to_i == 2 + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF + elsif acr_result_with_sp_defaults.identity_proofing_or_ialmax? + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF + else + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF + end + end + private def selected_vtr_parser_result_from_vtr_list @@ -83,19 +129,11 @@ def acr_result_with_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 - elsif service_provider&.default_aal.to_i == 2 - result.with(aal2?: true) - elsif service_provider&.default_aal.to_i >= 3 - result.with(aal2?: true, phishing_resistant?: true) - else - result - end + @acr_result_without_sp_defaults ||= if acr_values.present? + Vot::Parser.new(acr_values: acr_values).parse + else + Vot::Parser::Result.no_sp_result + end end def decorate_acr_result_with_user_context(result) @@ -121,13 +159,27 @@ def result_with_sp_ial_defaults(result) end end - def acr_aal_component_values - 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 + def result_with_sp_aal_defaults(result) + if acr_aal_component_values.any? + result + elsif service_provider&.default_aal.to_i == 2 + result.with(aal2?: true) + elsif service_provider&.default_aal.to_i >= 3 + result.with(aal2?: true, phishing_resistant?: true) + else + result end end + def acr_aal_component_values + @acr_aal_component_values ||= + Vot::AcrComponentValues.order_by_priority( + Vot::AcrComponentValues.aal_component_values( + acr_result_without_sp_defaults.component_values, + ), + ) + end + def acr_ial_component_values acr_result_without_sp_defaults.component_values.filter do |component_value| Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL.include?(component_value.name) @@ -147,4 +199,8 @@ def use_semantic_authn_contexts? @use_semantic_authn_contexts ||= service_provider&.semantic_authn_contexts_allowed? && Vot::AcrComponentValues.any_semantic_acrs?(acr_values) end + + def highest_aal_acr(aals) + Vot::AcrComponentValues.find_highest_priority(aals) + end end diff --git a/app/services/vot/acr_component_values.rb b/app/services/vot/acr_component_values.rb index a10ebee50e6..21ba64e1699 100644 --- a/app/services/vot/acr_component_values.rb +++ b/app/services/vot/acr_component_values.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Layout/LineLength module Vot module AcrComponentValues ## Identity proofing ACR values @@ -77,56 +78,113 @@ module AcrComponentValues ).freeze ## Authentication ACR values - DEFAULT = ComponentValue.new( + DEFAULT_AAL = ComponentValue.new( name: Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, - description: 'Legacy default authentication', + description: + 'Default - MFA required + remember device up to 30 days (AAL1, NIST SP 800-63B-3)', implied_component_values: [], requirements: [], ).freeze AAL1 = ComponentValue.new( name: Saml::Idp::Constants::AAL1_AUTHN_CONTEXT_CLASSREF, - description: 'Legacy AAL1', + description: + '(defunct) MFA required + remember device up to 30 days. (AAL1, NIST SP 800-63B-3)', implied_component_values: [], requirements: [], ).freeze AAL2 = ComponentValue.new( name: Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, - description: 'Legacy AAL2', + description: 'MFA required, remember device disallowed. (AAL2, NIST SP 800-63B-3)', implied_component_values: [], requirements: [:aal2], ).freeze AAL2_PHISHING_RESISTANT = ComponentValue.new( name: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF, - description: 'Legacy AAL2 with phishing resistance', + description: %{Phishing-resistant MFA required (e.g., WebAuthn or PIV/CAC cards), + remember device disallowed. (AAL2, NIST SP 800-63B-3)}, implied_component_values: [], requirements: [:aal2, :phishing_resistant], ).freeze AAL2_HSPD12 = ComponentValue.new( name: Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF, - description: 'Legacy AAL2 with HSPD12', + description: 'HSPD12-compliant MFA required (i.e., PIV/CAC only), remember device disallowed. (AAL2, NIST SP 800-63B-3)', implied_component_values: [], - requirements: [:aal2, :hspd12], + requirements: [:aal2, :phishing_resistant, :hspd12], ).freeze AAL3 = ComponentValue.new( name: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF, - description: 'Legacy AAL3', + description: 'Unsupported. (AAL3, NIST SP 800-63B-3)', implied_component_values: [], requirements: [:aal2, :phishing_resistant], ).freeze AAL3_HSPD12 = ComponentValue.new( name: Saml::Idp::Constants::AAL3_HSPD12_AUTHN_CONTEXT_CLASSREF, - description: 'Legacy AAL3 with HSPD12', + description: 'Unsupported. (AAL3, NIST SP 800-63B-3)', implied_component_values: [], - requirements: [:aal2, :hspd12], + requirements: [:aal2, :hspd12, :phishing_resistant], ).freeze - NAME_HASH = constants.map do |constant| - component_value = const_get(constant) - [component_value.name, component_value] - end.to_h.freeze + # @type [Hash] + NAME_HASH = constants(false). + map { |c| const_get(c, false) }. + filter { |c| c.is_a?(ComponentValue) }. + index_by(&:name).freeze + + VALUES = NAME_HASH.values.freeze + + DELIM = ' ' + + IAL_COMPONENTS = [ + LOA1, + LOA3, + IAL1, + IALMAX, + IAL2, + IAL2_BIO_PREFERRED, + IAL2_BIO_REQUIRED, + ].freeze + IAL_COMPONENTS_BY_PRIORITY = [ + IAL2_BIO_REQUIRED, + IAL2_BIO_PREFERRED, + IAL2, + LOA3, + IALMAX, + IAL1, + LOA1, + ].freeze + IALS_BY_PRIORITY = IAL_COMPONENTS_BY_PRIORITY.map(&:name).freeze DELIM = ' ' + AAL_COMPONENTS = [ + DEFAULT_AAL, + AAL1, + AAL2, + AAL2_PHISHING_RESISTANT, + AAL2_HSPD12, + AAL3, + AAL3_HSPD12, + ].freeze + AAL_COMPONENTS_BY_PRIORITY = [ + AAL2_HSPD12, + AAL3_HSPD12, + AAL2_PHISHING_RESISTANT, + AAL3, + AAL2, + AAL1, + DEFAULT_AAL, + ].freeze + + AALS_BY_PRIORITY = AAL_COMPONENTS_BY_PRIORITY.map(&:name).freeze + + ACR_COMPONENTS_BY_PRIORITY = [ + *IAL_COMPONENTS_BY_PRIORITY, + *AAL_COMPONENTS_BY_PRIORITY, + ].freeze + + ACRS_BY_PRIORITY = ACR_COMPONENTS_BY_PRIORITY.map(&:name).freeze + + # @return [Hash{String=>Vot::ComponentValue}] def self.by_name NAME_HASH end @@ -142,5 +200,109 @@ def self.any_semantic_acrs?(acr_values) ).to_a Saml::Idp::Constants::SEMANTIC_ACRS.intersect?(values) end + + def self.includes_requirements?(name, *requirements) + component = NAME_HASH[name] + component.present? && + component.requirements.intersection(requirements).size == requirements.length + end + + # Get the highest priority ACR value + # @return [String, nil] + def self.find_highest_priority(values) + AcrComponentValues.order_by_priority(values).first + end + + # Sort ACR values by priority, highest to lowest + # @param values [Array, String] + def self.order_by_priority(values) + order_by_priority_with(values, series: ACRS_BY_PRIORITY) + end + + # Order a list of ACR values by priority, highest to lowest + # Returns a new {Array} of {String} values where the order has been by set by the +series+, + # based on the index of the objects from the original in the series. + # + # If the +series+ includes values that have no corresponding element in the Enumerable, + # these are ignored. + # If the Enumerable has additional elements that aren't named in the +series+, + # these are not included in the result. + # @param values [Array, String] + # @param series [Array,nil] Defaults to #ACRS_BY_PRIORITY + def self.order_by_priority_with(values, series: nil) + rankings = series.presence || ACRS_BY_PRIORITY + to_names(values). + filter { |acr| AcrComponentValues.acr?(acr) && rankings.include?(acr) }. + sort_by { |acr| rankings.index(acr) } + end + + def self.ial_values(values) + to_names(values).filter { |acr| AcrComponentValues.ial?(acr) } + end + + def self.ial_component_values(values) + to_components(values).filter { |acr| AcrComponentValues.ial?(acr) } + end + + def self.aal_values(values) + to_names(values).filter { |acr| AcrComponentValues.aal?(acr) } + end + + def self.aal_component_values(values) + to_components(values).filter { |acr| AcrComponentValues.aal?(acr) } + end + + # Convert list of strings or {Vot::ComponentValue} to ACR values + # @return [Array] + def self.to_names(values = '') + [] unless values.present? && + (values.is_a?(String) || values.is_a?(Enumerable)) + values_ary = values.is_a?(String) && values.split(DELIM) || values.presence || [] + + values_ary. + map { |v| AcrComponentValues.to_name(v) }. + compact_blank. + uniq + end + + # Convert list of strings or {Vot::ComponentValue} to an array of ComponentValue + # @param values [String, Enumerable] + # @return [Array] + def self.to_components(values) + [] unless values.present? && + (values.is_a?(String) || values.is_a?(Enumerable)) + + values_ary = values.is_a?(String) && values.split(DELIM) || values + + values_ary. + map { |v| AcrComponentValues.to_component(v) }. + compact_blank. + uniq + end + + def self.to_name(value) + value.is_a?(ComponentValue) ? value.name : value.to_s + end + + def self.to_component(value) + value.is_a?(String) && by_name[value] || value + end + + def self.build(values) + values.is_a?(Enumerable) && to_names(values).join(DELIM) || values + end + + def self.ial?(value) + Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL.include?(AcrComponentValues.to_name(value)) + end + + def self.aal?(value) + Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_AAL.include?(AcrComponentValues.to_name(value)) + end + + def self.acr?(value) + AcrComponentValues.ial?(value) || AcrComponentValues.aal?(value) + end end end +# rubocop:enable Layout/LineLength diff --git a/lib/saml_idp_constants.rb b/lib/saml_idp_constants.rb index d8f4902fcd5..3f352577e06 100644 --- a/lib/saml_idp_constants.rb +++ b/lib/saml_idp_constants.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true require 'idp/constants' + # rubocop:disable Layout/LineLength # Global constants used by the SAML IdP module Saml module Idp module Constants + DELIM = ' ' LOA1_AUTHN_CONTEXT_CLASSREF = 'http://idmanagement.gov/ns/assurance/loa/1' LOA3_AUTHN_CONTEXT_CLASSREF = 'http://idmanagement.gov/ns/assurance/loa/3' @@ -30,11 +32,17 @@ module Constants DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF = 'urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo' AAL_AUTHN_CONTEXT_PREFIX = 'http://idmanagement.gov/ns/assurance/aal' + + # @deprecated Use {#DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF} AAL1_AUTHN_CONTEXT_CLASSREF = "#{AAL_AUTHN_CONTEXT_PREFIX}/1".freeze AAL2_AUTHN_CONTEXT_CLASSREF = "#{AAL_AUTHN_CONTEXT_PREFIX}/2".freeze AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF = "#{AAL_AUTHN_CONTEXT_PREFIX}/2?phishing_resistant=true".freeze AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF = "#{AAL_AUTHN_CONTEXT_PREFIX}/2?hspd12=true".freeze + + # @deprecated We do not support NIST SP 800-63-3 AAL3 AAL3_AUTHN_CONTEXT_CLASSREF = "#{AAL_AUTHN_CONTEXT_PREFIX}/3".freeze + + # @deprecated We do not support NIST SP 800-63-3 AAL3 AAL3_HSPD12_AUTHN_CONTEXT_CLASSREF = "#{AAL_AUTHN_CONTEXT_PREFIX}/3?hspd12=true".freeze NAME_ID_FORMAT_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index b09232ddcaa..ffe7572f584 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -1263,7 +1263,7 @@ def oidc_end_client_secret_jwt(vot: nil, prompt: nil, user: nil, redirs_to: nil) else expect(userinfo_response[:ial]).to eq(Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF) expect(userinfo_response[:aal]).to eq( - Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, ) expect(userinfo_response).not_to have_key(:vot) end diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 580983f27d7..6f78317546c 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -149,6 +149,7 @@ let(:vtr) { ['A1.B2.C3'].to_json } it 'has errors' do + raise_error expect(valid?).to eq(false) expect(form.errors[:vtr]). to include(t('openid_connect.authorization.errors.no_valid_vtr')) @@ -213,6 +214,7 @@ shared_examples 'allows biometric IAL only if sp is authorized' do |biometric_ial| let(:acr_values) { biometric_ial } + let(:vtr) { nil } context "when the IAL requested is #{biometric_ial}" do context 'when the service provider is allowed to use biometric ials' do @@ -455,6 +457,29 @@ describe '#requested_aal_value' do context 'with ACR values' do let(:vtr) { nil } + context 'when no AAL value is passed' do + context 'when identity proofing is requested' do + let(:acr_values) { Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } + + it 'returns AAL2' do + expect(form.requested_aal_value).to eq( + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, + ) + end + context 'when SP default AAL is 3' do + before do + allow_any_instance_of(ServiceProvider).to receive(:default_aal). + and_return(3) + end + + it 'returns AAL3' do + expect(form.requested_aal_value).to eq( + Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF, + ) + end + end + end + end context 'when AAL2 passed' do let(:acr_values) { Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF } @@ -581,8 +606,10 @@ let(:verified_within) { '45d' } it 'parses the value as a number of days' do - expect(form.valid?).to eq(true) - expect(form.verified_within).to eq(45.days) + aggregate_failures 'verified within verified_within' do + expect(form.valid?).to eq(true) + expect(form.verified_within).to eq(45.days) + end end end diff --git a/spec/services/id_token_builder_spec.rb b/spec/services/id_token_builder_spec.rb index 2d24daa0856..b242652c671 100644 --- a/spec/services/id_token_builder_spec.rb +++ b/spec/services/id_token_builder_spec.rb @@ -106,6 +106,10 @@ it 'sets the acr to the ial2 constant' do expect(decoded_payload[:acr]).to eq(Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF) end + + it 'sets the aal to the AAL2 ACR value' do + expect(decoded_payload[:aal]).to eq(Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF) + end end context 'ial2 with biometric comparison required' do @@ -130,6 +134,11 @@ it 'sets the acr to the ial1 constant' do expect(decoded_payload[:acr]).to eq(Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF) end + it 'sets the aal to the default ACR value' do + expect(decoded_payload[:aal]).to eq( + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ) + end end context 'ialmax request' do