require 'browser' module PandaPal::Helpers::ControllerHelper def save_session current_session.try(:save) end def current_session @current_session ||= PandaPal::Session.find_by(session_key: session_key) if session_key @current_session ||= PandaPal::Session.new(panda_pal_organization_id: current_organization.id) end 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_session_data current_session.data end def session_changed? current_session.changed? && current_session.changes[:data].present? end def validate_launch! authorized = false safari_override 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 # short-circuit if we know the user is not authorized. if !authorized render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized return authorized end if require_persistent_session if cookies_need_iframe_fix?(false) fix_iframe_cookies return false end # For safari we may have been launched temporarily full-screen by canvas. This allows us to set the session cookie. # In this case, we should make sure the session cookie is fixed and redirect back to canvas to properly launch the embedded LTI. if params[:platform_redirect_url] session[:safari_cookie_fixed] = true redirect_to params[:platform_redirect_url] return false end end return authorized end def require_persistent_session if PandaPal.lti_options.has_key?(:require_persistent_session) && PandaPal.lti_options[:require_persistent_session] == true return true end return 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 # Browsers that prevent 3rd party cookies by default (Safari and IE) run into problems # with CSRF handling because the Rails session cookie isn't set. To fix this, we # redirect the current page to the LTI using JavaScript, which will set the cookie, # and then immediately redirect back to Canvas. def fix_iframe_cookies if params[:safari_cookie_authorized].present? session[:safari_cookie_authorized] = true redirect_to params[:return_to] elsif (session[:safari_cookie_fixed] && !params[:safari_cookie_authorized]) render 'panda_pal/lti/iframe_cookie_authorize', layout: false else render 'panda_pal/lti/iframe_cookie_fix', layout: false end end def cookies_need_iframe_fix?(check_authorized=true) if check_authorized return browser.safari? && !request.referrer&.include?('sessionless_launch') && !(session[:safari_cookie_fixed] && session[:safari_cookie_authorized]) && !params[:platform_redirect_url] else return browser.safari? && !request.referrer&.include?('sessionless_launch') && !session[:safari_cookie_fixed] && !params[:platform_redirect_url] end end def forbid_access_if_lacking_session if require_persistent_session && cookies_need_iframe_fix?(true) fix_iframe_cookies else render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session? end safari_override end def valid_session? [ current_session.persisted?, current_organization, current_session.panda_pal_organization_id == current_organization.id, Apartment::Tenant.current == current_organization.name ].all? end def safari_override use_secure_headers_override(:safari_override) if browser.safari? end private def organization_key params[:oauth_consumer_key] || session[:organization_key] end def organization_id params[:organization_id] end def session_key if params[:encrypted_session_key] crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base[0..31]) return crypt.decrypt_and_verify(params[:encrypted_session_key]) end params[:session_key] || session_key_header || flash[:session_key] || session[:session_key] end def session_key_header if match = request.headers['Authorization'].try(:match, /token=(.+)/) match[1] end end # Redirect with the session key intact. In production, # handle this by encrypting the session key. That way if the # url is logged anywhere, it will all be encrypted data. In dev, # just put it in the URL. Putting it in the URL # is insecure, but is fine in development. # 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(location, params = {}) if Rails.env.development? redirect_development_mode(location, params) else redirect_production_mode(location, params) end end def redirect_development_mode(location, params) redirect_to send(location, { session_key: current_session.session_key, organization_id: current_organization.id }.merge(params)) end def redirect_production_mode(location, params) crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base[0..31]) encrypted_data = crypt.encrypt_and_sign(current_session.session_key) redirect_to send(location, { encrypted_session_key: encrypted_data, organization_id: current_organization.id }.merge(params)) end end