<commit_before>#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

class Login::SamlController < ApplicationController
  include Login::Shared

  protect_from_forgery except: [:create, :destroy], with: :exception

  before_action :forbid_on_files_domain
  before_action :run_login_hooks, only: [:new, :create]
  before_action :fix_ms_office_redirects, only: :new

  def new
    increment_saml_stat("login_attempt")
    session[:saml2_processing] = false if Canvas::Plugin.value_to_boolean(params[:saml2_processing], ignore_unrecognized: true) == false
    redirect_to delegated_auth_redirect_uri(aac.generate_authn_request_redirect(host: request.host_with_port,
                                                                                parent_registration: session[:parent_registration]))
  end

  def create
    login_error_message = t("There was a problem logging in at %{institution}",
                            institution: @domain_root_account.display_name)

    saml2_processing = true
    saml2_processing = false if session[:saml2_processing] == false
    saml2_processing = false if @domain_root_account.settings[:process_saml_responses_with_saml2] == false

    legacy_response = Onelogin::Saml::Response.new(params[:SAMLResponse])
    response, relay_state = SAML2::Bindings::HTTP_POST.decode(request.request_parameters)

    increment_saml_stat('login_response_received')

    aac = @domain_root_account.authentication_providers.active.
      where(auth_type: 'saml').
      where(idp_entity_id: legacy_response.issuer).
      first
    if aac.nil?
      logger.error "Attempted SAML login for #{legacy_response.issuer} on account without that IdP"
      flash[:delegated_message] = if @domain_root_account.auth_discovery_url
        t("Canvas did not recognize your identity provider")
      elsif response.issuer
        t("Canvas is not configured to receive logins from %{issuer}.", issuer: legacy_response.issuer)
      else
        t("The institution you logged in from is not configured on this account.")
      end
      return redirect_to login_url
    end

    debugging = aac.debugging? && response.is_a?(SAML2::Response) && aac.debug_get(:request_id) == response.in_response_to
    encrypted_xml = response.to_s if debugging

    settings = aac.saml_settings(request.host_with_port)

    verify_certificate = true
    verify_certificate = false if  @domain_root_account.settings[:verify_saml_certificate] == false
    aac.sp_metadata(request.host_with_port).valid_response?(response,
                                                            aac.idp_metadata,
                                                            allow_expired_certificate: @domain_root_account.settings[:allow_expired_saml_certificate],
                                                            verify_certificate: verify_certificate)
    legacy_response.process(settings) unless saml2_processing

    if debugging
      aac.debug_set(:debugging, t('debug.redirect_from_idp', "Received LoginResponse from IdP"))
      aac.debug_set(:idp_response_encoded, params[:SAMLResponse])
      aac.debug_set(:idp_response_xml_encrypted, saml2_processing ? encrypted_xml : legacy_response.xml)
      aac.debug_set(:idp_response_xml_decrypted, saml2_processing ? response.to_s : legacy_response.decrypted_document.to_s)
      aac.debug_set(:idp_in_response_to, saml2_processing ? response.try(:in_response_to) : legacy_response.in_response_to)
      aac.debug_set(:idp_login_destination, saml2_processing ? response.destination : legacy_response.destination)
      aac.debug_set(:login_to_canvas_success, 'false')
      unless saml2_processing
        aac.debug_set(:fingerprint_from_idp, legacy_response.fingerprint_from_idp)
      end
    end

    if !saml2_processing && legacy_response.is_valid? && !response.errors.empty?
      logger.warn("Response valid via legacy SAML processing from #{legacy_response.issuer}, but invalid according to SAML2 processing: #{response.errors.join("\n")}")
      unless aac.settings[:first_saml_error]
        aac.settings[:first_saml_error] = response.errors.join("\n")
        aac.save!
      end
    end

    if saml2_processing
      # yes, a lot of this is duplicated from below, but there are also a lot of changes,
      # and it's easier to not interweave them so the legacy code can be easily stripped
      # in the future
      assertion = response.assertions.first
      provider_attributes = assertion&.attribute_statements&.first&.to_h || {}
      subject_name_id = assertion&.subject&.name_id
      unique_id = if aac.login_attribute == 'NameID'
        subject_name_id&.id
      else
        provider_attributes[aac.login_attribute]
      end
      if unique_id && aac.strip_domain_from_login_attribute?
        unique_id = unique_id.split('@', 2)[0]
      end

      logger.info "Attempting SAML2 login for #{aac.login_attribute} #{unique_id} in account #{@domain_root_account.id}"

      unless response.errors.empty?
        increment_saml_stat("errors.invalid_response")
        if debugging
          aac.debug_set(:is_valid_login_response, 'false')
          aac.debug_set(:login_response_validation_error, response.errors.join("\n"))
        end
        logger.error "Failed to verify SAML signature: #{response.errors.join("\n")}"
        flash[:delegated_message] = login_error_message
        return redirect_to login_url
      end

      aac.debug_set(:is_valid_login_response, 'true') if debugging

      # for parent using self-registration to observe a student
      # the student is logged out after validation
      # and registration process resumed
      if session[:parent_registration]
        expected_unique_id = session[:parent_registration][:observee][:unique_id]
        session[:parent_registration][:unique_id_match] = (expected_unique_id == unique_id)
        saml = ExternalAuthObservation::Saml.new(@domain_root_account, request, response)
        redirect_to saml.logout_url
        return
      end

      reset_session_for_login

      pseudonym = @domain_root_account.pseudonyms.for_auth_configuration(unique_id, aac)
      if !pseudonym && aac.jit_provisioning?
        pseudonym = aac.provision_user(unique_id, provider_attributes)
      elsif pseudonym
        aac.apply_federated_attributes(pseudonym, provider_attributes)
      end

      if pseudonym
        # Successful login and we have a user
        @domain_root_account.pseudonym_sessions.create!(pseudonym, false)
        user = pseudonym.login_assertions_for_user

        if debugging
          aac.debug_set(:login_to_canvas_success, 'true')
          aac.debug_set(:logged_in_user_id, user.id)
        end
        increment_saml_stat("normal.login_success")

        session[:saml_unique_id] = unique_id
        session[:name_id] = subject_name_id.id
        session[:name_identifier_format] = subject_name_id.format
        session[:name_qualifier] = subject_name_id.name_qualifier
        session[:sp_name_qualifier] = subject_name_id.sp_name_qualifier
        session[:session_index] = assertion.authn_statements.first&.session_index
        session[:return_to] = relay_state if relay_state&.match(/\A\/(\z|[^\/])/)
        session[:login_aac] = aac.id

        successful_login(user, pseudonym)
      else
        unknown_user_url = @domain_root_account.unknown_user_url.presence || login_url
        increment_saml_stat("errors.unknown_user")
        message = "Received SAML login request for unknown user: #{unique_id} redirecting to: #{unknown_user_url}."
        logger.warn message
        aac.debug_set(:canvas_login_fail_message, message) if debugging
        flash[:delegated_message] = t("Canvas doesn't have an account for user: %{user}",
                                      user: unique_id)
        redirect_to unknown_user_url
      end

      return
    end

    provider_attributes = legacy_response.saml_attributes
    unique_id = if aac.login_attribute == 'NameID'
      legacy_response.name_id<commit_msg>saml2: subject name id is optional<commit_after>#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#

