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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45ffb1d5dba84eb776180589d036732efebe48d87472dfd8fbf620b7f38c1747
4
- data.tar.gz: 3f558eb4cb87853533369fa838bd92546060f5f6235c17655ee5c1908314d7f1
3
+ metadata.gz: 11cc21d4b7941edd42fc5a17f198a55766a46fed5ea37281389f91f71703636e
4
+ data.tar.gz: e4c4173587ad7feabf2478c98816b958cf153360da52b034656caf2e6a2a3d33
5
5
  SHA512:
6
- metadata.gz: ca3c23e381fedb64619951fd7ca1687c3f4aa77ba3395f59e62ebea73a6a5b8703f912afece5d5069b2c94de1daed3e2d998d3e3ffcec28f0ed448f8d01556e1
7
- data.tar.gz: 0d04904eec7b603682610cd92d9b482b11e9295f66e319c963d27182edca19db81c2fcdfc19cc7baa732fa3f09a7b05059e50705832f094bececac857c29164b
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
- prefix = [oauth2_protocol, '://', api_domain].join
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: 'post'}
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
- class CoalescingPanda::BearcatUri
2
- attr_accessor :uri
1
+ module CoalescingPanda
2
+ class BearcatUri
3
+ attr_accessor :uri
3
4
 
4
- def initialize(uri)
5
- Rails.logger.info "Parsing Bearcat URI: #{uri}"
6
- @uri = URI.parse(uri)
7
- end
5
+ def initialize(uri)
6
+ Rails.logger.info "Parsing Bearcat URI: #{uri}"
7
+ @uri = URI.parse(uri)
8
+ end
8
9
 
9
- def api_domain
10
- if Rails.env.test? or Rails.env.development?
11
- uri.port.present? ? URI.encode("#{uri.host}:#{uri.port.to_s}") : uri.host
12
- else
13
- uri.host
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
- def scheme
18
- [uri.scheme, '://'].join
19
- end
18
+ def scheme
19
+ [uri.scheme, '://'].join
20
+ end
20
21
 
21
- def prefix
22
- [scheme, api_domain].join
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
- return if have_session?
18
- if lti_authorize!(*roles)
19
- user_id = params['user_id']
20
- launch_presentation_return_url = @lti_account.settings[:launch_presentation_return_url] || params['launch_presentation_return_url']
21
- launch_presentation_return_url = [CoalescingPanda::BearcatUri.new(request.env["HTTP_REFERER"]).prefix, launch_presentation_return_url].join unless launch_presentation_return_url.include?('http')
22
- uri = CoalescingPanda::BearcatUri.new(launch_presentation_return_url)
23
- set_session(launch_presentation_return_url)
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(uri, user_id)
43
- @lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key'])
44
- return unless @lti_account
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: { key: params['oauth_consumer_key'], user_id: user_id, api_domain: uri.api_domain }
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(uri, api_auth)
60
- refresh_client = Bearcat::Client.new(prefix: uri.prefix)
61
- refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id, resolve_coalescing_panda_url(:oauth2_redirect_url),
62
- @lti_account.oauth2_client_key, api_auth.refresh_token, 'refresh_token')
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 check_refresh_token
67
- return unless current_session_data['uri'] && current_session_data['user_id'] && current_session_data['oauth_consumer_key']
68
- uri = CoalescingPanda::BearcatUri.new(current_session_data['uri'])
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
- if (current_session_data['user_id'] && current_session_data['uri'])
94
- uri = CoalescingPanda::BearcatUri.new(current_session_data['uri'])
95
- api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', current_session_data['user_id'], uri.api_domain)
96
- if api_auth && !api_auth.expired?
97
- @client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix)
98
- @client.user_profile 'self'
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
- authorized = authorized && (roles.count == 0 || (roles & lti_roles).count > 0)
122
- logger.info 'not authorized on roles' unless authorized
123
- authorized = authorized && @lti_account.validate_nonce(params['oauth_nonce'], DateTime.strptime(params['oauth_timestamp'], '%s'))
124
- logger.info 'not authorized on nonce' unless authorized
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 valid_session?
165
- return false unless current_session(create_missing: false)&.persisted?
166
- true
167
- rescue SessionNonceMismatch
168
- false
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
@@ -1,3 +1,3 @@
1
1
  module CoalescingPanda
2
- VERSION = '5.1.12'
2
+ VERSION = '5.2.0'
3
3
  end
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.1.12
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-09-03 00:00:00.000000000 Z
13
+ date: 2021-11-22 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails