lti2 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|