class Login::SamlController < ApplicationController
  include Login::Shared

  protect_from_forgery except: [:create, :destroy], with: :exception

  before_action :forbid_on_files_domain
  before_action :run_login_hooks, only: [:new, :create]
  before_action :fix_ms_office_redirects, only: :new

  def new
    increment_saml_stat("login_attempt")
    session[:saml2_processing] = false if Canvas::Plugin.value_to_boolean(params[:saml2_processing], ignore_unrecognized: true) == false
    redirect_to delegated_auth_redirect_uri(aac.generate_authn_request_redirect(host: request.host_with_port,
                                                                                parent_registration: session[:parent_registration]))
  end

  def create
    login_error_message = t("There was a problem logging in at %{institution}",
                            institution: @domain_root_account.display_name)

    saml2_processing = true
    saml2_processing = false if session[:saml2_processing] == false
    saml2_processing = false if @domain_root_account.settings[:process_saml_responses_with_saml2] == false

    legacy_response = Onelogin::Saml::Response.new(params[:SAMLResponse])
    response, relay_state = SAML2::Bindings::HTTP_POST.decode(request.request_parameters)

    increment_saml_stat('login_response_received')

    aac = @domain_root_account.authentication_providers.active.
      where(auth_type: 'saml').
      where(idp_entity_id: legacy_response.issuer).
      first
    if aac.nil?
      logger.error "Attempted SAML login for #{legacy_response.issuer} on account without that IdP"
      flash[:delegated_message] = if @domain_root_account.auth_discovery_url
        t("Canvas did not recognize your identity provider")
      elsif response.issuer
        t("Canvas is not configured to receive logins from %{issuer}.", issuer: legacy_response.issuer)
      else
        t("The institution you logged in from is not configured on this account.")
      end
      return redirect_to login_url
    end

    debugging = aac.debugging? && response.is_a?(SAML2::Response) && aac.debug_get(:request_id) == response.in_response_to
    encrypted_xml = response.to_s if debugging

    settings = aac.saml_settings(request.host_with_port)

    verify_certificate = true
    verify_certificate = false if  @domain_root_account.settings[:verify_saml_certificate] == false
    aac.sp_metadata(request.host_with_port).valid_response?(response,
                                                            aac.idp_metadata,
                                                            allow_expired_certificate: @domain_root_account.settings[:allow_expired_saml_certificate],
                                                            verify_certificate: verify_certificate)
    legacy_response.process(settings) unless saml2_processing

    if debugging
      aac.debug_set(:debugging, t('debug.redirect_from_idp', "Received LoginResponse from IdP"))
      aac.debug_set(:idp_response_encoded, params[:SAMLResponse])
      aac.debug_set(:idp_response_xml_encrypted, saml2_processing ? encrypted_xml : legacy_response.xml)
      aac.debug_set(:idp_response_xml_decrypted, saml2_processing ? response.to_s : legacy_response.decrypted_document.to_s)
      aac.debug_set(:idp_in_response_to, saml2_processing ? response.try(:in_response_to) : legacy_response.in_response_to)
      aac.debug_set(:idp_login_destination, saml2_processing ? response.destination : legacy_response.destination)
      aac.debug_set(:login_to_canvas_success, 'false')
      unless saml2_processing
        aac.debug_set(:fingerprint_from_idp, legacy_response.fingerprint_from_idp)
      end
    end

    if !saml2_processing && legacy_response.is_valid? && !response.errors.empty?
      logger.warn("Response valid via legacy SAML processing from #{legacy_response.issuer}, but invalid according to SAML2 processing: #{response.errors.join("\n")}")
      unless aac.settings[:first_saml_error]
        aac.settings[:first_saml_error] = response.errors.join("\n")
        aac.save!
      end
    end

    if saml2_processing
      # yes, a lot of this is duplicated from below, but there are also a lot of changes,
      # and it's easier to not interweave them so the legacy code can be easily stripped
      # in the future
      assertion = response.assertions.first
      provider_attributes = assertion&.attribute_statements&.first&.to_h || {}
      subject_name_id = assertion&.subject&.name_id
      unique_id = if aac.login_attribute == 'NameID'
        subject_name_id&.id
      else
        provider_attributes[aac.login_attribute]
      end
      if unique_id && aac.strip_domain_from_login_attribute?
        unique_id = unique_id.split('@', 2)[0]
      end

      logger.info "Attempting SAML2 login for #{aac.login_attribute} #{unique_id} in account #{@domain_root_account.id}"

      unless response.errors.empty?
        increment_saml_stat("errors.invalid_response")
        if debugging
          aac.debug_set(:is_valid_login_response, 'false')
          aac.debug_set(:login_response_validation_error, response.errors.join("\n"))
        end
        logger.error "Failed to verify SAML signature: #{response.errors.join("\n")}"
        flash[:delegated_message] = login_error_message
        return redirect_to login_url
      end

      aac.debug_set(:is_valid_login_response, 'true') if debugging

      # for parent using self-registration to observe a student
      # the student is logged out after validation
      # and registration process resumed
      if session[:parent_registration]
        expected_unique_id = session[:parent_registration][:observee][:unique_id]
        session[:parent_registration][:unique_id_match] = (expected_unique_id == unique_id)
        saml = ExternalAuthObservation::Saml.new(@domain_root_account, request, response)
        redirect_to saml.logout_url
        return
      end

      reset_session_for_login

      pseudonym = @domain_root_account.pseudonyms.for_auth_configuration(unique_id, aac)
      if !pseudonym && aac.jit_provisioning?
        pseudonym = aac.provision_user(unique_id, provider_attributes)
      elsif pseudonym
        aac.apply_federated_attributes(pseudonym, provider_attributes)
      end

      if pseudonym
        # Successful login and we have a user
        @domain_root_account.pseudonym_sessions.create!(pseudonym, false)
        user = pseudonym.login_assertions_for_user

        if debugging
          aac.debug_set(:login_to_canvas_success, 'true')
          aac.debug_set(:logged_in_user_id, user.id)
        end
        increment_saml_stat("normal.login_success")

        session[:saml_unique_id] = unique_id
        session[:name_id] = subject_name_id&.id
        session[:name_identifier_format] = subject_name_id&.format
        session[:name_qualifier] = subject_name_id&.name_qualifier
        session[:sp_name_qualifier] = subject_name_id&.sp_name_qualifier
        session[:session_index] = assertion.authn_statements.first&.session_index
        session[:return_to] = relay_state if relay_state&.match(/\A\/(\z|[^\/])/)
        session[:login_aac] = aac.id

        successful_login(user, pseudonym)
      else
        unknown_user_url = @domain_root_account.unknown_user_url.presence || login_url
        increment_saml_stat("errors.unknown_user")
        message = "Received SAML login request for unknown user: #{unique_id} redirecting to: #{unknown_user_url}."
        logger.warn message
        aac.debug_set(:canvas_login_fail_message, message) if debugging
        flash[:delegated_message] = t("Canvas doesn't have an account for user: %{user}",
                                      user: unique_id)
        redirect_to unknown_user_url
      end

      return
    end

    provider_attributes = legacy_response.saml_attributes
    unique_id = if aac.login_attribute == 'NameID'
      legacy_response.name_id
{'fraction_non_alphanumeric': 0.08261348483970085, 'fraction_numerical': 0.003072641892283611, 'mean_word_length': 3.3626707132018208, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 10}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5512753', 'n_tokens_mistral': 5075, 'n_tokens_neox': 4940, 'n_words': 1110}