coalescing_panda 5.1.12 → 5.2.0
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 +4 -4
- data/app/controllers/coalescing_panda/oauth2_controller.rb +3 -11
- data/app/views/coalescing_panda/oauth2/oauth2.html.haml +3 -1
- data/lib/coalescing_panda/bearcat_uri.rb +20 -18
- data/lib/coalescing_panda/controller_helpers.rb +97 -79
- data/lib/coalescing_panda/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11cc21d4b7941edd42fc5a17f198a55766a46fed5ea37281389f91f71703636e
|
|
4
|
+
data.tar.gz: e4c4173587ad7feabf2478c98816b958cf153360da52b034656caf2e6a2a3d33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bd652d61d8e4968e1b8289c44d8be96c6602c5f60c8e2ef0cc17789d57b67a1b8dfb88fd746c2655d99b0c8c954225853a7883531f4d565d759b2836eea4f5f
|
|
7
|
+
data.tar.gz: 23178183610e5449d6bf5c41d90f02b83f6f71b5f5c2a4c4ff2d7f2028c9063d5873795dc2f7015c86857a401bf1da4c5de38c6d5461a8993b2ac2f443d04639
|
|
@@ -15,10 +15,12 @@ module CoalescingPanda
|
|
|
15
15
|
client_key = lti_account.oauth2_client_key
|
|
16
16
|
user_id = @oauth_state.data[:user_id]
|
|
17
17
|
api_domain = @oauth_state.data[:api_domain]
|
|
18
|
+
prefix = @oauth_state.data[:api_url]
|
|
18
19
|
@oauth_state.destroy
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
Rails.logger.info "Creating Bearcat client for auth token retrieval pointed to: #{prefix}"
|
|
21
22
|
client = Bearcat::Client.new(prefix: prefix)
|
|
23
|
+
|
|
22
24
|
token_body = client.retrieve_token(client_id, coalescing_panda.oauth2_redirect_url, client_key, params['code'])
|
|
23
25
|
auth = CanvasApiAuth.where('user_id = ? and api_domain = ?', user_id, api_domain).first_or_initialize
|
|
24
26
|
auth.api_token = token_body['access_token']
|
|
@@ -30,20 +32,10 @@ module CoalescingPanda
|
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
|
|
34
35
|
private
|
|
35
36
|
|
|
36
|
-
def oauth2_protocol
|
|
37
|
-
ENV['OAUTH_PROTOCOL'] || (Rails.env.development? ? 'http' : 'https')
|
|
38
|
-
end
|
|
39
|
-
|
|
40
37
|
def retrieve_oauth_state
|
|
41
38
|
@oauth_state ||= params[:state].present? && OauthState.find_by(state_key: params[:state])
|
|
42
39
|
end
|
|
43
|
-
|
|
44
|
-
def valid_state_token
|
|
45
|
-
return false unless params['state'].present? && session['state'].present?
|
|
46
|
-
params['state'] == session['state']
|
|
47
|
-
end
|
|
48
40
|
end
|
|
49
41
|
end
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
%h2 Canvas Authentication Required
|
|
4
4
|
%button.btn-canvas#oauth_btn Authenticate
|
|
5
5
|
|
|
6
|
-
%form{id: 'lti_form', action: "#{request.original_url}", method:
|
|
6
|
+
%form{id: 'lti_form', action: "#{request.original_url}", method: request.method}
|
|
7
|
+
- unless @lti_params.include?(:session_token) || @lti_params.include?(:session_key)
|
|
8
|
+
%input{:type =>"hidden", :value=>link_nonce, :name=>"session_token"}
|
|
7
9
|
- @lti_params.each do |k,v|
|
|
8
10
|
%input{:type =>"hidden", :value=>"#{v}", :name=>"#{k}"}
|
|
9
11
|
|
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module CoalescingPanda
|
|
2
|
+
class BearcatUri
|
|
3
|
+
attr_accessor :uri
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
def initialize(uri)
|
|
6
|
+
Rails.logger.info "Parsing Bearcat URI: #{uri}"
|
|
7
|
+
@uri = URI.parse(uri)
|
|
8
|
+
end
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
def api_domain
|
|
11
|
+
if Rails.env.test? or Rails.env.development?
|
|
12
|
+
uri.port.present? ? URI.encode("#{uri.host}:#{uri.port.to_s}") : uri.host
|
|
13
|
+
else
|
|
14
|
+
uri.host
|
|
15
|
+
end
|
|
14
16
|
end
|
|
15
|
-
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
def scheme
|
|
19
|
+
[uri.scheme, '://'].join
|
|
20
|
+
end
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
def prefix
|
|
23
|
+
[scheme, api_domain].join
|
|
24
|
+
end
|
|
23
25
|
end
|
|
24
|
-
end
|
|
26
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'browser'
|
|
2
2
|
require_relative 'session_replacement'
|
|
3
|
+
require_relative 'bearcat_uri'
|
|
3
4
|
|
|
4
5
|
module CoalescingPanda
|
|
5
6
|
module ControllerHelpers
|
|
@@ -14,96 +15,77 @@ module CoalescingPanda
|
|
|
14
15
|
def current_organization; current_lti_account; end
|
|
15
16
|
|
|
16
17
|
def canvas_oauth2(*roles)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', user_id, uri.api_domain)
|
|
26
|
-
if api_auth
|
|
27
|
-
begin
|
|
28
|
-
refresh_token(uri, api_auth) if api_auth.expired?
|
|
29
|
-
@client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix)
|
|
30
|
-
@client.user_profile 'self'
|
|
31
|
-
rescue Footrest::HttpError::BadRequest, Footrest::HttpError::Unauthorized
|
|
32
|
-
# If we can't retrieve our own user profile, or the token refresh fails, something is awry on the canvas end
|
|
33
|
-
# and we'll need to go through the oauth flow again
|
|
34
|
-
render_oauth2_page uri, user_id
|
|
35
|
-
end
|
|
36
|
-
else
|
|
37
|
-
render_oauth2_page uri, user_id
|
|
38
|
-
end
|
|
18
|
+
unless valid_session?
|
|
19
|
+
lti_authorize!(*roles)
|
|
20
|
+
|
|
21
|
+
current_session_data['user_id'] = params['user_id']
|
|
22
|
+
current_session_data['lis_person_sourcedid'] = params['lis_person_sourcedid']
|
|
23
|
+
current_session_data['oauth_consumer_key'] = params['oauth_consumer_key']
|
|
24
|
+
current_session_data['custom_canvas_account_id'] = params['custom_canvas_account_id']
|
|
39
25
|
end
|
|
26
|
+
|
|
27
|
+
@lti_account = LtiAccount.find_by_key(current_session_data['oauth_consumer_key']) if current_session_data['oauth_consumer_key']
|
|
28
|
+
|
|
29
|
+
require_user_api_client
|
|
40
30
|
end
|
|
41
31
|
|
|
42
|
-
def render_oauth2_page
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
def render_oauth2_page
|
|
33
|
+
client_id = current_lti_account.oauth2_client_id
|
|
34
|
+
client = Bearcat::Client.new(prefix: canvas_api_uri)
|
|
45
35
|
|
|
46
|
-
client_id = @lti_account.oauth2_client_id
|
|
47
|
-
client = Bearcat::Client.new(prefix: uri.prefix)
|
|
48
36
|
state = SecureRandom.hex(32)
|
|
49
|
-
OauthState.create! state_key: state, data: {
|
|
37
|
+
OauthState.create! state_key: state, data: {
|
|
38
|
+
key: current_session_data.dig(:launch_params, :oauth_consumer_key),
|
|
39
|
+
user_id: current_session_data.dig(:launch_params, :user_id),
|
|
40
|
+
api_domain: URI.parse(canvas_api_uri).host,
|
|
41
|
+
api_url: canvas_api_uri,
|
|
42
|
+
}
|
|
43
|
+
|
|
50
44
|
@canvas_url = client.auth_redirect_url(client_id, resolve_coalescing_panda_url(:oauth2_redirect_url), { state: state })
|
|
51
45
|
|
|
52
|
-
#delete the added params so the original oauth sig still works
|
|
53
46
|
@lti_params = params.to_unsafe_h
|
|
54
47
|
@lti_params.delete('action')
|
|
55
48
|
@lti_params.delete('controller')
|
|
49
|
+
|
|
56
50
|
render 'coalescing_panda/oauth2/oauth2', layout: 'coalescing_panda/application'
|
|
57
51
|
end
|
|
58
52
|
|
|
59
|
-
def refresh_token(
|
|
60
|
-
refresh_client = Bearcat::Client.new(prefix:
|
|
61
|
-
refresh_body = refresh_client.retrieve_token(
|
|
62
|
-
|
|
53
|
+
def refresh_token(api_auth)
|
|
54
|
+
refresh_client = Bearcat::Client.new(prefix: canvas_api_uri)
|
|
55
|
+
refresh_body = refresh_client.retrieve_token(
|
|
56
|
+
current_lti_account.oauth2_client_id,
|
|
57
|
+
resolve_coalescing_panda_url(:oauth2_redirect_url),
|
|
58
|
+
current_lti_account.oauth2_client_key,
|
|
59
|
+
api_auth.refresh_token,
|
|
60
|
+
'refresh_token'
|
|
61
|
+
)
|
|
63
62
|
api_auth.update({ api_token: refresh_body['access_token'], expires_at: (Time.now + refresh_body['expires_in']) })
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
api_auth = CanvasApiAuth.find_by(user_id: current_session_data['user_id'], api_domain: uri.api_domain)
|
|
70
|
-
@lti_account = LtiAccount.find_by(key: current_session_data['oauth_consumer_key'])
|
|
71
|
-
return if @lti_account.nil? || api_auth.nil? # Not all tools use oauth
|
|
72
|
-
|
|
73
|
-
refresh_token(uri, api_auth) if api_auth.expired?
|
|
74
|
-
rescue Footrest::HttpError::BadRequest
|
|
75
|
-
render_oauth2_page uri, current_session_data['user_id']
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def set_session(launch_presentation_return_url)
|
|
79
|
-
current_session_data['user_id'] = params['user_id']
|
|
80
|
-
current_session_data['uri'] = launch_presentation_return_url
|
|
81
|
-
current_session_data['lis_person_sourcedid'] = params['lis_person_sourcedid']
|
|
82
|
-
current_session_data['oauth_consumer_key'] = params['oauth_consumer_key']
|
|
83
|
-
current_session_data['custom_canvas_account_id'] = params['custom_canvas_account_id']
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def have_session?
|
|
87
|
-
if params['tool_consumer_instance_guid'] && current_session_data['user_id'] != params['user_id']
|
|
88
|
-
reset_session
|
|
89
|
-
logger.info("resetting session params")
|
|
90
|
-
current_session_data['user_id'] = params['user_id']
|
|
65
|
+
def require_user_api_client
|
|
66
|
+
if user_api_client.nil?
|
|
67
|
+
render_oauth2_page
|
|
91
68
|
end
|
|
69
|
+
end
|
|
92
70
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
71
|
+
def user_api_client
|
|
72
|
+
@user_api_client ||= begin
|
|
73
|
+
user_id = current_session_data.dig(:launch_params, :user_id)
|
|
74
|
+
puri = URI.parse(canvas_api_uri)
|
|
75
|
+
api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', user_id, puri.host)
|
|
76
|
+
if api_auth
|
|
77
|
+
begin
|
|
78
|
+
refresh_token(api_auth) if api_auth.expired?
|
|
79
|
+
client = Bearcat::Client.new(token: api_auth.api_token, prefix: canvas_api_uri)
|
|
80
|
+
client.user_profile('self')
|
|
81
|
+
client
|
|
82
|
+
rescue Footrest::HttpError::BadRequest, Footrest::HttpError::Unauthorized
|
|
83
|
+
# If we can't retrieve our own user profile, or the token refresh fails, something is awry on the canvas end
|
|
84
|
+
# and we'll need to go through the oauth flow again
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
99
87
|
end
|
|
100
88
|
end
|
|
101
|
-
|
|
102
|
-
@lti_account = LtiAccount.find_by_key(current_session_data['oauth_consumer_key']) if current_session_data['oauth_consumer_key']
|
|
103
|
-
|
|
104
|
-
!!@client
|
|
105
|
-
rescue Footrest::HttpError::Unauthorized
|
|
106
|
-
false
|
|
107
89
|
end
|
|
108
90
|
|
|
109
91
|
def validate_launch!
|
|
@@ -111,23 +93,43 @@ module CoalescingPanda
|
|
|
111
93
|
end
|
|
112
94
|
|
|
113
95
|
def lti_authorize!(*roles)
|
|
96
|
+
if valid_session? # This means that we are returning from an OAuth dance.
|
|
97
|
+
# Set the params as they were at launch to avoid any bait-and-switch attack vulnerabilities in the App's launch controller
|
|
98
|
+
params.merge!(current_session_data[:launch_params])
|
|
99
|
+
return true
|
|
100
|
+
end
|
|
101
|
+
|
|
114
102
|
authorized = false
|
|
115
103
|
if (@lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key']))
|
|
116
104
|
sanitized_params = sanitize_params
|
|
117
105
|
@tp = IMS::LTI::ToolProvider.new(@lti_account.key, @lti_account.secret, sanitized_params)
|
|
118
106
|
authorized = @tp.valid_request?(request)
|
|
119
107
|
end
|
|
108
|
+
|
|
120
109
|
logger.info 'not authorized on tp valid request' unless authorized
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
|
|
111
|
+
if authorized && !(authorized = (roles.count == 0 || (roles & lti_roles).count > 0))
|
|
112
|
+
logger.info 'not authorized on roles'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if authorized && !(authorized = @lti_account.validate_nonce(params['oauth_nonce'], DateTime.strptime(params['oauth_timestamp'], '%s')))
|
|
116
|
+
logger.info 'not authorized on nonce'
|
|
117
|
+
end
|
|
118
|
+
|
|
125
119
|
render :text => 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
|
|
120
|
+
|
|
126
121
|
# create session on first launch
|
|
127
122
|
current_session
|
|
128
123
|
authorized
|
|
129
124
|
end
|
|
130
125
|
|
|
126
|
+
def valid_session?
|
|
127
|
+
return false unless current_session(create_missing: false)&.persisted?
|
|
128
|
+
true
|
|
129
|
+
rescue SessionNonceMismatch
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
131
133
|
# code for method taken from panda_pal v 4.0.8
|
|
132
134
|
# used for safari workaround
|
|
133
135
|
def sanitize_params
|
|
@@ -161,11 +163,27 @@ module CoalescingPanda
|
|
|
161
163
|
end
|
|
162
164
|
end
|
|
163
165
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
def canvas_api_uri
|
|
167
|
+
@canvas_api_uri ||= begin
|
|
168
|
+
launch_params = current_session_data[:launch_params] || {}
|
|
169
|
+
uri = 'canvas.instructure.com'
|
|
170
|
+
if launch_params[:custom_canvas_api_domain].present?
|
|
171
|
+
uri = launch_params[:custom_canvas_api_domain]
|
|
172
|
+
else
|
|
173
|
+
uri = current_lti_account.settings[:launch_presentation_return_url] || launch_params[:launch_presentation_return_url]
|
|
174
|
+
unless uri.include?('://')
|
|
175
|
+
referrer = URI.parse(request.env["HTTP_REFERER"])
|
|
176
|
+
referrer.path = ''
|
|
177
|
+
uri = [referrer.to_s, uri].join
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
uri = ((Rails.env.test? or Rails.env.development?) ? 'http' : 'https') + '://' + uri unless uri.include?('://')
|
|
182
|
+
|
|
183
|
+
uri = URI.parse(uri)
|
|
184
|
+
uri.path = ''
|
|
185
|
+
uri.to_s
|
|
186
|
+
end
|
|
169
187
|
end
|
|
170
188
|
|
|
171
189
|
private
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: coalescing_panda
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Mills
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2021-
|
|
13
|
+
date: 2021-11-22 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: rails
|