require_dependency "panda_pal/application_controller" module PandaPal class LtiV1P3Controller < ApplicationController if Rails.version < '5.0' skip_before_action :verify_authenticity_token else skip_before_action :verify_authenticity_token, raise: false end before_action :validate_launch!, only: [:resource_link_request] around_action :switch_tenant, only: [:resource_link_request] before_action :enforce_environment!, only: [:resource_link_request] # Redirect to beta/test as necessary before_action :forward_to_env_and_region, only: [:login] def login @current_lti_platform = PandaPal::Platform.resolve_platform(params) current_session_data[:lti_platform] = @current_lti_platform&.serialize current_session_data[:lti_oauth_nonce] = SecureRandom.uuid current_session_data[:canvas_environment] = params['canvas_environment'] current_session_data[:canvas_region] = params['canvas_region'] current_session.panda_pal_organization_id = -1 @form_action = current_lti_platform.authentication_redirect_url @method = :post @form_data = { scope: 'openid', response_type: 'id_token', response_mode: 'form_post', prompt: 'none', redirect_uri: params[:target_link_uri] || v1p3_resource_link_request_url, client_id: params.require(:client_id), login_hint: params.require(:login_hint), lti_message_hint: params.require(:lti_message_hint), state: current_session.session_key, nonce: current_session_data[:lti_oauth_nonce] } end def resource_link_request ltype = @decoded_lti_jwt['https://www.instructure.com/placement'] if ltype current_session_data.merge!({ lti_version: 'v1p3', lti_launch_placement: ltype, launch_params: @decoded_lti_jwt, }) redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(ltype)}_url", route_context: main_app) end end def tool_config if PandaPal.lti_environments.empty? render plain: 'Domains must be set in lti_environments' return end platform = PandaPal.lti_options.delete(:platform) || 'canvas.instructure.com' request_url = "#{request.scheme}://#{request.host_with_port}" parsed_request_url = URI.parse(request_url) mapped_placements = PandaPal.lti_paths.map do |k, opts| opts = LaunchUrlHelpers.normalize_lti_launch_desc(opts) opts.merge!({ placement: k, target_link_uri: LaunchUrlHelpers.absolute_launch_url( k.to_sym, host: parsed_request_url, launch_handler: v1p3_resource_link_request_path, default_auto_launch: true ), }) opts end config_json = { title: PandaPal.lti_options[:title], scopes: [], public_jwk_url: v1p3_public_jwks_url, description: PandaPal.lti_options[:description] || 'PandaPal LTI', target_link_uri: v1p3_resource_link_request_url, #app_url(:resource_link_request, request), oidc_initiation_url: v1p3_oidc_login_url, extensions: [{ platform: platform, privacy_level: "public", settings: { placements: mapped_placements, }, }], custom_fields: PandaPal.lti_custom_params, # PandaPal.lti_options[:custom_fields], } render json: config_json end def public_jwks render json: { keys: [JWT::JWK.new(PandaPal.lti_private_key).export] } end protected def forward_to_env_and_region login_redirects = params['lti_login_redirects'] || [] login_redirects << request.url redir_url = apply_environment_to_url(v1p3_oidc_login_url) if redir_url.present? && redir_url != v1p3_oidc_login_url && !login_redirects.include?(redir_url) @form_action = redir_url @method = request.method.downcase @form_data = (request.POST.presence || request.GET).merge({ lti_login_redirects: login_redirects, }).with_indifferent_access @form_data[:target_link_uri] = apply_environment_to_url(@form_data[:target_link_uri]) render action: :login end end def apply_environment_to_url(url) return url unless url.present? if (canvas_env = params['canvas_environment']).present? tdomain = PandaPal.lti_environments[:"#{canvas_env}_domain"] if tdomain.present? && !request.url.include?(tdomain) && PandaPal.lti_environments[:domain].present? return url.gsub(PandaPal.lti_environments[:domain], tdomain) end end url end def enforce_environment! canvas_env = current_session_data[:canvas_environment] return unless canvas_env.present? org_canvas_url = current_organization.canvas_url if (canvas_env == 'beta' || canvas_env == 'test') && org_canvas_url.present && !org_canvas_url.include?(".#{canvas_env}.") render plain: "This tool is not properly configured for use in #{canvas_env}", status: 400 end end private def auth_redirect_query return unless params[:target_link_uri]&.include? 'platform_redirect_url=' platform_redirect_url = Rack::Utils.parse_query(URI(params[:target_link_uri]).query)&.dig('platform_redirect_url') "?platform_redirect_url=#{platform_redirect_url}" end end end