require 'browser' require_relative 'session_replacement' module PandaPal::Helpers module ControllerHelper extend ActiveSupport::Concern include SessionReplacement def current_organization @organization ||= PandaPal::Organization.find_by!(key: organization_key) if organization_key @organization ||= PandaPal::Organization.find_by(id: organization_id) if organization_id @organization ||= PandaPal::Organization.find_by_name(Apartment::Tenant.current) end def current_lti_platform return @current_lti_platform if @current_lti_platform.present? # TODO: (Future) This could be expanded more to take better advantage of the LTI 1.3 Multi-Tenancy model. if (canvas_url = current_organization&.settings&.dig(:canvas, :base_url)).present? @current_lti_platform ||= PandaPal::Platform::Canvas.new(canvas_url) end @current_lti_platform ||= PandaPal::Platform::Canvas.new('http://localhost:3000') if Rails.env.development? @current_lti_platform ||= PandaPal::Platform::CANVAS @current_lti_platform end def lti_launch_params current_session_data[:launch_params] end def validate_launch! safari_override if params[:id_token].present? validate_v1p3_launch elsif params[:oauth_consumer_key].present? validate_v1p0_launch end end def validate_v1p0_launch authorized = false if @organization = params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key']) sanitized_params = request.request_parameters # These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out. safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url", "dummy_param"] safe_unexpected_params.each do |p| sanitized_params.delete(p) end authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.original_url, sanitized_params, @organization.secret) authorized = authenticator.valid_signature? end if !authorized render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized end authorized end def validate_v1p3_launch decoded_jwt = JSON::JWT.decode(params.require(:id_token), :skip_verification) raise JSON::JWT::VerificationFailed, 'error decoding id_token' if decoded_jwt.blank? client_id = decoded_jwt['aud'] @organization = PandaPal::Organization.find_by!(key: 'PandaPal') # client_id) raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present? decoded_jwt.verify!(current_lti_platform.public_jwks) params[:session_key] = params[:state] raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce'] jwt_verifier = PandaPal::LtiJwtValidator.new(decoded_jwt, client_id) raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid? @decoded_lti_jwt = decoded_jwt rescue JSON::JWT::VerificationFailed => e payload = Array(e.message) render json: { message: [ { errors: payload }, { id_token: params.require(:id_token) }, ], }, status: :unauthorized false end def switch_tenant(organization = current_organization, &block) return unless organization raise 'This method should be called in an around_action callback' unless block_given? Apartment::Tenant.switch(organization.name) do yield end end def forbid_access_if_lacking_session super safari_override end def valid_session? return false unless current_session(create_missing: false)&.persisted? return false unless current_organization return false unless current_session.panda_pal_organization_id == current_organization.id return false unless Apartment::Tenant.current == current_organization.name true rescue SessionNonceMismatch false end def safari_override use_secure_headers_override(:safari_override) if browser.safari? end private def find_or_create_session(key:) if key == :create PandaPal::Session.new(panda_pal_organization_id: current_organization.id) else PandaPal::Session.find_by(session_key: key) end end def organization_key org_key ||= params[:oauth_consumer_key] org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present? org_key ||= session[:organization_key] org_key end def organization_id params[:organization_id] end end end