Store LTI message in Redis
Closes PLAT-3949 Test Plan: - Verify the following for assignment, course nav, and user nav placements: * The ID token is sored as JSON in redis using the same key scheme as sessionless launches * The ID token contains all resource link request claims - The generate sesionles launch endpoint works as before. - The sessionless launch endpoint works as before. Change-Id: I5a3b201ab460facedd4eb03094fbac0ca12a4546 Reviewed-on: https://gerrit.instructure.com/170827 Tested-by: Jenkins Reviewed-by: Marc Phillips <mphillips@instructure.com> QA-Review: Marc Phillips <mphillips@instructure.com> Product-Review: Weston Dransfield <wdransfield@instructure.com>
This commit is contained in:
parent
97a910bcf0
commit
160356823e
|
@ -1618,6 +1618,7 @@ class ApplicationController < ActionController::Base
|
|||
@lti_launch.params = adapter.generate_post_payload
|
||||
end
|
||||
|
||||
adapter.cache_payload if adapter.respond_to?(:cache_payload)
|
||||
@lti_launch.resource_url = @resource_url
|
||||
@lti_launch.link_text = @resource_title
|
||||
@lti_launch.analytics_id = @tool.tool_id
|
||||
|
|
|
@ -30,8 +30,8 @@ class ExternalToolsController < ApplicationController
|
|||
before_action :get_context, :only => [:retrieve, :show, :resource_selection]
|
||||
skip_before_action :verify_authenticity_token, only: :resource_selection
|
||||
include Api::V1::ExternalTools
|
||||
include Lti::RedisMessageClient
|
||||
|
||||
REDIS_PREFIX = 'external_tool:sessionless_launch:'.freeze
|
||||
WHITELISTED_QUERY_PARAMS = [
|
||||
:platform
|
||||
].freeze
|
||||
|
@ -242,9 +242,11 @@ class ExternalToolsController < ApplicationController
|
|||
|
||||
def sessionless_launch
|
||||
if Canvas.redis_enabled?
|
||||
redis_key = "#{@context.class.name}:#{REDIS_PREFIX}#{params[:verifier]}"
|
||||
launch_settings = Canvas.redis.get(redis_key)
|
||||
Canvas.redis.del(redis_key)
|
||||
launch_settings = fetch_and_delete_launch(
|
||||
@context,
|
||||
params[:verifier],
|
||||
prefix: Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX
|
||||
)
|
||||
end
|
||||
unless launch_settings
|
||||
render :plain => t(:cannot_locate_launch_request, 'Cannot locate launch request, please try again.'), :status => :not_found
|
||||
|
@ -503,11 +505,11 @@ class ExternalToolsController < ApplicationController
|
|||
opts: opts
|
||||
)
|
||||
else
|
||||
Lti::LtiOutboundAdapter.new(tool, @current_user, @context).prepare_tool_launch(
|
||||
@return_url,
|
||||
expander,
|
||||
opts
|
||||
)
|
||||
Lti::LtiOutboundAdapter.new(tool, @current_user, @context).prepare_tool_launch(
|
||||
@return_url,
|
||||
expander,
|
||||
opts
|
||||
)
|
||||
end
|
||||
|
||||
lti_launch.params = if selection_type == 'homework_submission' && assignment
|
||||
|
@ -516,6 +518,7 @@ class ExternalToolsController < ApplicationController
|
|||
adapter.generate_post_payload
|
||||
end
|
||||
|
||||
adapter.cache_payload if adapter.respond_to?(:cache_payload)
|
||||
lti_launch.resource_url = opts[:launch_url] || adapter.launch_url
|
||||
lti_launch.link_text = selection_type ? tool.label_for(selection_type.to_sym, I18n.locale) : tool.default_label
|
||||
lti_launch.analytics_id = tool.tool_id
|
||||
|
@ -1157,8 +1160,7 @@ class ExternalToolsController < ApplicationController
|
|||
end
|
||||
|
||||
# store the launch settings and return to the user
|
||||
verifier = SecureRandom.hex(64)
|
||||
Canvas.redis.setex("#{@context.class.name}:#{REDIS_PREFIX}#{verifier}", 5.minutes, launch_settings.to_json)
|
||||
verifier = cache_launch(launch_settings, @context, prefix: Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX)
|
||||
|
||||
uri = if @context.is_a?(Account)
|
||||
URI(account_external_tools_sessionless_launch_url(@context))
|
||||
|
|
|
@ -1284,6 +1284,8 @@ class UsersController < ApplicationController
|
|||
else
|
||||
Lti::LtiOutboundAdapter.new(@tool, @current_user, @domain_root_account).prepare_tool_launch(@return_url, variable_expander, opts)
|
||||
end
|
||||
|
||||
adapter.cache_payload if adapter.respond_to?(:cache_payload)
|
||||
@lti_launch.params = adapter.generate_post_payload
|
||||
|
||||
@lti_launch.resource_url = @tool.user_navigation(:url)
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
module Lti
|
||||
class LtiAdvantageAdapter
|
||||
include Lti::RedisMessageClient
|
||||
|
||||
delegate :generate_post_payload_for_assignment, to: :resource_link_request
|
||||
delegate :generate_post_payload_for_homework_submission, to: :resource_link_request
|
||||
|
||||
|
@ -43,6 +45,10 @@ module Lti
|
|||
resource_type ? @tool.extension_setting(resource_type, :url) : @tool.url
|
||||
end
|
||||
|
||||
def cache_payload
|
||||
cache_launch(generate_post_payload, @context)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_link_request
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
#
|
||||
# Copyright (C) 2018 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
module Lti::RedisMessageClient
|
||||
TTL = 5.minutes
|
||||
LTI_1_3_PREFIX = 'external_tool:id_token:'.freeze
|
||||
SESSIONLESS_LAUNCH_PREFIX = 'external_tool:sessionless_launch:'.freeze
|
||||
|
||||
def cache_launch(launch, context, prefix: LTI_1_3_PREFIX)
|
||||
return unless Canvas.redis_enabled?
|
||||
verifier = SecureRandom.hex(64)
|
||||
Canvas.redis.setex("#{context.class.name}:#{prefix}#{verifier}", 5.minutes, launch.to_json)
|
||||
verifier
|
||||
end
|
||||
|
||||
def fetch_and_delete_launch(context, verifier, prefix: LTI_1_3_PREFIX)
|
||||
return unless Canvas.redis_enabled?
|
||||
redis_key = "#{context.class.name}:#{prefix}#{verifier}"
|
||||
launch = Canvas.redis.get(redis_key)
|
||||
Canvas.redis.del(redis_key)
|
||||
launch
|
||||
end
|
||||
end
|
|
@ -606,21 +606,39 @@ RSpec.describe ApplicationController do
|
|||
tool.save!
|
||||
end
|
||||
|
||||
shared_examples_for 'a placement that caches the launch' do
|
||||
let(:verifier) { "e5e774d015f42370dcca2893025467b414d39009dfe9a55250279cca16f5f3c2704f9c56fef4cea32825a8f72282fa139298cf846e0110238900567923f9d057" }
|
||||
let(:redis_key) { "#{course.class.name}:#{Lti::RedisMessageClient::LTI_1_3_PREFIX}#{verifier}" }
|
||||
let(:cached_launch) { JSON::JWT.decode(JSON.parse(Canvas.redis.get(redis_key))['id_token'], :skip_verification) }
|
||||
|
||||
before { allow(SecureRandom).to receive(:hex).and_return(verifier) }
|
||||
|
||||
it 'caches the LTI 1.3 launch' do
|
||||
controller.send(:content_tag_redirect, course, content_tag, nil)
|
||||
expect(cached_launch["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
end
|
||||
|
||||
context 'assignments' do
|
||||
it 'creates a resource link request when tool is configured to use LTI 1.3' do
|
||||
controller.send(:content_tag_redirect, course, content_tag, nil)
|
||||
jwt = JSON::JWT.decode(assigns[:lti_launch].params[:id_token], :skip_verification)
|
||||
expect(jwt["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
|
||||
it_behaves_like 'a placement that caches the launch'
|
||||
end
|
||||
|
||||
context 'module items' do
|
||||
before { content_tag.update!(context: course.account) }
|
||||
|
||||
it 'creates a resource link request when tool is configured to use LTI 1.3' do
|
||||
content_tag.update!(context: course.account)
|
||||
controller.send(:content_tag_redirect, course, content_tag, nil)
|
||||
jwt = JSON::JWT.decode(assigns[:lti_launch].params[:id_token], :skip_verification)
|
||||
expect(jwt["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
|
||||
it_behaves_like 'a placement that caches the launch'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -113,12 +113,24 @@ describe ExternalToolsController do
|
|||
tool
|
||||
end
|
||||
|
||||
it 'creates a resource link request when tool is configured to use LTI 1.3' do
|
||||
let(:verifier) { "e5e774d015f42370dcca2893025467b414d39009dfe9a55250279cca16f5f3c2704f9c56fef4cea32825a8f72282fa139298cf846e0110238900567923f9d057" }
|
||||
let(:redis_key) { "#{@course.class.name}:#{Lti::RedisMessageClient::LTI_1_3_PREFIX}#{verifier}" }
|
||||
let(:cached_launch) { JSON::JWT.decode(JSON.parse(Canvas.redis.get(redis_key))['id_token'], :skip_verification) }
|
||||
|
||||
before do
|
||||
allow(SecureRandom).to receive(:hex).and_return(verifier)
|
||||
user_session(@teacher)
|
||||
get :show, params: {:course_id => @course.id, id: tool.id}
|
||||
end
|
||||
|
||||
it 'creates a resource link request when tool is configured to use LTI 1.3' do
|
||||
jwt = JSON::JWT.decode(assigns[:lti_launch].params[:id_token], :skip_verification)
|
||||
expect(jwt["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
|
||||
it 'caches the the LTI 1.3 launch' do
|
||||
expect(cached_launch["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
end
|
||||
|
||||
context 'basic-lti-launch-request' do
|
||||
|
@ -1435,7 +1447,7 @@ describe ExternalToolsController do
|
|||
|
||||
json = JSON.parse(response.body.sub(/^while\(1\)\;/, ''))
|
||||
verifier = CGI.parse(URI.parse(json['url']).query)['verifier'].first
|
||||
redis_key = "#{@course.class.name}:#{ExternalToolsController::REDIS_PREFIX}#{verifier}"
|
||||
redis_key = "#{@course.class.name}:#{Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX}#{verifier}"
|
||||
launch_settings = JSON.parse(Canvas.redis.get(redis_key))
|
||||
tool_settings = launch_settings['tool_settings']
|
||||
|
||||
|
@ -1455,7 +1467,7 @@ describe ExternalToolsController do
|
|||
|
||||
json = JSON.parse(response.body.sub(/^while\(1\)\;/, ''))
|
||||
verifier = CGI.parse(URI.parse(json['url']).query)['verifier'].first
|
||||
redis_key = "#{@course.class.name}:#{ExternalToolsController::REDIS_PREFIX}#{verifier}"
|
||||
redis_key = "#{@course.class.name}:#{Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX}#{verifier}"
|
||||
launch_settings = JSON.parse(Canvas.redis.get(redis_key))
|
||||
|
||||
expect(launch_settings['launch_url']).to eq 'http://www.example.com/basic_lti'
|
||||
|
@ -1478,7 +1490,7 @@ describe ExternalToolsController do
|
|||
|
||||
json = JSON.parse(response.body.sub(/^while\(1\)\;/, ''))
|
||||
verifier = CGI.parse(URI.parse(json['url']).query)['verifier'].first
|
||||
redis_key = "#{@course.class.name}:#{ExternalToolsController::REDIS_PREFIX}#{verifier}"
|
||||
redis_key = "#{@course.class.name}:#{Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX}#{verifier}"
|
||||
launch_settings = JSON.parse(Canvas.redis.get(redis_key))
|
||||
tool_settings = launch_settings['tool_settings']
|
||||
|
||||
|
@ -1511,7 +1523,7 @@ describe ExternalToolsController do
|
|||
|
||||
json = JSON.parse(response.body.sub(/^while\(1\)\;/, ''))
|
||||
verifier = CGI.parse(URI.parse(json['url']).query)['verifier'].first
|
||||
redis_key = "#{@course.class.name}:#{ExternalToolsController::REDIS_PREFIX}#{verifier}"
|
||||
redis_key = "#{@course.class.name}:#{Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX}#{verifier}"
|
||||
launch_settings = JSON.parse(Canvas.redis.get(redis_key))
|
||||
tool_settings = launch_settings['tool_settings']
|
||||
|
||||
|
@ -1557,7 +1569,7 @@ describe ExternalToolsController do
|
|||
|
||||
json = JSON.parse(response.body.sub(/^while\(1\)\;/, ''))
|
||||
verifier = CGI.parse(URI.parse(json['url']).query)['verifier'].first
|
||||
redis_key = "#{@course.class.name}:#{ExternalToolsController::REDIS_PREFIX}#{verifier}"
|
||||
redis_key = "#{@course.class.name}:#{Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX}#{verifier}"
|
||||
launch_settings = JSON.parse(Canvas.redis.get(redis_key))
|
||||
|
||||
expect(launch_settings['tool_settings']['resource_link_id']). to eq opaque_id(@tg)
|
||||
|
@ -1585,7 +1597,7 @@ describe ExternalToolsController do
|
|||
|
||||
json = JSON.parse(response.body.sub(/^while\(1\)\;/, ''))
|
||||
verifier = CGI.parse(URI.parse(json['url']).query)['verifier'].first
|
||||
redis_key = "#{@course.class.name}:#{ExternalToolsController::REDIS_PREFIX}#{verifier}"
|
||||
redis_key = "#{@course.class.name}:#{Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX}#{verifier}"
|
||||
launch_settings = JSON.parse(Canvas.redis.get(redis_key))
|
||||
expect(launch_settings.dig('tool_settings', 'custom_standard')).to eq @tg.id.to_s
|
||||
end
|
||||
|
|
|
@ -82,20 +82,29 @@ describe UsersController do
|
|||
context 'using LTI 1.3 when specified' do
|
||||
include_context 'lti_1_3_spec_helper'
|
||||
|
||||
let(:verifier) { "e5e774d015f42370dcca2893025467b414d39009dfe9a55250279cca16f5f3c2704f9c56fef4cea32825a8f72282fa139298cf846e0110238900567923f9d057" }
|
||||
let(:redis_key) { "#{assigns[:domain_root_account].class_name}:#{Lti::RedisMessageClient::LTI_1_3_PREFIX}#{verifier}" }
|
||||
let(:cached_launch) { JSON::JWT.decode(JSON.parse(Canvas.redis.get(redis_key))['id_token'], :skip_verification) }
|
||||
|
||||
subject do
|
||||
get :external_tool, params: {id:tool.id, user_id:user.id}
|
||||
JSON::JWT.decode(assigns[:lti_launch].params[:id_token], :skip_verification)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(SecureRandom).to receive(:hex).and_return(verifier)
|
||||
tool.use_1_3 = true
|
||||
tool.developer_key = DeveloperKey.create!
|
||||
tool.save!
|
||||
get :external_tool, params: {id:tool.id, user_id:user.id}
|
||||
end
|
||||
|
||||
it 'does LTI 1.3 launch' do
|
||||
expect(subject["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
|
||||
it 'caches the LTI 1.3 launch' do
|
||||
expect(cached_launch["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
#
|
||||
# Copyright (C) 2018 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
||||
|
||||
describe Lti::RedisMessageClient do
|
||||
include Lti::RedisMessageClient
|
||||
|
||||
let_once(:context) { course_model }
|
||||
|
||||
let(:launch) { {foo: 'bar'} }
|
||||
|
||||
describe '#cache_launch' do
|
||||
it 'caches the launch as JSON' do
|
||||
verifier = cache_launch(launch, context)
|
||||
redis_key = "#{context.class.name}:#{Lti::RedisMessageClient::LTI_1_3_PREFIX}#{verifier}"
|
||||
expect(Canvas.redis.get(redis_key)).to eq launch.to_json
|
||||
end
|
||||
|
||||
it 'allows setting the prefix' do
|
||||
verifier = cache_launch(launch, context, prefix: Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX)
|
||||
redis_key = "#{context.class.name}:#{Lti::RedisMessageClient::SESSIONLESS_LAUNCH_PREFIX}#{verifier}"
|
||||
expect(Canvas.redis.get(redis_key)).to eq launch.to_json
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch_and_delete_launch' do
|
||||
let(:redis_key) { cache_launch(launch, context) }
|
||||
|
||||
it 'fetches the launch data' do
|
||||
cached_launch = fetch_and_delete_launch(context, redis_key)
|
||||
expect(cached_launch).to eq launch.to_json
|
||||
end
|
||||
|
||||
it 'deletes the redis entry' do
|
||||
fetch_and_delete_launch(context, redis_key)
|
||||
cached_launch = fetch_and_delete_launch(context, redis_key)
|
||||
expect(cached_launch).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -72,6 +72,15 @@ describe Lti::LtiAdvantageAdapter do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#cache_payload" do
|
||||
it "caches the payload" do
|
||||
verifier = adapter.cache_payload
|
||||
redis_key = "#{course.class.name}:#{Lti::RedisMessageClient::LTI_1_3_PREFIX}#{verifier}"
|
||||
jwt = JSON::JWT.decode(JSON.parse(Canvas.redis.get(redis_key))['id_token'], :skip_verification)
|
||||
expect(jwt["https://purl.imsglobal.org/spec/lti/claim/message_type"]).to eq "LtiResourceLinkRequest"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#generate_post_payload_for_assignment' do
|
||||
let(:outcome_service_url) { 'https://www.outcome_service_url.com' }
|
||||
let(:legacy_outcome_service_url) { 'https://www.legacy_url.com' }
|
||||
|
|
Loading…
Reference in New Issue