module Lti2Commons::MessageSupport

Module to support LTI 1 and 2 secure messaging. This messaging is documented in the LTI 2 Security Document

LTI 2 defines two types of LTI secure messaging:

1. LTI Messages
  This is the model used exclusively in LTI 1.
  It is also used in LTI 2 for user-submitted actions such as LtiLaunch and ToolDeployment.

  It works the following way:
    1. LTI parameters are signed by OAuth.
    2. The message is marshalled into an HTML Form with the params
       specified in form fields.
    3. The form is sent to the browser with a redirect.
    4. An attached Javascript script 'auto-submits' the form.

  This structure appears to the Tool Provider as a user submission with all user browser context
  intact.

2. LTI Services
  This is a standard REST web service with OAuth message security added.
  In LTI 2.0 Services are only defined for Tool Provider --> Tool Consumer services;
  such as, GetToolConsumerProfile, RegisterToolProxy, and LTI 2 Outcomes.
  LTI 2.x will add Tool Consumer --> Tool Provider services using the same machinery.

Constants

TIMEOUT

Public Instance Methods

create_lti_message_body(launch_url, parameters, wire_log = nil, title = nil, is_open_in_external_window = false) click to toggle source

Convenience method signs and then invokes create_lti_message_from_signed_request

@params launch_url [String] Launch url @params parameters [Hash] Full set of params for message @return [String] Post body ready for use

# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 37
def create_lti_message_body(launch_url, parameters, wire_log = nil, title = nil, is_open_in_external_window = false)
  result = create_message_header(launch_url, is_open_in_external_window)
  result += create_message_body(parameters)
  result += create_message_footer(is_open_in_external_window)

  if wire_log
    wire_log.timestamp
    wire_log.raw_log((title.nil?) ? 'LtiMessage' : "LtiMessage: #{title}")
    wire_log.raw_log "LaunchUrl: #{launch_url}"
    wire_log.raw_log result.strip
    wire_log.newline
    wire_log.flush
  end

  result
end
create_lti_message_body_from_signed_request(signed_request, is_include_oauth_params = true, is_open_in_external_window = false) click to toggle source

Creates an LTI Message (POST body) ready for redirecting to the launch_url. Note that the is_include_oauth_params option specifies that 'oauth_' params are included in the body. This option should be false when sender is putting them in the HTTP Authorization header (now recommended).

@param params [Hash] Full set of params for message (including OAuth provided params) @return [String] Post body ready for use

# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 62
def create_lti_message_body_from_signed_request(signed_request, is_include_oauth_params = true,
                                                is_open_in_external_window = false)
  result = create_message_header(signed_request.uri, is_open_in_external_window)
  result += create_message_body(signed_request.parameters, is_include_oauth_params)
  result += create_message_footer(is_open_in_external_window)
  result
end
invoke_service(request, wire_log = nil, title = nil, other_headers = {}) click to toggle source

Invokes an LTI Service. This is fully-compliant REST request suitable for LTI server-to-server services.

@param request [Request] Signed Request encapsulates everything needed for service.

# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 74
def invoke_service(request, wire_log = nil, title = nil, other_headers = {})
  uri = request.uri.to_s
  # set_headers_proc = lambda { |http|
  # http.headers['Authorization'] = request.oauth_header
  # http.headers['Content-Type'] = request.content_type if request.content_type
  # http.headers['Accept'] = request.content_type if request.content_type
  # # http.headers['Content-Length'] = request.body.length if request.body
  # }
  method = request.method.downcase

  headers = {}
  headers['Authorization'] = request.oauth_header
  headers['Content-Type'] = request.content_type if request.content_type
  headers['Accept'] = request.accept if request.accept
  headers['Content-Length'] = request.body.length.to_s if request.body
  headers.merge!(other_headers)

  parameters = request.parameters
  output_parameters = {}
  parameters.each { |k, v| output_parameters[k] = v unless k =~ /^oauth_/ }

  (write_wirelog_header wire_log, title, request.method, uri, headers, parameters, request.body, output_parameters) if wire_log

  full_uri = uri
  full_uri += '?' unless uri.include? '?'
  full_uri += '&' unless full_uri =~ /[?&]$/
  output_parameters.each_pair do |key, _value|
    full_uri << '&' unless key == output_parameters.keys.first
    full_uri << "#{URI.encode(key.to_s)}=#{URI.encode(output_parameters[key] || '')}"
  end

  case method
  when 'get'
    response = HTTParty.get(full_uri, headers: headers, timeout: TIMEOUT)
  when 'post'
    response = HTTParty.post(full_uri, body: request.body, headers: headers, timeout: TIMEOUT)
  when 'put'
    response = HTTParty.put(full_uri, body: request.body, headers: headers, timeout: TIMEOUT)
  when 'delete'
    response = HTTParty.delete(full_uri, headers: headers, timeout: TIMEOUT)
  end

  wire_log.log_response(response, title) if wire_log

  response
