# 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 . # # 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