module PandaPal::Helpers class SessionNonceMismatch < StandardError; end module SessionReplacement extend ActiveSupport::Concern included do # ActionController::API does not use views, so helper_method would not be defined unless name == "ActionController::API" helper_method :link_nonce, :current_session, :current_session_data helper_method :link_with_session_to, :url_with_session, :session_url_for end prepend_around_action :monkeypatch_flash prepend_around_action :auto_save_session end class_methods do def link_nonce_type(value = :not_given, use_non_app: false) if value == :not_given if use_non_app || !defined?(::ApplicationController) || self <= ::ApplicationController @link_nonce_type || superclass.try(:link_nonce_type, use_non_app: true) || :nonce else ::ApplicationController.link_nonce_type(value, use_non_app: true) end else @link_nonce_type = value end end end def save_session current_session.try(:save) end def current_session(create_missing: true) return @current_session if @current_session.present? if params[:session_token] payload = JSON.parse(session_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access matched_session = find_or_create_session(key: payload[:session_key]) if matched_session.present? if payload[:token_type] == 'nonce' && matched_session.data[:link_nonce] == payload[:nonce] @current_session = matched_session @current_session.data[:link_nonce] = nil elsif payload[:token_type] == 'fixed_ip' && matched_session.data[:remote_ip] == request.remote_ip && DateTime.parse(matched_session.data[:last_ip_token_requested]) > session_expiration_period_minutes.minutes.ago @current_session = matched_session elsif payload[:token_type] == 'expiring' && DateTime.parse(matched_session.data[:last_token_requested]) > session_expiration_period_minutes.minutes.ago @current_session = matched_session end end raise SessionNonceMismatch, "Session Not Found" unless @current_session.present? elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present? @current_session = find_or_create_session(key: session_key) end @current_session ||= find_or_create_session(key: :create) if create_missing @current_session end def current_session_data current_session.data end def session_changed? current_session.changed? && current_session.changes[:data].present? end def forbid_access_if_lacking_session render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session? end def verify_authenticity_token # No need to check CSRF when no cookies were sent. This fixes CSRF failures in Browsers # that restrict Cookie setting within an IFrame. return unless request.cookies.keys.length > 0 super end # Redirect with the session key intact. In production, # handle this by adding a one-time use encrypted token to the URL. # Keeping it in the URL in development means that it plays # nicely with webpack-dev-server live reloading (otherwise # you get an access error everytime it tries to live reload). def redirect_with_session_to(*args) redirect_to url_with_session(*args) end def link_with_session_to(*args) helpers.link_to url_with_session(*args) end def session_url_for(*args) url_for(build_session_url_params(*args)) end def url_with_session(location, *args, route_context: self, **kwargs) route_context.send(location, *build_session_url_params(*args, **kwargs)) end def link_nonce(type: link_nonce_type) type = instance_exec(&type) if type.is_a?(Proc) type = type.to_s @cached_link_nonces ||= {} @cached_link_nonces[type] ||= begin payload = { token_type: type, session_key: current_session.session_key, organization_id: current_organization.id, } if type == 'nonce' current_session_data[:link_nonce] = SecureRandom.hex payload.merge!(nonce: current_session_data[:link_nonce]) elsif type == 'fixed_ip' current_session_data[:remote_ip] ||= request.remote_ip current_session_data[:last_ip_token_requested] = DateTime.now.iso8601 elsif type == 'expiring' current_session_data[:last_token_requested] = DateTime.now.iso8601 else raise StandardError, "Unsupported link_nonce_type: '#{type}'" end session_cryptor.encrypt_and_sign(payload.to_json) end end def link_nonce_type self.class.link_nonce_type end def session_expiration_period_minutes 15 end private def session_cryptor secret_key_base = Rails.application.try(:secret_key_base) || Rails.application.secrets.secret_key_base @session_cryptor ||= ActiveSupport::MessageEncryptor.new(secret_key_base[0..31]) end def session_key_header if match = request.headers['Authorization'].try(:match, /token=(.+)/) match[1] end end def build_session_url_params(*args, nonce_type: link_nonce_type, **kwargs) if args[-1].is_a?(Hash) args[-1] = args[-1].dup else args.push({}) end if Rails.env.development? args[-1].merge!( session_key: current_session.session_key, organization_id: current_organization.id, ) else args[-1].merge!( session_token: link_nonce(type: nonce_type), organization_id: current_organization.id, ) end args[-1].merge!(kwargs) args end def auto_save_session yield if block_given? save_session if @current_session && session_changed? end def monkeypatch_flash if valid_session? && (value = current_session_data['flashes']).present? flashes = value["flashes"] if discard = value["discard"] flashes.except!(*discard) end flash.replace(flashes) flash.discard() end yield if @current_session.present? current_session_data['flashes'] = flash.to_session_value flash.discard() end end end end