end
invoke_unsigned_service(uri, method, params = {}, headers = {}, data = nil, wire_log = nil, title = nil) click to toggle source
# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 121
def invoke_unsigned_service(uri, method, params = {}, headers = {},
                            data = nil, wire_log = nil, title = nil)
  full_uri = uri
  full_uri += '?' unless uri.include? '?'
  full_uri += '&' unless full_uri =~ /[?&]$/
  params.each_pair do |key, _value|
    full_uri << '&' unless key == params.keys.first
    full_uri << "#{URI.encode(key.to_s)}=#{URI.encode(params[key])}"
  end

  write_wirelog_header(wire_log, title, method, uri, headers, params, data, {}) if wire_log

  case method
  when 'get'
    response = HTTParty.get(full_uri, headers: headers, timeout: 120)
  when 'post'
    response = HTTParty.post(full_uri, body: data, headers: headers, timeout: 120)
  when 'put'
    response = HTTParty.put(full_uri, body: data, headers: headers, timeout: 120)
  when 'delete'
    response = HTTParty.delete(full_uri, headers: headers, timeout: 120)
  end

  wire_log.log_response(response, title) if wire_log

  response
end

Private Instance Methods

create_message_body(params, is_include_oauth_params = true) click to toggle source
# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 161
def create_message_body(params, is_include_oauth_params = true)
  result = ''
  params.each_pair do |k, v|
    if is_include_oauth_params || ! (k =~ /^oauth_/)
      result +=
          %Q(      <input type="hidden" name="#{k}" value="#{CGI.escapeHTML(v)}"/>\n)
    end
  end
  result
end
create_message_header(launch_url, is_open_in_external_window = false) click to toggle source
# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 151
    def create_message_header(launch_url, is_open_in_external_window = false)
      attribute_for_external_window = is_open_in_external_window ? 'target="_blank"' : ''
      %Q(
<div id="ltiLaunchFormSubmitArea">
  <form action="#{launch_url}" #{attribute_for_external_window}
    name="ltiLaunchForm" id="ltiLaunchForm" method="post"
    encType="application/x-www-form-urlencoded">
)
    end
set_http_headers(http, request) click to toggle source
# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 189
def set_http_headers(http, request)
  http.headers['Authorization'] = request.oauth_header
  http.headers['Content-Type'] = request.content_type if request.content_type
  http.headers['Content-Length'] = request.body.length if request.body
end
write_wirelog_header(wire_log, title, method, uri, headers = {}, parameters = {}, body = nil, output_parameters = {}) click to toggle source
# File lib/lti2_commons/lib/lti2_commons/message_support.rb, line 195
def write_wirelog_header(wire_log, title, method, uri, headers = {},
                         parameters = {}, body = nil, output_parameters = {})
  wire_log.timestamp
  wire_log.raw_log((title.nil?) ? 'LtiService' : "LtiService: #{title}")
  wire_log.raw_log("#{method.upcase} #{uri}")
  unless headers.blank?
    wire_log.raw_log('Headers:')
    headers.each { |k, v| wire_log.raw_log("#{k}: #{v}") }
  end
  parameters.each { |k, v| output_parameters[k] = v unless k =~ /^oauth_/ }

  if output_parameters.length > 0
    wire_log.raw_log('Parameters:')
    output_parameters.each { |k, v| wire_log.raw_log("#{k}: #{v}") }
  end
  if body
    wire_log.raw_log('Body:')
    wire_log.raw_log(body)
  end
  wire_log.newline
  wire_log.flush
end