canvas-lms/lib/lti/security.rb

137 lines
5.4 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2016 - 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/>.
#
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
module Lti
module Security
def self.signed_post_params(params, url, key, secret, disable_lti_post_only=false)
if disable_lti_post_only
signed_post_params_frd(params, url, key, secret)
else
generate_params_deprecated(params, url, key, secret)
end
end
# This is the correct way to sign the params, but in the name of not breaking things, we are using the
# #generate_params_deprecated method by default
def self.signed_post_params_frd(params, url, key, secret)
message = IMS::LTI::Models::Messages::Message.generate(params.merge({oauth_consumer_key: key}))
message.launch_url = url
signed_parameters = message.signed_post_params(secret).stringify_keys
Lti::Logging.lti_1_launch_generated(message.message_authenticator.base_string)
signed_parameters
end
private_class_method :signed_post_params_frd
# This method does a couple of things wrong
# 1. It copies params from the url to the body by default, this should really be a config setting instead, not the
# default behaviour
# 2. It doesn't generate the signature correctly when there are duplicate params in the body and query. It should
# add the params to the base string for each time they appear in the query and body. Instead it only adds the
# params once no matter how many times it appears. For query params since we copy them to the body, it should
# appear a minimum of twice in the base string.
def self.generate_params_deprecated(params, url, key, secret)
uri = URI.parse(url.strip)
if uri.port == uri.default_port
host = uri.host
else
host = "#{uri.host}:#{uri.port}"
end
consumer = OAuth::Consumer.new(key, secret, {
:site => "#{uri.scheme}://#{host}",
:signature_method => 'HMAC-SHA1'
})
path = uri.path
path = '/' if path.empty?
if uri.query && uri.query != ''
CGI.parse(uri.query).each do |query_key, query_values|
unless params[query_key]
params[query_key] = query_values.first
end
end
end
options = {:scheme => 'body'}
request = consumer.create_signed_request(:post, path, nil, options, params.stringify_keys)
# the request is made by a html form in the user's browser, so we
# want to revert the escapage and return the hash of post parameters ready
# for embedding in a html view
hash = {}
request.body.split(/&/).each do |param|
key, val = param.split(/=/).map { |v| CGI.unescape(v) }
hash[key] = val
end
# note that this base string has duplicate oauth parameters in it when logged,
# though these parameters don't affect signature generation and oauth launches (I hope?)
Lti::Logging.lti_1_launch_generated(request.oauth_helper.signature_base_string)
hash.stringify_keys
end
private_class_method :generate_params_deprecated
##
# Used to determine if the nonce is still valid
#
# +cache_key+:: This is the redis cache key used to check if the nonce key has been used
# +timestamp+:: The timestamp of when the request was signed
# +nonce_age+:: An ActiveSupport::Duration describing how old a nonce can be
#
# The +nonce_age+ creates a range that the timestamp must fall between for the nonce to be valid
# valid_range = +Time.now+ - (the +nonce_age+ duration)
# i.e. if the current time was 2010-04-23T12:30:00Z and the +nonce_age+ was 30min
# then the valid time range that the timestamp must fall between would
# be "2010-04-23T12:30:00Z/2010-04-23T13:00:00Z"
#
# =Time line Examples for valid and invalid timestamps
#
# |---nonce_age---timestamp---Time.now---| VALID
#
# |---timestamp---nonce_age---Time.now---| INVALID
#
# |---nonce_age---Time.now---timestamp---| INVALID
#
def self.check_and_store_nonce(cache_key, timestamp, nonce_age)
allowed_future_skew = Setting.get('oauth.allowed_timestamp_future_skew', 1.minute.to_s).to_i.seconds
valid = timestamp.to_i > nonce_age.ago.to_i
valid &&= timestamp.to_i <= (Time.zone.now + allowed_future_skew).to_i
valid &&= !Rails.cache.exist?(cache_key)
Rails.cache.write(cache_key, 'OK', expires_in: nonce_age + allowed_future_skew) if valid
valid
end
def self.decoded_lti_assignment_id(secure_params)
return if secure_params.blank?
secure_params = Canvas::Security.decode_jwt(secure_params)
secure_params[:lti_assignment_id]
rescue Canvas::Security::InvalidToken
return nil
end
end
end