18F/identity-idp

View on GitHub
app/controllers/concerns/saml_idp_auth_concern.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
98%
# frozen_string_literal: true

module SamlIdpAuthConcern
  extend ActiveSupport::Concern
  extend Forwardable
  include ForcedReauthenticationConcern

  included do
    # rubocop:disable Rails/LexicallyScopedActionFilter
    before_action :validate_and_create_saml_request_object, only: :auth
    before_action :validate_service_provider_and_authn_context, only: :auth
    before_action :check_sp_active, only: :auth
    before_action :log_external_saml_auth_request, only: [:auth]
    # this must take place _before_ the store_saml_request action or the SAML
    # request is cleared (along with the rest of the session) when the user is
    # signed out
    before_action :sign_out_if_forceauthn_is_true_and_user_is_signed_in, only: :auth
    before_action :store_saml_request, only: :auth
    # rubocop:enable Rails/LexicallyScopedActionFilter
  end

  private

  def sign_out_if_forceauthn_is_true_and_user_is_signed_in
    if !saml_request.force_authn?
      set_issuer_forced_reauthentication(
        issuer: saml_request_service_provider.issuer,
        is_forced_reauthentication: false,
      )
    end

    return unless user_signed_in? && saml_request.force_authn?

    if !sp_session[:final_auth_request]
      sign_out
      set_issuer_forced_reauthentication(
        issuer: saml_request_service_provider.issuer,
        is_forced_reauthentication: true,
      )
    end
    sp_session[:final_auth_request] = false
  end

  def check_sp_active
    return if saml_request_service_provider&.active?
    redirect_to sp_inactive_error_url
  end

  def validate_service_provider_and_authn_context
    return if result.success?

    capture_analytics
    render 'saml_idp/auth/error', status: :bad_request
  end

  def result
    @result ||= @saml_request_validator.call(
      service_provider: saml_request_service_provider,
      authn_context: requested_authn_contexts,
      authn_context_comparison: saml_request.requested_authn_context_comparison,
      nameid_format: name_id_format,
    )
  end

  def validate_and_create_saml_request_object
    # this saml_idp method creates the saml_request object used for validations
    validate_saml_request
    @saml_request_validator = SamlRequestValidator.new
  rescue SamlIdp::XMLSecurity::SignedDocument::ValidationError
    @saml_request_validator = SamlRequestValidator.new(blank_cert: true)
  end

  def name_id_format
    @name_id_format ||= specified_name_id_format || default_name_id_format
  end

  def specified_name_id_format
    if recognized_name_id_format? || saml_request_service_provider&.use_legacy_name_id_behavior
      saml_request.name_id_format
    end
  end

  def recognized_name_id_format?
    Saml::Idp::Constants::VALID_NAME_ID_FORMATS.include?(saml_request.name_id_format)
  end

  def default_name_id_format
    if saml_request_service_provider&.email_nameid_format_allowed
      return Saml::Idp::Constants::NAME_ID_FORMAT_EMAIL
    end
    Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT
  end

  def store_saml_request
    ServiceProviderRequestHandler.new(
      url: request_url,
      session: session,
      protocol_request: saml_request,
      protocol: FederatedProtocols::Saml,
    ).call
  end

  def requested_authn_contexts
    @requested_authn_contexts ||= saml_request.requested_authn_contexts.presence ||
                                  [default_aal_context]
  end

  def default_aal_context
    if saml_request_service_provider&.default_aal
      Saml::Idp::Constants::AUTHN_CONTEXT_AAL_TO_CLASSREF[saml_request_service_provider.default_aal]
    else
      Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF
    end
  end

  def default_ial_context
    if saml_request_service_provider&.ial
      Saml::Idp::Constants::AUTHN_CONTEXT_IAL_TO_CLASSREF[saml_request_service_provider.ial]
    else
      Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF
    end
  end

  def response_authn_context
    if saml_request.requested_vtr_authn_contexts.present?
      resolved_authn_context_result.expanded_component_values
    else
      saml_request.requested_aal_authn_context ||
        default_aal_context
    end
  end

  def requested_ial_authn_context
    saml_request.requested_ial_authn_context || default_ial_context
  end

  def link_identity_from_session_data
    IdentityLinker.
      new(current_user, saml_request_service_provider).
      link_identity(
        ial: resolved_authn_context_int_ial,
        rails_session_id: session.id,
      )
  end

  def identity_needs_verification?
    resolved_authn_context_result.identity_proofing? && current_user.identity_not_verified?
  end

  def active_identity
    current_user.last_identity
  end

  def encode_authn_response(principal, opts)
    build_asserted_attributes(principal)
    super(principal, opts)
  end

  def attribute_asserter(principal)
    AttributeAsserter.new(
      user: principal,
      service_provider: saml_request_service_provider,
      name_id_format: name_id_format,
      authn_request: saml_request,
      decrypted_pii: decrypted_pii,
      user_session: user_session,
    )
  end

  def decrypted_pii
    cacher = Pii::Cacher.new(current_user, user_session)
    cacher.fetch(current_user&.active_profile&.id)
  end

  def build_asserted_attributes(principal)
    asserter = attribute_asserter(principal)
    asserter.build
  end

  def saml_response
    encode_response(
      current_user,
      name_id_format: name_id_format,
      authn_context_classref: response_authn_context,
      reference_id: active_identity.session_uuid,
      encryption: encryption_opts,
      signature: saml_response_signature_options,
      signed_response_message: saml_request_service_provider&.signed_response_message_requested,
    )
  end

  def encryption_opts
    query_params = UriService.params(request.original_url)
    if query_params[:skip_encryption].present? &&
       saml_request_service_provider&.skip_encryption_allowed
      nil
    elsif saml_request_service_provider&.encrypt_responses?
      {
        cert: encryption_cert,
        block_encryption: saml_request_service_provider&.block_encryption,
        key_transport: 'rsa-oaep-mgf1p',
      }
    end
  end

  def encryption_cert
    saml_request.service_provider.matching_cert ||
      saml_request_service_provider&.ssl_certs&.first
  end

  def saml_response_signature_options
    endpoint = SamlEndpoint.new(params[:path_year])
    {
      x509_certificate: endpoint.x509_certificate,
      secret_key: endpoint.secret_key,
    }
  end

  def saml_request_service_provider
    return @saml_request_service_provider if defined?(@saml_request_service_provider)
    @saml_request_service_provider = ServiceProvider.find_by(issuer: current_issuer)
  end

  def current_issuer
    @current_issuer ||= saml_request.service_provider&.identifier
  end

  def request_url
    url = URI(api_saml_auth_url(path_year: params[:path_year]))

    query_params = request.query_parameters
    unless query_params['SAMLRequest']
      orig_saml_request = saml_request.options[:get_params][:SAMLRequest]
      query_params['SAMLRequest'] = orig_saml_request
    end
    unless query_params['RelayState']
      orig_relay_state = saml_request.options[:get_params][:RelayState]
      query_params['RelayState'] = orig_relay_state if orig_relay_state
    end

    url.query = Rack::Utils.build_query(query_params).presence
    url.to_s
  end
end