lti2 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +184 -0
- data/Rakefile +23 -0
- data/app/assets/javascripts/lti2/application.js +13 -0
- data/app/assets/stylesheets/lti2/application.css +15 -0
- data/app/controllers/lti2/application_controller.rb +4 -0
- data/app/helpers/lti2/application_helper.rb +4 -0
- data/app/views/layouts/lti2/application.html.erb +14 -0
- data/config/routes.rb +4 -0
- data/lib/lti2.rb +7 -0
- data/lib/lti2/engine.rb +9 -0
- data/lib/lti2/version.rb +3 -0
- data/lib/lti2_commons/lib/lti2_commons.rb +9 -0
- data/lib/lti2_commons/lib/lti2_commons/cache.rb +29 -0
- data/lib/lti2_commons/lib/lti2_commons/json_wrapper.rb +118 -0
- data/lib/lti2_commons/lib/lti2_commons/lti2_launch.rb +158 -0
- data/lib/lti2_commons/lib/lti2_commons/message_support.rb +218 -0
- data/lib/lti2_commons/lib/lti2_commons/oauth_request.rb +179 -0
- data/lib/lti2_commons/lib/lti2_commons/signer.rb +75 -0
- data/lib/lti2_commons/lib/lti2_commons/substitution_support.rb +87 -0
- data/lib/lti2_commons/lib/lti2_commons/utils.rb +28 -0
- data/lib/lti2_commons/lib/lti2_commons/version.rb +3 -0
- data/lib/lti2_commons/lib/lti2_commons/wire_log.rb +163 -0
- data/lib/lti2_commons/test/test_jsonpath.rb +255 -0
- data/lib/lti2_commons/test/test_jsonwrapper.rb +230 -0
- data/lib/lti2_commons/test/test_oauth_request.rb +143 -0
- data/lib/lti2_commons/test/test_substitution_support.rb +71 -0
- data/lib/lti2_commons/test/test_wire_log.rb +15 -0
- data/lib/tasks/lti2_tasks.rake +4 -0
- metadata +199 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'lti2_commons/signer'
|
2
|
+
require 'lti2_commons/message_support'
|
3
|
+
require 'lti2_commons/substitution_support'
|
4
|
+
|
5
|
+
include Lti2Commons::Signer
|
6
|
+
include Lti2Commons::MessageSupport
|
7
|
+
include Lti2Commons::SubstitutionSupport
|
8
|
+
|
9
|
+
module Lti2Commons
|
10
|
+
module Core
|
11
|
+
def lti2_launch(user, link, return_url)
|
12
|
+
tool_proxy = link.resource.tool.get_tool_proxy
|
13
|
+
|
14
|
+
tool = link.resource.tool
|
15
|
+
tool_name = tool_proxy.first_at('tool_profile.product_instance.product_info.product_name.default_value')
|
16
|
+
fail "Tool #{tool_name} is currently disabled" unless tool.is_enabled
|
17
|
+
|
18
|
+
base_url = tool_proxy.select('tool_profile.base_url_choice', 'selector.applies_to',
|
19
|
+
'MessageHandler', 'default_base_url')
|
20
|
+
resource_type = link.resource.resource_type
|
21
|
+
resource_handler_node = tool_proxy.search('tool_profile.resource_handler', { 'resource_type' => resource_type }, '@')
|
22
|
+
resource_handler = JsonWrapper.new(resource_handler_node)
|
23
|
+
message = resource_handler.search('@..message', { 'message_type' => 'basic-lti-launch-request' }, '@')
|
24
|
+
path = message['path']
|
25
|
+
tp_parameters = message['parameter']
|
26
|
+
service_endpoint = base_url + path
|
27
|
+
|
28
|
+
enrollment = Enrollment.where(admin_user_id: user.id, course_id: link.course.id).first
|
29
|
+
if enrollment
|
30
|
+
role = enrollment.role
|
31
|
+
else
|
32
|
+
role = user.role
|
33
|
+
end
|
34
|
+
|
35
|
+
tool_consumer_registry = Rails.application.config.tool_consumer_registry
|
36
|
+
parameters = {
|
37
|
+
'lti_version' => tool_proxy.first_at('lti_version'),
|
38
|
+
'lti_message_type' => 'basic-lti-launch-request',
|
39
|
+
'resource_link_id' => link.id.to_s,
|
40
|
+
'user_id' => user.id.to_s,
|
41
|
+
'roles' => role,
|
42
|
+
'launch_presentation_return_url' => "#{tool_consumer_registry.tc_deployment_url}#{return_url}",
|
43
|
+
|
44
|
+
# optional
|
45
|
+
'context_id' => link.course.id.to_s
|
46
|
+
}
|
47
|
+
|
48
|
+
# add parameters from ToolProxy
|
49
|
+
tp_parameters.each do |parameter|
|
50
|
+
name = parameter['name']
|
51
|
+
if parameter.key? 'fixed'
|
52
|
+
value = parameter['fixed']
|
53
|
+
elsif parameter.key? 'variable'
|
54
|
+
value = parameter['variable']
|
55
|
+
else
|
56
|
+
value = '[link-to-resolve]'
|
57
|
+
end
|
58
|
+
parameters[name] = value
|
59
|
+
end
|
60
|
+
|
61
|
+
# add parameters from Link
|
62
|
+
link_parameter_json = link.link_parameters
|
63
|
+
if link_parameter_json
|
64
|
+
link_parameters = JSON.parse(link_parameter_json)
|
65
|
+
parameters.merge! link_parameters
|
66
|
+
end
|
67
|
+
|
68
|
+
# auto-create result (if required) and add to reference to parameters
|
69
|
+
capabilities = resource_handler.first_at('message[0].enabled_capability')
|
70
|
+
if capabilities && capabilities.include?('Result.autocreate')
|
71
|
+
if link.grade_item_id
|
72
|
+
grade_result = GradeResult.where(link_id: link.id, admin_user_id: user.id).first
|
73
|
+
# TEST ONLY
|
74
|
+
# grade_result.delete if grade_result
|
75
|
+
# grade_result = nil
|
76
|
+
|
77
|
+
if grade_result
|
78
|
+
# note that a nil grade_result still let's us through
|
79
|
+
unless grade_result.result.nil?
|
80
|
+
fail 'Grade result associated with this tool launch has already been filled in'
|
81
|
+
end
|
82
|
+
else
|
83
|
+
grade_result = GradeResult.new
|
84
|
+
grade_result.link_id = link.id
|
85
|
+
grade_result.admin_user_id = user.id
|
86
|
+
grade_result.save
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# perform variable substition on all parameters
|
92
|
+
resolver = Resolver.new
|
93
|
+
resolver.add_resolver('$User', user.method(:user_resolver))
|
94
|
+
resolver.add_resolver('$Person', user.method(:person_resolver))
|
95
|
+
course = Course.find(parameters['context_id'])
|
96
|
+
resolver.add_resolver('$CourseOffering', course.method(:course_resolver))
|
97
|
+
resolver.add_resolver('$Result', grade_result.method(:grade_result_resolver)) if grade_result
|
98
|
+
|
99
|
+
# and resolve and normalize
|
100
|
+
final_parameters = {}
|
101
|
+
parameters.each do |k, v|
|
102
|
+
# only apply substitution when the value looks like a candidate
|
103
|
+
if v =~ /^\$\w+\.\w/
|
104
|
+
resolved_value = resolver.resolve(v)
|
105
|
+
else
|
106
|
+
resolved_value = v
|
107
|
+
end
|
108
|
+
if known_lti2_parameters.include?(k) || deprecated_lti_parameters.include?(k)
|
109
|
+
final_parameters[k] = v
|
110
|
+
else
|
111
|
+
name = 'custom_' + k
|
112
|
+
lti1_name = slugify(name)
|
113
|
+
final_parameters[lti1_name] = resolved_value unless name == lti1_name
|
114
|
+
final_parameters[name] = resolved_value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
key = link.resource.tool.key
|
119
|
+
secret = link.resource.tool.secret
|
120
|
+
|
121
|
+
signed_request = Signer.create_signed_request(service_endpoint, 'post', key, secret, final_parameters)
|
122
|
+
puts "TC Signed Request: #{signed_request.signature_base_string}"
|
123
|
+
puts 'before'
|
124
|
+
puts Rails.application.config.wire_log.inspect
|
125
|
+
puts 'after'
|
126
|
+
body = MessageSupport.create_lti_message_body(
|
127
|
+
service_endpoint, final_parameters, Rails.application.config.wire_log, 'Lti Launch')
|
128
|
+
puts body
|
129
|
+
body
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def deprecated_lti_parameters
|
135
|
+
['context_title', 'context_label', 'resource_link_title', 'resource_link_description',
|
136
|
+
'lis_person_name_given', 'lis_person_name_family', 'lis_person_name_full', 'lis_person_contact_email_primary',
|
137
|
+
'user_image', 'lis_person_sourcedid', 'lis_course_offering_sourcedid', 'lis_course_section_sourcedid',
|
138
|
+
'tool_consumer_instance_name', 'tool_consumer_instance_description', 'tool_consumer_instance_url',
|
139
|
+
'tool_consumer_instance_contact_email']
|
140
|
+
end
|
141
|
+
|
142
|
+
def known_lti2_parameters
|
143
|
+
['lti_message_type', 'lti_version', 'user_id', 'roles', 'resource_link_id',
|
144
|
+
'context_id', 'context_type',
|
145
|
+
'launch_presentation_locale', 'launch_presentation_document_target', 'launch_presentation_css_url',
|
146
|
+
'launch_presentation_width', 'launch_presentation_height', 'launch_presentation_return_url',
|
147
|
+
'reg_key', 'reg_password', 'tc_profile_url']
|
148
|
+
end
|
149
|
+
|
150
|
+
def slugify(name)
|
151
|
+
result = ''
|
152
|
+
name.each_char do |c|
|
153
|
+
result << (c =~ /\w/ ? c.downcase : '_')
|
154
|
+
end
|
155
|
+
result
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module Lti2Commons
|
4
|
+
# Module to support LTI 1 and 2 secure messaging.
|
5
|
+
# This messaging is documented in the LTI 2 Security Document
|
6
|
+
#
|
7
|
+
# LTI 2 defines two types of LTI secure messaging:
|
8
|
+
# 1. LTI Messages
|
9
|
+
# This is the model used exclusively in LTI 1.
|
10
|
+
# It is also used in LTI 2 for user-submitted actions such as LtiLaunch and ToolDeployment.
|
11
|
+
#
|
12
|
+
# It works the following way:
|
13
|
+
# 1. LTI parameters are signed by OAuth.
|
14
|
+
# 2. The message is marshalled into an HTML Form with the params
|
15
|
+
# specified in form fields.
|
16
|
+
# 3. The form is sent to the browser with a redirect.
|
17
|
+
# 4. An attached Javascript script 'auto-submits' the form.
|
18
|
+
#
|
19
|
+
# This structure appears to the Tool Provider as a user submission with all user browser context
|
20
|
+
# intact.
|
21
|
+
#
|
22
|
+
# 2. LTI Services
|
23
|
+
# This is a standard REST web service with OAuth message security added.
|
24
|
+
# In LTI 2.0 Services are only defined for Tool Provider --> Tool Consumer services;
|
25
|
+
# such as, GetToolConsumerProfile, RegisterToolProxy, and LTI 2 Outcomes.
|
26
|
+
# LTI 2.x will add Tool Consumer --> Tool Provider services using the same machinery.
|
27
|
+
|
28
|
+
module MessageSupport
|
29
|
+
TIMEOUT = 300
|
30
|
+
|
31
|
+
# Convenience method signs and then invokes create_lti_message_from_signed_request
|
32
|
+
#
|
33
|
+
# @params launch_url [String] Launch url
|
34
|
+
# @params parameters [Hash] Full set of params for message
|
35
|
+
# @return [String] Post body ready for use
|
36
|
+
#
|
37
|
+
def create_lti_message_body(launch_url, parameters, wire_log = nil, title = nil, is_open_in_external_window = false)
|
38
|
+
result = create_message_header(launch_url, is_open_in_external_window)
|
39
|
+
result += create_message_body(parameters)
|
40
|
+
result += create_message_footer(is_open_in_external_window)
|
41
|
+
|
42
|
+
if wire_log
|
43
|
+
wire_log.timestamp
|
44
|
+
wire_log.raw_log((title.nil?) ? 'LtiMessage' : "LtiMessage: #{title}")
|
45
|
+
wire_log.raw_log "LaunchUrl: #{launch_url}"
|
46
|
+
wire_log.raw_log result.strip
|
47
|
+
wire_log.newline
|
48
|
+
wire_log.flush
|
49
|
+
end
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates an LTI Message (POST body) ready for redirecting to the launch_url.
|
55
|
+
# Note that the is_include_oauth_params option specifies that 'oauth_' params are
|
56
|
+
# included in the body. This option should be false when sender is putting them in the
|
57
|
+
# HTTP Authorization header (now recommended).
|
58
|
+
#
|
59
|
+
# @param params [Hash] Full set of params for message (including OAuth provided params)
|
60
|
+
# @return [String] Post body ready for use
|
61
|
+
#
|
62
|
+
def create_lti_message_body_from_signed_request(signed_request, is_include_oauth_params = true,
|
63
|
+
is_open_in_external_window = false)
|
64
|
+
result = create_message_header(signed_request.uri, is_open_in_external_window)
|
65
|
+
result += create_message_body(signed_request.parameters, is_include_oauth_params)
|
66
|
+
result += create_message_footer(is_open_in_external_window)
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
# Invokes an LTI Service.
|
71
|
+
# This is fully-compliant REST request suitable for LTI server-to-server services.
|
72
|
+
#
|
73
|
+
# @param request [Request] Signed Request encapsulates everything needed for service.
|
74
|
+
def invoke_service(request, wire_log = nil, title = nil, other_headers = {})
|
75
|
+
uri = request.uri.to_s
|
76
|
+
# set_headers_proc = lambda { |http|
|
77
|
+
# http.headers['Authorization'] = request.oauth_header
|
78
|
+
# http.headers['Content-Type'] = request.content_type if request.content_type
|
79
|
+
# http.headers['Accept'] = request.content_type if request.content_type
|
80
|
+
# # http.headers['Content-Length'] = request.body.length if request.body
|
81
|
+
# }
|
82
|
+
method = request.method.downcase
|
83
|
+
|
84
|
+
headers = {}
|
85
|
+
headers['Authorization'] = request.oauth_header
|
86
|
+
headers['Content-Type'] = request.content_type if request.content_type
|
87
|
+
headers['Accept'] = request.accept if request.accept
|
88
|
+
headers['Content-Length'] = request.body.length.to_s if request.body
|
89
|
+
headers.merge!(other_headers)
|
90
|
+
|
91
|
+
parameters = request.parameters
|
92
|
+
output_parameters = {}
|
93
|
+
parameters.each { |k, v| output_parameters[k] = v unless k =~ /^oauth_/ }
|
94
|
+
|
95
|
+
(write_wirelog_header wire_log, title, request.method, uri, headers, parameters, request.body, output_parameters) if wire_log
|
96
|
+
|
97
|
+
full_uri = uri
|
98
|
+
full_uri += '?' unless uri.include? '?'
|
99
|
+
full_uri += '&' unless full_uri =~ /[?&]$/
|
100
|
+
output_parameters.each_pair do |key, _value|
|
101
|
+
full_uri << '&' unless key == output_parameters.keys.first
|
102
|
+
full_uri << "#{URI.encode(key.to_s)}=#{URI.encode(output_parameters[key] || '')}"
|
103
|
+
end
|
104
|
+
|
105
|
+
case method
|
106
|
+
when 'get'
|
107
|
+
response = HTTParty.get(full_uri, headers: headers, timeout: TIMEOUT)
|
108
|
+
when 'post'
|
109
|
+
response = HTTParty.post(full_uri, body: request.body, headers: headers, timeout: TIMEOUT)
|
110
|
+
when 'put'
|
111
|
+
response = HTTParty.put(full_uri, body: request.body, headers: headers, timeout: TIMEOUT)
|
112
|
+
when 'delete'
|
113
|
+
response = HTTParty.delete(full_uri, headers: headers, timeout: TIMEOUT)
|
114
|
+
end
|
115
|
+
|
116
|
+
wire_log.log_response(response, title) if wire_log
|
117
|
+
|
118
|
+
response
|
119
|
+
end
|
120
|
+
|
121
|
+
def invoke_unsigned_service(uri, method, params = {}, headers = {},
|
122
|
+
data = nil, wire_log = nil, title = nil)
|
123
|
+
full_uri = uri
|
124
|
+
full_uri += '?' unless uri.include? '?'
|
125
|
+
full_uri += '&' unless full_uri =~ /[?&]$/
|
126
|
+
params.each_pair do |key, _value|
|
127
|
+
full_uri << '&' unless key == params.keys.first
|
128
|
+
full_uri << "#{URI.encode(key.to_s)}=#{URI.encode(params[key])}"
|
129
|
+
end
|
130
|
+
|
131
|
+
write_wirelog_header(wire_log, title, method, uri, headers, params, data, {}) if wire_log
|
132
|
+
|
133
|
+
case method
|
134
|
+
when 'get'
|
135
|
+
response = HTTParty.get(full_uri, headers: headers, timeout: 120)
|
136
|
+
when 'post'
|
137
|
+
response = HTTParty.post(full_uri, body: data, headers: headers, timeout: 120)
|
138
|
+
when 'put'
|
139
|
+
response = HTTParty.put(full_uri, body: data, headers: headers, timeout: 120)
|
140
|
+
when 'delete'
|
141
|
+
response = HTTParty.delete(full_uri, headers: headers, timeout: 120)
|
142
|
+
end
|
143
|
+
|
144
|
+
wire_log.log_response(response, title) if wire_log
|
145
|
+
|
146
|
+
response
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def create_message_header(launch_url, is_open_in_external_window = false)
|
152
|
+
attribute_for_external_window = is_open_in_external_window ? 'target="_blank"' : ''
|
153
|
+
%Q(
|
154
|
+
<div id="ltiLaunchFormSubmitArea">
|
155
|
+
<form action="#{launch_url}" #{attribute_for_external_window}
|
156
|
+
name="ltiLaunchForm" id="ltiLaunchForm" method="post"
|
157
|
+
encType="application/x-www-form-urlencoded">
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_message_body(params, is_include_oauth_params = true)
|
162
|
+
result = ''
|
163
|
+
params.each_pair do |k, v|
|
164
|
+
if is_include_oauth_params || ! (k =~ /^oauth_/)
|
165
|
+
result +=
|
166
|
+
%Q( <input type="hidden" name="#{k}" value="#{CGI.escapeHTML(v)}"/>\n)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
result
|
170
|
+
end
|
171
|
+
|
172
|
+
def create_message_footer(is_open_in_external_window=false)
|
173
|
+
footer = ""
|
174
|
+
if is_open_in_external_window
|
175
|
+
footer += %Q{
|
176
|
+
<a href="/admin/launches" target="_self">Return to Admin</a>\n
|
177
|
+
}
|
178
|
+
end
|
179
|
+
footer += %Q{
|
180
|
+
</form>
|
181
|
+
</div>
|
182
|
+
<script language="javascript">
|
183
|
+
document.ltiLaunchForm.submit();
|
184
|
+
</script>
|
185
|
+
}
|
186
|
+
footer
|
187
|
+
end
|
188
|
+
|
189
|
+
def set_http_headers(http, request)
|
190
|
+
http.headers['Authorization'] = request.oauth_header
|
191
|
+
http.headers['Content-Type'] = request.content_type if request.content_type
|
192
|
+
http.headers['Content-Length'] = request.body.length if request.body
|
193
|
+
end
|
194
|
+
|
195
|
+
def write_wirelog_header(wire_log, title, method, uri, headers = {},
|
196
|
+
parameters = {}, body = nil, output_parameters = {})
|
197
|
+
wire_log.timestamp
|
198
|
+
wire_log.raw_log((title.nil?) ? 'LtiService' : "LtiService: #{title}")
|
199
|
+
wire_log.raw_log("#{method.upcase} #{uri}")
|
200
|
+
unless headers.blank?
|
201
|
+
wire_log.raw_log('Headers:')
|
202
|
+
headers.each { |k, v| wire_log.raw_log("#{k}: #{v}") }
|
203
|
+
end
|
204
|
+
parameters.each { |k, v| output_parameters[k] = v unless k =~ /^oauth_/ }
|
205
|
+
|
206
|
+
if output_parameters.length > 0
|
207
|
+
wire_log.raw_log('Parameters:')
|
208
|
+
output_parameters.each { |k, v| wire_log.raw_log("#{k}: #{v}") }
|
209
|
+
end
|
210
|
+
if body
|
211
|
+
wire_log.raw_log('Body:')
|
212
|
+
wire_log.raw_log(body)
|
213
|
+
end
|
214
|
+
wire_log.newline
|
215
|
+
wire_log.flush
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'oauth/request_proxy/base'
|
2
|
+
|
3
|
+
module OAuth
|
4
|
+
module OAuthProxy
|
5
|
+
# RequestProxy for Hashes to facilitate simpler signature creation.
|
6
|
+
# Usage:
|
7
|
+
# request = OAuth::RequestProxy.proxy \
|
8
|
+
# "method" => "iq",
|
9
|
+
# "uri" => [from, to] * "&",
|
10
|
+
# "parameters" => {
|
11
|
+
# "oauth_consumer_key" => oauth_consumer_key,
|
12
|
+
# "oauth_token" => oauth_token,
|
13
|
+
# "oauth_signature_method" => "HMAC-SHA1"
|
14
|
+
# }
|
15
|
+
#
|
16
|
+
# signature = OAuth::Signature.sign \
|
17
|
+
# request,
|
18
|
+
# consumer_secret: oauth_consumer_secret,
|
19
|
+
# token_secret: oauth_token_secret,
|
20
|
+
|
21
|
+
# allow a certain amount of clock skew between servers
|
22
|
+
CLOCK_SKEW_ALLOWANCE_IN_SECS = 300
|
23
|
+
|
24
|
+
# nonce must be unique for some period of time
|
25
|
+
NONCE_REPLAY_UNIQUE_WITHIN_SECS = CLOCK_SKEW_ALLOWANCE_IN_SECS
|
26
|
+
|
27
|
+
class OAuthRequest < OAuth::RequestProxy::Base
|
28
|
+
proxies Hash
|
29
|
+
|
30
|
+
attr_accessor :body, :content_type, :accept
|
31
|
+
|
32
|
+
def self.collect_rack_parameters(rack_request)
|
33
|
+
parameters = HashWithIndifferentAccess.new
|
34
|
+
parameters.merge!(rack_request.query_parameters)
|
35
|
+
parameters.merge!(self.parse_authorization_header(rack_request.headers['HTTP_AUTHORIZATION']))
|
36
|
+
@content_type = rack_request.headers['CONTENT_TYPE']
|
37
|
+
@accept = rack_request.headers['ACCEPT']
|
38
|
+
if @content_type == 'application/x-www-form-urlencoded'
|
39
|
+
parameters.merge!(rack_request.request_parameters)
|
40
|
+
end
|
41
|
+
parameters
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.create_from_rack_request(rack_request)
|
45
|
+
parameters = self.collect_rack_parameters(rack_request)
|
46
|
+
result = OAuth::OAuthProxy::OAuthRequest.new(
|
47
|
+
'method' => rack_request.method,
|
48
|
+
'uri' => rack_request.url,
|
49
|
+
'parameters' => parameters
|
50
|
+
)
|
51
|
+
rack_request.body.rewind
|
52
|
+
result.body = rack_request.body.read
|
53
|
+
rack_request.body.rewind
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.parse_authorization_header(authorization_header)
|
58
|
+
result = {}
|
59
|
+
if authorization_header =~ /^OAuth/
|
60
|
+
authorization_header[6..-1].split(',').inject({}) do |_h, part|
|
61
|
+
parts = part.split('=')
|
62
|
+
name = parts[0].strip.intern
|
63
|
+
value = parts[1..-1].join('=').strip
|
64
|
+
value.gsub!(/\A['"]+|['"]+\Z/, '')
|
65
|
+
result[name] = Rack::Utils.unescape(value) unless name == :realm
|
66
|
+
end
|
67
|
+
end
|
68
|
+
Rails.logger.info "AuthHdr_Parms: #{result.inspect}"
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
def final_uri
|
73
|
+
@request['final_uri']
|
74
|
+
end
|
75
|
+
|
76
|
+
def log(msg)
|
77
|
+
Rails.logger.info(msg)
|
78
|
+
end
|
79
|
+
|
80
|
+
def parameters
|
81
|
+
@request['parameters']
|
82
|
+
end
|
83
|
+
|
84
|
+
def method
|
85
|
+
@request['method']
|
86
|
+
end
|
87
|
+
|
88
|
+
def normalized_uri
|
89
|
+
super
|
90
|
+
rescue
|
91
|
+
# if this is a non-standard URI, it may not parse properly
|
92
|
+
# in that case, assume that it's already been normalized
|
93
|
+
uri
|
94
|
+
end
|
95
|
+
|
96
|
+
def uri
|
97
|
+
@request['uri']
|
98
|
+
end
|
99
|
+
|
100
|
+
# Creates the value of an OAuth body hash
|
101
|
+
#
|
102
|
+
# @param launch_url [String] Content to be body signed
|
103
|
+
# @return [String] Signature base string (useful for debugging signature problems)
|
104
|
+
#
|
105
|
+
def compute_oauth_body_hash(content)
|
106
|
+
Base64.encode64(Digest::SHA1.digest(content.chomp)).gsub(/\n/, '')
|
107
|
+
end
|
108
|
+
|
109
|
+
# A shallow+1 copy
|
110
|
+
#
|
111
|
+
def copy
|
112
|
+
result = OAuth::OAuthProxy::OAuthRequest.new(
|
113
|
+
'method' => self.method.dup,
|
114
|
+
'uri' => self.uri.dup,
|
115
|
+
'parameters' => self.parameters.dup
|
116
|
+
)
|
117
|
+
result.body = self.body.dup if self.body
|
118
|
+
result
|
119
|
+
end
|
120
|
+
|
121
|
+
def is_timestamp_expired?(timestampString)
|
122
|
+
timestamp = Time.at(timestampString.to_i)
|
123
|
+
now = Time.now
|
124
|
+
(now - timestamp).abs > CLOCK_SKEW_ALLOWANCE_IN_SECS
|
125
|
+
end
|
126
|
+
|
127
|
+
# Validates an OAuth request using the OAuth Gem - https://github.com/oauth/oauth-ruby
|
128
|
+
#
|
129
|
+
# @return [Bool] Whether the request was valid
|
130
|
+
def verify_signature?(secret, nonce_cache, is_handle_error_not_raise_exception = true, ignore_timestamp_and_nonce = false)
|
131
|
+
log 'in verify_signature'
|
132
|
+
test_request = self.copy
|
133
|
+
test_signature = test_request.sign(consumer_secret: secret)
|
134
|
+
# log "DEBUG: signed"
|
135
|
+
begin
|
136
|
+
unless self.oauth_signature == test_signature
|
137
|
+
log "Secret: #{secret}"
|
138
|
+
log "Verify_signature--send_signature: #{self.oauth_signature} test_signature: #{test_signature}"
|
139
|
+
log "Verify signature_base_string: #{self.signature_base_string}"
|
140
|
+
fail 'Invalid signature'
|
141
|
+
end
|
142
|
+
unless ignore_timestamp_and_nonce
|
143
|
+
fail 'Timestamp expired' if is_timestamp_expired? self.oauth_timestamp
|
144
|
+
fail 'Duplicate nonce to one already received' if nonce_cache.fetch(self.oauth_nonce)
|
145
|
+
end
|
146
|
+
nonce_cache.store(self.oauth_nonce, '<who-cares>')
|
147
|
+
|
148
|
+
# check body-signing if oauth_body_signature
|
149
|
+
if self.body && self.parameters.key?('oauth_body_hash')
|
150
|
+
fail 'Invalid signature of message body' unless compute_oauth_body_hash(self.body) == self.parameters['oauth_body_hash']
|
151
|
+
end
|
152
|
+
[true, test_request.signature_base_string]
|
153
|
+
rescue Exception => e
|
154
|
+
log(e.message)
|
155
|
+
if is_handle_error_not_raise_exception
|
156
|
+
[false, test_request.signature_base_string]
|
157
|
+
else
|
158
|
+
raise e.message
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Runs validation logic but always returns true
|
164
|
+
#
|
165
|
+
# @return [Bool] Whether the request was valid
|
166
|
+
#
|
167
|
+
def verify_signature_always?(secret, nonce_cache, is_handle_error_not_raise_exception = true,
|
168
|
+
ignore_timestamp_and_nonce = false)
|
169
|
+
test_request = self.copy
|
170
|
+
test_signature = test_request.sign(consumer_secret: secret)
|
171
|
+
log "TC Signature: #{test_signature}"
|
172
|
+
log "TP Signature: #{self.oauth_signature}"
|
173
|
+
log "Signature_Base_String: #{test_request.signature_base_string}"
|
174
|
+
# log "Authorization_Header: #{request.headers['Authorization']}"
|
175
|
+
[self.oauth_signature == test_signature, test_request.signature_base_string]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|