app/forms/openid_connect_authorize_form.rb
# frozen_string_literal: true
class OpenidConnectAuthorizeForm
include ActiveModel::Model
include ActionView::Helpers::TranslationHelper
include RedirectUriValidator
extend Forwardable
SIMPLE_ATTRS = %i[
client_id
code_challenge
code_challenge_method
nonce
prompt
redirect_uri
response_type
state
].freeze
ATTRS = [
:unauthorized_scope,
:acr_values,
:vtr,
:scope,
:verified_within,
*SIMPLE_ATTRS,
].freeze
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].freeze
attr_reader(*ATTRS)
RANDOM_VALUE_MINIMUM_LENGTH = 22
MINIMUM_REPROOF_VERIFIED_WITHIN_DAYS = 30
validates :acr_values, presence: true, if: ->(form) { form.vtr.blank? }
validates :client_id, presence: true
validates :redirect_uri, presence: true
validates :scope, presence: true
validates :state, presence: true, length: { minimum: RANDOM_VALUE_MINIMUM_LENGTH }
validates :nonce, presence: true, length: { minimum: RANDOM_VALUE_MINIMUM_LENGTH }
validates :response_type, inclusion: { in: %w[code] }
validates :prompt, presence: true, inclusion: { in: %w[login select_account] }
validates :code_challenge_method, inclusion: { in: %w[S256] }, if: :code_challenge
validate :validate_acr_values
validate :validate_vtr
validate :validate_client_id
validate :validate_scope
validate :validate_unauthorized_scope
validate :validate_privileges
validate :validate_prompt
validate :validate_verified_within_format, if: :verified_within_allowed?
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)
@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)
@unauthorized_scope = check_for_unauthorized_scope(params)
if verified_within_allowed?
@duration_parser = DurationParser.new(params[:verified_within])
@verified_within = @duration_parser.parse
end
end
def submit
@success = valid?
FormResponse.new(success: success, errors: errors, extra: extra_analytics_attributes)
end
def verified_at_requested?
scope.include?('profile:verified_at')
end
def cannot_validate_redirect_uri?
errors.include?(:redirect_uri) || errors.include?(:client_id)
end
def service_provider
return @service_provider if defined?(@service_provider)
@service_provider = ServiceProvider.find_by(issuer: client_id)
end
def link_identity_to_service_provider(
current_user:,
ial:,
rails_session_id:
)
identity_linker = IdentityLinker.new(current_user, service_provider)
@identity = identity_linker.link_identity(
nonce: nonce,
rails_session_id: rails_session_id,
ial: ial,
acr_values: acr_values&.join(' '),
vtr: vtr,
requested_aal_value: requested_aal_value,
scope: scope.join(' '),
code_challenge: code_challenge,
)
end
def success_redirect_uri
return if cannot_validate_redirect_uri?
code = identity&.session_uuid
UriService.add_params(redirect_uri, code: code, state: state) if code
end
def ial_values
acr_values.filter { |acr| acr.include?('ial') || acr.include?('loa') }
end
def aal_values
acr_values.filter { |acr| acr.include?('aal') }
end
def requested_aal_value
highest_level_aal(aal_values) ||
Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF
end
def biometric_comparison_requested?
!!parsed_vectors_of_trust&.any?(&:biometric_comparison?)
end
def parsed_vectors_of_trust
return @parsed_vectors_of_trust if defined?(@parsed_vectors_of_trust)
@parsed_vectors_of_trust = begin
if vtr.is_a?(Array) && !vtr.empty?
vtr.map { |vot| Vot::Parser.new(vector_of_trust: vot).parse }
end
rescue Vot::Parser::ParseException
nil
end
end
private
attr_reader :identity, :success
def code
identity&.session_uuid
end
def check_for_unauthorized_scope(params)
param_value = params[:scope]
return false if identity_proofing_requested_or_default? || param_value.blank?
return true if verified_at_requested? && !identity_proofing_service_provider?
@scope != param_value.split(' ').compact
end
def parse_to_values(param_value, possible_values)
return [] if param_value.blank?
param_value.split(' ').compact & possible_values
end
def parse_vtr(param_value)
return if !IdentityConfig.store.use_vot_in_sp_requests
return if param_value.blank?
JSON.parse(param_value)
rescue JSON::ParserError
nil
end
def validate_acr_values
return if vtr.present?
if acr_values.empty?
errors.add(
:acr_values, t('openid_connect.authorization.errors.no_valid_acr_values'),
type: :no_valid_acr_values
)
elsif ial_values.empty?
errors.add(
:acr_values, t('openid_connect.authorization.errors.missing_ial'),
type: :missing_ial
)
end
end
def validate_vtr
return if vtr.blank?
return if parsed_vectors_of_trust.present?
errors.add(
:vtr, t('openid_connect.authorization.errors.no_valid_vtr'),
type: :no_valid_vtr
)
end
# This checks that the SP matches something in the database
# OpenidConnect::AuthorizationController#check_sp_active checks that it's currently active
def validate_client_id
return if service_provider
errors.add(
:client_id, t('openid_connect.authorization.errors.bad_client_id'),
type: :bad_client_id
)
end
def validate_scope
return if scope.present?
errors.add(
:scope, t('openid_connect.authorization.errors.no_valid_scope'),
type: :no_valid_scope
)
end
def validate_unauthorized_scope
return unless @unauthorized_scope && IdentityConfig.store.unauthorized_scope_enabled
errors.add(
:scope, t('openid_connect.authorization.errors.unauthorized_scope'),
type: :unauthorized_scope
)
end
def validate_prompt
return if prompt == 'select_account'
return if prompt == 'login' && service_provider&.allow_prompt_login
errors.add(
:prompt, t('openid_connect.authorization.errors.prompt_invalid'),
type: :prompt_invalid
)
end
def validate_verified_within_format
return true if @duration_parser.valid?
errors.add(
:verified_within,
t('openid_connect.authorization.errors.invalid_verified_within_format'),
type: :invalid_verified_within_format,
)
false
end
def validate_verified_within_duration
return true if verified_within.blank?
return true if verified_within >= MINIMUM_REPROOF_VERIFIED_WITHIN_DAYS.days
errors.add(
:verified_within,
t(
'openid_connect.authorization.errors.invalid_verified_within_duration',
count: MINIMUM_REPROOF_VERIFIED_WITHIN_DAYS,
),
type: :invalid_verified_within_duration,
)
false
end
def extra_analytics_attributes
{
client_id: client_id,
prompt: prompt,
allow_prompt_login: service_provider&.allow_prompt_login,
redirect_uri: result_uri,
scope: scope&.sort&.join(' '),
acr_values: acr_values&.sort&.join(' '),
vtr: vtr,
unauthorized_scope: @unauthorized_scope,
code_digest: code ? Digest::SHA256.hexdigest(code) : nil,
code_challenge_present: code_challenge.present?,
service_provider_pkce: service_provider&.pkce,
}
end
def result_uri
success ? success_redirect_uri : error_redirect_uri
end
def error_redirect_uri
return if cannot_validate_redirect_uri?
UriService.add_params(
redirect_uri,
error: 'invalid_request',
error_description: errors.full_messages.join(' '),
state: state,
)
end
def scopes
if identity_proofing_requested_or_default?
return OpenidConnectAttributeScoper::VALID_SCOPES
end
OpenidConnectAttributeScoper::VALID_IAL1_SCOPES
end
def validate_privileges
if (identity_proofing_requested? && !identity_proofing_service_provider?) ||
(ialmax_requested? && !ialmax_allowed_for_sp?)
errors.add(
:acr_values, t('openid_connect.authorization.errors.no_auth'),
type: :no_auth
)
end
end
def identity_proofing_requested_or_default?
identity_proofing_requested? ||
ialmax_requested? ||
sp_defaults_to_identity_proofing?
end
def sp_defaults_to_identity_proofing?
vtr.blank? && ial_values.blank? && identity_proofing_service_provider?
end
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
end
end
def identity_proofing_service_provider?
service_provider&.ial.to_i >= 2
end
def ialmax_allowed_for_sp?
IdentityConfig.store.allowed_ialmax_providers.include?(client_id)
end
def ialmax_requested?
Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_IAL[ial_values.sort.max] == 0
end
def highest_level_aal(aal_values)
AALS_BY_PRIORITY.find { |aal| aal_values.include?(aal) }
end
def verified_within_allowed?
IdentityConfig.store.allowed_verified_within_providers.include?(client_id)
end
end