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 @current_lti_platform ||= current_session(create_missing: false)&.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 # We should verify the timestamp is recent (within 5 minutes). The approved timestamp is part of the signature, # so we don't need to worry about malicious users messing with it. We should deny requests that come too long # after the approved timestamp. good_timestamp = params['oauth_timestamp'] && params['oauth_timestamp'].to_i > Time.now.to_i - 300 if @organization = good_timestamp && 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 tp = IMS::LTI::ToolProvider.new(@organization.key, @organization.secret, sanitized_params) authorized = tp.valid_request?(request) end if !authorized render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized end authorized end def validate_v1p3_launch require "json/jwt" 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'] deployment_id = decoded_jwt['https://purl.imsglobal.org/spec/lti/claim/deployment_id'] if deployment_id.present? @organization ||= PandaPal::Organization.find_by(key: "#{client_id}/#{deployment_id}") @organization ||= PandaPal::Organization.find_by(key: deployment_id) end @organization ||= PandaPal::Organization.find_by(key: client_id) params[:session_key] = params[:state] raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present? raise JSON::JWT::VerificationFailed, 'Organization does not trust platform' unless @organization.trusted_platform?(current_lti_platform) decoded_jwt.verify!(current_lti_platform.public_jwks) 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? current_session.update(panda_pal_organization: @organization) @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 ||= session[:organization_key] org_key end def organization_id params[:organization_id] end end end