LTI 1.3: Add security claims and sign with Canvas private key
Closes PLAT-3633, PLAT-3634 Test Plan: - Do an LTI 1.3 launch in Canvas and verify the id token is signed with the current canvas secret key. - Verify the following claims are included and correct: * exp * iat * iss * nonce * sub Change-Id: I57699ac42bbe98a9fa03f82f3f9b9a16c6923011 Reviewed-on: https://gerrit.instructure.com/159855 Tested-by: Jenkins Reviewed-by: Marc Alan Phillips <mphillips@instructure.com> QA-Review: Marc Alan Phillips <mphillips@instructure.com> Product-Review: Marc Alan Phillips <mphillips@instructure.com>
This commit is contained in:
parent
a709c080cd
commit
59942f4ff8
|
@ -1,6 +1,7 @@
|
|||
production:
|
||||
# replace this with a random string of at least 20 characters
|
||||
encryption_key: 12345
|
||||
lti_iss: 'https://canvas.instructure.com'
|
||||
|
||||
development:
|
||||
encryption_key: facdd3a131ddd8988b14f6e4e01039c93cfa0160
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
production: &default
|
||||
encryption_key: <%= ENV["ENCRYPTION_KEY"] %>
|
||||
lti_iss: 'https://canvas.instructure.com'
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
|
|
@ -49,7 +49,7 @@ module Lti
|
|||
|
||||
# Retrieve the public keys in JWK format
|
||||
#
|
||||
# @return [Hash] The hash of public key in JWK format
|
||||
# @return [Array] The array of public keys in JWK format
|
||||
def public_keyset
|
||||
retrieve_keys.values.map do |private_jwk|
|
||||
public_jwk = private_jwk.to_key.public_key.to_jwk
|
||||
|
@ -57,6 +57,13 @@ module Lti
|
|||
end
|
||||
end
|
||||
|
||||
# Retrieve the present key
|
||||
#
|
||||
# @return [JSON::JWK] the present private key
|
||||
def present_key
|
||||
JSON::JWK.new(Lti::KeyStorage.retrieve_keys[PRESENT])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize_keys
|
||||
|
|
|
@ -40,19 +40,18 @@ module Lti::Messages
|
|||
add_private_claims!
|
||||
|
||||
@expander.expand_variables!(@message.extensions)
|
||||
{ id_token: @message.to_jws(OpenSSL::PKey::RSA.new(1024)) }
|
||||
{ id_token: @message.to_jws(Lti::KeyStorage.present_key) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_security_claims!
|
||||
@message.aud = 'TODO: aud'
|
||||
@message.azp = 'TODO: azp'
|
||||
@message.deployment_id = 'TODO: deployment id'
|
||||
@message.exp = 'TODO: exp'
|
||||
@message.aud = "TODO: Client ID"
|
||||
@message.deployment_id = 'TODO: Deployment ID'
|
||||
@message.exp = Setting.get('lti.oauth2.access_token.exp', 1.hour).to_i.seconds.from_now.to_i
|
||||
@message.iat = Time.zone.now.to_i
|
||||
@message.iss = 'TODO: iss'
|
||||
@message.nonce = 'TODO: nonce'
|
||||
@message.iss = Canvas::Security.config['lti_iss']
|
||||
@message.nonce = SecureRandom.uuid
|
||||
@message.sub = Lti::Asset.opaque_identifier_for(@user)
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ require 'openssl'
|
|||
module Lti
|
||||
class RSAKeyPair < JWKKeyPair
|
||||
ALG = 'RS256'.freeze
|
||||
SIZE = 256
|
||||
SIZE = 2048
|
||||
def initialize(use: 'sig')
|
||||
@alg = ALG
|
||||
@use = use
|
||||
|
|
|
@ -16,18 +16,13 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../api_spec_helper')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../../lti_1_3_spec_helper')
|
||||
|
||||
require_dependency "lti/ims/security_controller"
|
||||
|
||||
module Lti::Ims
|
||||
RSpec.describe SecurityController, type: :request do
|
||||
before do
|
||||
@fallback_proxy = Canvas::DynamicSettings::FallbackProxy.new({
|
||||
Lti::KeyStorage::PAST => Lti::RSAKeyPair.new.to_jwk.to_json,
|
||||
Lti::KeyStorage::PRESENT => Lti::RSAKeyPair.new.to_jwk.to_json,
|
||||
Lti::KeyStorage::FUTURE => Lti::RSAKeyPair.new.to_jwk.to_json
|
||||
})
|
||||
allow(Canvas::DynamicSettings).to receive(:kv_proxy).and_return(@fallback_proxy)
|
||||
end
|
||||
include_context 'lti_1_3_spec_helper'
|
||||
|
||||
let(:url) { Rails.application.routes.url_helpers.jwks_show_path }
|
||||
let(:json) { JSON.parse(response.body) }
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../sharding_spec_helper')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../lti_1_3_spec_helper')
|
||||
|
||||
describe ApplicationController do
|
||||
|
||||
|
@ -545,6 +546,8 @@ describe ApplicationController do
|
|||
end
|
||||
|
||||
describe 'LTI 1.3' do
|
||||
include_context 'lti_1_3_spec_helper'
|
||||
|
||||
before do
|
||||
tool.settings['use_1_3'] = true
|
||||
tool.save!
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../lti_1_3_spec_helper')
|
||||
|
||||
describe ExternalToolsController do
|
||||
include ExternalToolsSpecHelper
|
||||
|
@ -96,6 +97,8 @@ describe ExternalToolsController do
|
|||
|
||||
describe "GET 'show'" do
|
||||
context 'resource link request' do
|
||||
include_context 'lti_1_3_spec_helper'
|
||||
|
||||
let(:tool) do
|
||||
tool = @course.context_external_tools.new(
|
||||
name: "bob",
|
||||
|
|
|
@ -15,9 +15,11 @@
|
|||
# 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.rb')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../../lti_1_3_spec_helper')
|
||||
|
||||
describe Lti::Messages::JwtMessage do
|
||||
include_context 'lti_1_3_spec_helper'
|
||||
|
||||
let(:return_url) { 'http://www.platform.com/return_url' }
|
||||
let(:user) { @student }
|
||||
let(:opts) { { resource_type: 'course_navigation' } }
|
||||
|
@ -69,7 +71,12 @@ describe Lti::Messages::JwtMessage do
|
|||
|
||||
let(:decoded_jwt) do
|
||||
jws = jwt_message.generate_post_payload
|
||||
JSON::JWT.decode(jws[:id_token], :skip_verification)
|
||||
JSON::JWT.decode(jws[:id_token], pub_key)
|
||||
end
|
||||
|
||||
let(:pub_key) do
|
||||
jwk = JSON::JWK.new(Lti::KeyStorage.retrieve_keys['jwk-present.json'])
|
||||
jwk.to_key.public_key
|
||||
end
|
||||
|
||||
let_once(:course) do
|
||||
|
@ -79,37 +86,57 @@ describe Lti::Messages::JwtMessage do
|
|||
|
||||
let_once(:assignment) { assignment_model(course: course) }
|
||||
|
||||
xdescribe '#add_security_claims!' do
|
||||
it 'sets the "aud" claim' do
|
||||
describe 'signing' do
|
||||
it 'signs the id token with the current canvas private key' do
|
||||
jws = jwt_message.generate_post_payload
|
||||
|
||||
expect do
|
||||
JSON::JWT.decode(jws[:id_token], pub_key)
|
||||
end.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
describe 'security claims' do
|
||||
xit 'sets the "aud" claim' do
|
||||
expect(decoded_jwt.aud).to eq ''
|
||||
end
|
||||
|
||||
it 'sets the "azp" claim' do
|
||||
xit 'sets the "azp" claim' do
|
||||
expect(decoded_jwt.azp).to eq ''
|
||||
end
|
||||
|
||||
it 'sets the "deployment_id" claim' do
|
||||
xit 'sets the "deployment_id" claim' do
|
||||
expect(decoded_jwt.deployment_id).to eq ''
|
||||
end
|
||||
|
||||
it 'sets the "exp" claim' do
|
||||
expect(decoded_jwt.exp).to eq ''
|
||||
it 'sets the "exp" claim to lti.oauth2.access_token.exp' do
|
||||
Timecop.freeze do
|
||||
expect(decoded_jwt['exp']).to eq Setting.get('lti.oauth2.access_token.exp', 1.hour).to_i.seconds.from_now.to_i
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the "iat" claim' do
|
||||
expect(decoded_jwt.iat).to eq ''
|
||||
it 'sets the "iat" claim to the current time' do
|
||||
Timecop.freeze do
|
||||
expect(decoded_jwt['iat']).to eq Time.zone.now.to_i
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the "iss" claim' do
|
||||
expect(decoded_jwt.iss).to eq ''
|
||||
it 'sets the "iss" to "https://canvas.instructure.com"' do
|
||||
config = "test:\n lti_iss: 'https://canvas.instructure.com'"
|
||||
allow(Canvas::Security).to receive(:config).and_return(YAML.safe_load(config)[Rails.env])
|
||||
expect(decoded_jwt['iss']).to eq 'https://canvas.instructure.com'
|
||||
end
|
||||
|
||||
it 'sets the "nonce" claim' do
|
||||
expect(decoded_jwt.nonce).to eq ''
|
||||
it 'sets the "nonce" claim to a unique ID' do
|
||||
first_nonce = decoded_jwt['nonce']
|
||||
jws = jwt_message.generate_post_payload
|
||||
second_nonce = JSON::JWT.decode(jws[:id_token], pub_key)['nonce']
|
||||
|
||||
expect(first_nonce).not_to eq second_nonce
|
||||
end
|
||||
|
||||
it 'sets the "sub" claim' do
|
||||
expect(decoded_jwt.sub).to eq ''
|
||||
expect(decoded_jwt['sub']).to eq user.lti_context_id
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -314,7 +341,7 @@ describe Lti::Messages::JwtMessage do
|
|||
|
||||
let(:account_jwt) do
|
||||
jws = account_jwt_message.generate_post_payload
|
||||
JSON::JWT.decode(jws[:id_token], :skip_verification)
|
||||
JSON::JWT.decode(jws[:id_token], pub_key)
|
||||
end
|
||||
|
||||
it 'adds the canvas account id' do
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper.rb')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../../lti_1_3_spec_helper')
|
||||
|
||||
describe Lti::Messages::ResourceLinkRequest do
|
||||
include_context 'lti_1_3_spec_helper'
|
||||
|
||||
let(:return_url) { 'http://www.platform.com/return_url' }
|
||||
let(:opts) { { resource_type: 'course_navigation' } }
|
||||
|
|
|
@ -20,14 +20,14 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
|||
|
||||
describe Lti::RSAKeyPair do
|
||||
describe "initialize" do
|
||||
it 'generates a public key of default size 256' do
|
||||
it 'generates a public key of default size 2048' do
|
||||
keys = Lti::RSAKeyPair.new
|
||||
expect(/\d+/.match(keys.public_key.to_text())[0]).to eq "256"
|
||||
expect(/\d+/.match(keys.public_key.to_text())[0]).to eq "2048"
|
||||
end
|
||||
|
||||
it 'generates a private key of default size 256' do
|
||||
it 'generates a private key of default size 2048' do
|
||||
keys = Lti::RSAKeyPair.new
|
||||
expect(/\d+/.match(keys.private_key.to_text())[0]).to eq "256"
|
||||
expect(/\d+/.match(keys.private_key.to_text())[0]).to eq "2048"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,7 +20,6 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
|
|||
require 'simple_oauth'
|
||||
|
||||
describe Lti::Security do
|
||||
|
||||
describe '.signed_post_params' do
|
||||
let(:params) { {custom_a: 1, custom_b:2} }
|
||||
let(:consumer_key) { 'test' }
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
#
|
||||
# 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.rb')
|
||||
|
||||
RSpec.shared_context "lti_1_3_spec_helper", shared_context: :metadata do
|
||||
let(:fallback_proxy) do
|
||||
Canvas::DynamicSettings::FallbackProxy.new({
|
||||
Lti::KeyStorage::PAST => Lti::RSAKeyPair.new.to_jwk.to_json,
|
||||
Lti::KeyStorage::PRESENT => Lti::RSAKeyPair.new.to_jwk.to_json,
|
||||
Lti::KeyStorage::FUTURE => Lti::RSAKeyPair.new.to_jwk.to_json
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Canvas::DynamicSettings).to receive(:kv_proxy).and_return(fallback_proxy)
|
||||
end
|
||||
end
|
|
@ -16,8 +16,11 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../lti_1_3_spec_helper')
|
||||
|
||||
describe Lti::LtiAdvantageAdapter do
|
||||
include_context 'lti_1_3_spec_helper'
|
||||
|
||||
let(:return_url) { 'http://www.platform.com/return_url' }
|
||||
let(:user) { @student }
|
||||
let(:opts) { { resource_type: 'course_navigation' } }
|
||||
|
|
Loading…
Reference in New Issue