require 'browser' module CoalescingPanda module ControllerHelpers extend ActiveSupport::Concern included do alias_method :rails_session, :session helper_method :encrypted_session_key, :current_session_data, :current_session append_after_action :save_session, if: -> { @current_session && session_changed? } end class_methods do def use_native_sessions after_action do rails_session['persistent_session_key'] = current_session.session_key if @current_session.present? end end end def current_session @current_session ||= (CoalescingPanda::PersistentSession.find_by(session_key: session_key) if session_key) @current_session ||= (CoalescingPanda::PersistentSession.create_from_launch(params, current_lti_account.id) if current_lti_account.present?) @current_session end def current_lti_account @account ||= (CoalescingPanda::LtiAccount.find_by!(key: organization_key) if organization_key) @account ||= (CoalescingPanda::LtiAccount.find_by(id: organization_id) if organization_id) @account end def current_session_data current_session.data end def encrypted_session_key msg_encryptor.encrypt_and_sign(current_session.session_key) end def save_session current_session.try(:save) end def session_changed? current_session.changed? && current_session.changes[:data].present? end def canvas_oauth2(*roles) return if have_session? if lti_authorize!(*roles) user_id = params['user_id'] launch_presentation_return_url = @lti_account.settings[:launch_presentation_return_url] || params['launch_presentation_return_url'] launch_presentation_return_url = [BearcatUri.new(request.env["HTTP_REFERER"]).prefix, launch_presentation_return_url].join unless launch_presentation_return_url.include?('http') uri = BearcatUri.new(launch_presentation_return_url) set_session(launch_presentation_return_url) api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', user_id, uri.api_domain) if api_auth begin refresh_token(uri, api_auth) if api_auth.expired? @client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix) @client.user_profile 'self' rescue Footrest::HttpError::BadRequest, Footrest::HttpError::Unauthorized # If we can't retrieve our own user profile, or the token refresh fails, something is awry on the canvas end # and we'll need to go through the oauth flow again render_oauth2_page uri, user_id end else render_oauth2_page uri, user_id end end end def render_oauth2_page(uri, user_id) @lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key']) return unless @lti_account client_id = @lti_account.oauth2_client_id client = Bearcat::Client.new(prefix: uri.prefix) state = SecureRandom.hex(32) OauthState.create! state_key: state, data: { key: params['oauth_consumer_key'], user_id: user_id, api_domain: uri.api_domain } redirect_path = coalescing_panda.oauth2_redirect_path redirect_url = [coalescing_panda_url, redirect_path.sub(/^\/lti/, '')].join @canvas_url = client.auth_redirect_url(client_id, redirect_url, { state: state }) #delete the added params so the original oauth sig still works @lti_params = params.to_hash @lti_params.delete('action') @lti_params.delete('controller') render 'coalescing_panda/oauth2/oauth2', layout: 'coalescing_panda/application' end def refresh_token(uri, api_auth) refresh_client = Bearcat::Client.new(prefix: uri.prefix) refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id, coalescing_panda.oauth2_redirect_url, @lti_account.oauth2_client_key, api_auth.refresh_token, 'refresh_token') api_auth.update({ api_token: refresh_body['access_token'], expires_at: (Time.now + refresh_body['expires_in']) }) end def check_refresh_token return unless current_session_data['uri'] && current_session_data['user_id'] && current_session_data['oauth_consumer_key'] uri = BearcatUri.new(current_session_data['uri']) api_auth = CanvasApiAuth.find_by(user_id: current_session_data['user_id'], api_domain: uri.api_domain) @lti_account = LtiAccount.find_by(key: current_session_data['oauth_consumer_key']) return if @lti_account.nil? || api_auth.nil? # Not all tools use oauth refresh_token(uri, api_auth) if api_auth.expired? rescue Footrest::HttpError::BadRequest render_oauth2_page uri, current_session_data['user_id'] end def set_session(launch_presentation_return_url) current_session_data['user_id'] = params['user_id'] current_session_data['uri'] = launch_presentation_return_url current_session_data['lis_person_sourcedid'] = params['lis_person_sourcedid'] current_session_data['oauth_consumer_key'] = params['oauth_consumer_key'] current_session_data['custom_canvas_account_id'] = params['custom_canvas_account_id'] end def have_session? if params['tool_consumer_instance_guid'] && current_session_data['user_id'] != params['user_id'] reset_session logger.info("resetting session params") current_session_data['user_id'] = params['user_id'] end if (current_session_data['user_id'] && current_session_data['uri']) uri = BearcatUri.new(current_session_data['uri']) api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', current_session_data['user_id'], uri.api_domain) if api_auth && !api_auth.expired? @client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix) @client.user_profile 'self' end end @lti_account = LtiAccount.find_by_key(current_session_data['oauth_consumer_key']) if current_session_data['oauth_consumer_key'] !!@client rescue Footrest::HttpError::Unauthorized false end def lti_authorize!(*roles) authorized = false if (@lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key'])) sanitized_params = sanitize_params @tp = IMS::LTI::ToolProvider.new(@lti_account.key, @lti_account.secret, sanitized_params) authorized = @tp.valid_request?(request) end logger.info 'not authorized on tp valid request' unless authorized authorized = authorized && (roles.count == 0 || (roles & lti_roles).count > 0) logger.info 'not authorized on roles' unless authorized authorized = authorized && @lti_account.validate_nonce(params['oauth_nonce'], DateTime.strptime(params['oauth_timestamp'], '%s')) logger.info 'not authorized on nonce' unless authorized render :text => 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized # create session on first launch current_session authorized end # code for method taken from panda_pal v 4.0.8 # used for safari workaround def sanitize_params 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"] safe_unexpected_params.each do |p| sanitized_params.delete(p) end sanitized_params end def lti_editor_button_response(return_type, return_params) valid_return_types = [:image_url, :iframe, :url, :lti_launch_url] raise "invalid editor button return type #{return_type}" unless valid_return_types.include?(return_type) return_params[:return_type] = return_type.to_s return_url = "#{params['launch_presentation_return_url']}?#{return_params.to_query}" redirect_to return_url end def lti_roles @lti_roles ||= current_session_data[:roles] end def canvas_environment case params['custom_test_environment'] when 'true' :test else :production end end def session_check logger.warn 'session_check is deprecated. Functionality moved to lti_authorize.' end private def msg_encryptor @crypt ||= ActiveSupport::MessageEncryptor.new(Rails.application.secrets.secret_key_base[0..31]) end def organization_key params[:oauth_consumer_key] || (current_session_data[:launch_params][:oauth_consumer_key] if @current_session) end def organization_id params[:organization_id] || (current_session_data[:launch_params][:organization_id] if @current_session) end def session_key if params[:encrypted_session_key] return msg_encryptor.decrypt_and_verify(params[:encrypted_session_key]) end params[:session_key] || session_key_header || rails_session['persistent_session_key'] end def session_key_header if (match = request.headers['Authorization'].try(:match, /crypted_token=(.+)/)) msg_encryptor.decrypt_and_verify(match[1]) elsif (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 every time it tries to live reload). def redirect_with_session_to(path, id_or_resource = nil, redirect_params = {}) if Rails.env.development? || Rails.env.test? redirect_development_mode(path, id_or_resource, redirect_params) else redirect_production_mode(path, id_or_resource, redirect_params) end end def redirect_development_mode(path, id_or_resource = nil, redirect_params) redirect_to send(path, id_or_resource, { session_key: current_session.session_key, organization_id: current_lti_account.id }.merge(redirect_params)) end def redirect_production_mode(path, id_or_resource = nil, redirect_params) redirect_to send(path, id_or_resource, { encrypted_session_key: encrypted_session_key, organization_id: current_lti_account.id }.merge(redirect_params)) end end end