coalescing_panda 1.1.21.1 → 1.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
- SHA256:
3
- metadata.gz: 32540f0bb85313eba7c4fd929b7bf55e7eaf9530877403972269f55abe92921a
4
- data.tar.gz: 7780ee6b54a92fc11d95cc2c7c25c294a6bed3d7954c1754f6ec8180a4761e44
2
+ SHA1:
3
+ metadata.gz: 829bbfc42e1c72b89558457db1a751e63cae9ad6
4
+ data.tar.gz: c045205fcd8fa84ec952e683cf8591f395ae31b8
5
5
  SHA512:
6
- metadata.gz: f72e05c8581d3caf3541718516b4a20e72c6cb78af4c9b822982805e6da1dab9f7f7ce17ce5179eaa703898452d65d338685b3f8bf890f6c42ed67eca2f5725c
7
- data.tar.gz: b8079a6c869505f33092f442fc33c3865d515e8d781252fbc35a7fa033d9351a2650a3a558db56debaf18b6c6d1fa930c9713054e6ae168887a754ae6f6cbc1b
6
+ metadata.gz: ae89c71cbbabc5adedc5ef81a630d260eb9df0955f271d07dce2477459a28a28c7385a40899e8362a19652e9fcf333390bb9585414fd628c2e4efad5c2a8707d
7
+ data.tar.gz: 97b7a7708254b674cf679c33731b524837f1205f359a5ec7ff80033835c69e043c1fd43b339c3e2410b9878c8c1ae7e78033f434f2d7253dbb968f075839aeee
@@ -13,13 +13,17 @@ module CoalescingPanda
13
13
  client_key = lti_account.oauth2_client_key
14
14
  user_id = params[:user_id]
15
15
  api_domain = params[:api_domain]
16
- client = Bearcat::Client.new(prefix: oauth2_protocol+'://'+api_domain)
17
- token = client.retrieve_token(client_id, coalescing_panda.oauth2_redirect_url, client_key, params['code'])
18
- CanvasApiAuth.where('user_id = ? and api_domain = ?', user_id, api_domain).first_or_create do |auth|
19
- auth.api_token = token
20
- auth.user_id = user_id
21
- auth.api_domain = api_domain
22
- end
16
+ prefix = [oauth2_protocol, '://', api_domain].join
17
+ Rails.logger.info "Creating Bearcat client for auth token retrieval pointed to: #{prefix}"
18
+ client = Bearcat::Client.new(prefix: prefix)
19
+ token_body = client.retrieve_token(client_id, coalescing_panda.oauth2_redirect_url, client_key, params['code'])
20
+ auth = CanvasApiAuth.where('user_id = ? and api_domain = ?', user_id, api_domain).first_or_initialize
21
+ auth.api_token = token_body['access_token']
22
+ auth.refresh_token = token_body['refresh_token']
23
+ auth.expires_at = Time.now + token_body['expires_in'] if token_body['expires_in']
24
+ auth.user_id = user_id
25
+ auth.api_domain = api_domain
26
+ auth.save!
23
27
  end
24
28
  end
25
29
 
@@ -3,7 +3,9 @@ module CoalescingPanda
3
3
  validates :user_id, :api_domain, presence: true
4
4
  validates :user_id, uniqueness: {scope: :api_domain}
5
5
 
6
- attr_accessible :user_id, :api_domain
6
+ def expired?
7
+ expires_at && expires_at < Time.now
8
+ end
7
9
  end
8
10
 
9
11
  end
@@ -6,7 +6,6 @@ module CoalescingPanda
6
6
  :foreign_key => :coalescing_panda_lti_account_id,
7
7
  :class_name => 'CoalescingPanda::LtiNonce'
8
8
 
9
- attr_accessible :name, :key, :secret, :oauth2_client_id, :oauth2_client_key, :settings
10
9
  serialize :settings
11
10
 
12
11
  def validate_nonce(nonce, timestamp)
@@ -0,0 +1,6 @@
1
+ class AddRefreshSettingsToCanvasApiAuth < ActiveRecord::Migration
2
+ def change
3
+ add_column :coalescing_panda_canvas_api_auths, :refresh_token, :string
4
+ add_column :coalescing_panda_canvas_api_auths, :expires_at, :datetime
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ class CoalescingPanda::BearcatUri
2
+ attr_accessor :uri
3
+
4
+ def initialize(uri)
5
+ Rails.logger.info "Parsing Bearcat URI: #{uri}"
6
+ @uri = URI.parse(uri)
7
+ end
8
+
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
14
+ end
15
+ end
16
+
17
+ def scheme
18
+ [uri.scheme, '://'].join
19
+ end
20
+
21
+ def prefix
22
+ [scheme, api_domain].join
23
+ end
24
+ end
@@ -1,5 +1,3 @@
1
- require 'browser'
2
-
3
1
  module CoalescingPanda
4
2
  module ControllerHelpers
5
3
  require 'useragent'
@@ -9,91 +7,111 @@ module CoalescingPanda
9
7
  if lti_authorize!(*roles)
10
8
  user_id = params['user_id']
11
9
  launch_presentation_return_url = @lti_account.settings[:launch_presentation_return_url] || params['launch_presentation_return_url']
12
- uri = URI.parse(launch_presentation_return_url)
13
- api_domain = uri.host
14
- api_domain = "#{api_domain}:#{uri.port.to_s}" if uri.port
15
- scheme = uri.scheme + '://'
16
- @lti_params = params.to_hash
17
- session['user_id'] = user_id
18
- session['uri'] = launch_presentation_return_url
19
- session['lis_person_sourcedid'] = params['lis_person_sourcedid']
20
- session['oauth_consumer_key'] = params['oauth_consumer_key']
21
- session['custom_canvas_account_id'] = params['custom_canvas_account_id']
22
-
23
- if token = CanvasApiAuth.where('user_id = ? and api_domain = ?', user_id, api_domain).pluck(:api_token).first
24
- @client = Bearcat::Client.new(token: token, prefix: scheme+api_domain)
25
- elsif @lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key'])
26
- client_id = @lti_account.oauth2_client_id
27
- client = Bearcat::Client.new(prefix: scheme+api_domain)
28
- session['state'] = SecureRandom.hex(32)
29
- @canvas_url = client.auth_redirect_url(client_id,
30
- coalescing_panda.oauth2_redirect_url({key: params['oauth_consumer_key'],
31
- user_id: user_id, api_domain: api_domain, state: session['state']}))
32
- #delete the added params so the original oauth sig still works
33
- @lti_params.delete('action')
34
- @lti_params.delete('controller')
35
- render 'coalescing_panda/oauth2/oauth2'
10
+ launch_presentation_return_url = [BearcatUri.new(request.env["HTTP_REFERER"]).prefix, launch_presentation_return_url].join unless launch_presentation_return_url.include?('http')
11
+ uri = BearcatUri.new(launch_presentation_return_url)
12
+ set_session(launch_presentation_return_url)
13
+
14
+ api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', user_id, uri.api_domain)
15
+ if api_auth
16
+ begin
17
+ refresh_token(uri, api_auth) if api_auth.expired?
18
+ @client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix)
19
+ @client.user_profile 'self'
20
+ rescue Footrest::HttpError::BadRequest, Footrest::HttpError::Unauthorized
21
+ # If we can't retrieve our own user profile, or the token refresh fails, something is awry on the canvas end
22
+ # and we'll need to go through the oauth flow again
23
+ render_oauth2_page uri, user_id
24
+ end
25
+ else
26
+ render_oauth2_page uri, user_id
36
27
  end
37
28
  end
38
29
  end
39
30
 
31
+ def render_oauth2_page(uri, user_id)
32
+ @lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key'])
33
+ return unless @lti_account
34
+
35
+ client_id = @lti_account.oauth2_client_id
36
+ client = Bearcat::Client.new(prefix: uri.prefix)
37
+ session['state'] = SecureRandom.hex(32)
38
+ redirect_path = coalescing_panda.oauth2_redirect_path({key: params['oauth_consumer_key'], user_id: user_id, api_domain: uri.api_domain, state: session['state']})
39
+ redirect_url = [coalescing_panda_url, redirect_path.sub(/^\/lti/, '')].join
40
+ @canvas_url = client.auth_redirect_url(client_id, redirect_url)
41
+
42
+ #delete the added params so the original oauth sig still works
43
+ @lti_params = params.to_hash
44
+ @lti_params.delete('action')
45
+ @lti_params.delete('controller')
46
+ render 'coalescing_panda/oauth2/oauth2', layout: 'coalescing_panda/application'
47
+ end
48
+
49
+ def refresh_token(uri, api_auth)
50
+ refresh_client = Bearcat::Client.new(prefix: uri.prefix)
51
+ refresh_body = refresh_client.retrieve_token(@lti_account.oauth2_client_id, coalescing_panda.oauth2_redirect_url,
52
+ @lti_account.oauth2_client_key, api_auth.refresh_token, 'refresh_token')
53
+ api_auth.update({ api_token: refresh_body['access_token'], expires_at: (Time.now + refresh_body['expires_in']) })
54
+ end
55
+
56
+ def check_refresh_token
57
+ uri = BearcatUri.new(session['uri'])
58
+ api_auth = CanvasApiAuth.find_by(user_id: session['user_id'], api_domain: uri.api_domain)
59
+ @lti_account = LtiAccount.find_by(key: session['oauth_consumer_key'])
60
+ return if @lti_account.nil? || api_auth.nil? # Not all tools use oauth
61
+
62
+ refresh_token(uri, api_auth) if api_auth.expired?
63
+ rescue Footrest::HttpError::BadRequest
64
+ render_oauth2_page uri, session['user_id']
65
+ end
66
+
67
+ def set_session(launch_presentation_return_url)
68
+ session['user_id'] = params['user_id']
69
+ session['uri'] = launch_presentation_return_url
70
+ session['lis_person_sourcedid'] = params['lis_person_sourcedid']
71
+ session['oauth_consumer_key'] = params['oauth_consumer_key']
72
+ session['custom_canvas_account_id'] = params['custom_canvas_account_id']
73
+ end
74
+
40
75
  def have_session?
41
- if params['oauth_consumer_key'] && session['user_id'] != params['user_id']
76
+ if params['tool_consumer_instance_guid'] && session['user_id'] != params['user_id']
42
77
  reset_session
43
78
  logger.info("resetting session params")
44
79
  session['user_id'] = params['user_id']
45
80
  end
46
81
 
47
82
  if (session['user_id'] && session['uri'])
48
- uri = URI.parse(session['uri'])
49
- api_domain = uri.host
50
- api_domain = "#{api_domain}:#{uri.port.to_s}" if uri.port
51
- scheme = uri.scheme + '://'
52
- token = CanvasApiAuth.where('user_id = ? and api_domain = ?', session['user_id'], api_domain).pluck(:api_token).first
53
- @client = Bearcat::Client.new(token: token, prefix: scheme+api_domain) if token
83
+ uri = BearcatUri.new(session['uri'])
84
+ api_auth = CanvasApiAuth.find_by('user_id = ? and api_domain = ?', session['user_id'], uri.api_domain)
85
+ if api_auth && !api_auth.expired?
86
+ @client = Bearcat::Client.new(token: api_auth.api_token, prefix: uri.prefix)
87
+ @client.user_profile 'self'
88
+ end
54
89
  end
55
90
 
56
91
  @lti_account = LtiAccount.find_by_key(session['oauth_consumer_key']) if session['oauth_consumer_key']
57
92
 
58
93
  !!@client
94
+ rescue Footrest::HttpError::Unauthorized
95
+ false
59
96
  end
60
97
 
61
98
  def lti_authorize!(*roles)
62
99
  authorized = false
63
- use_secure_headers_override(:safari_override) if browser.safari?
64
100
  if @lti_account = params['oauth_consumer_key'] && LtiAccount.find_by_key(params['oauth_consumer_key'])
65
- sanitized_params = sanitize_params
66
- authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.original_url, sanitized_params, @lti_account.secret)
67
- authorized = authenticator.valid_signature?
68
101
  @tp = IMS::LTI::ToolProvider.new(@lti_account.key, @lti_account.secret, params)
69
- authorized = authorized && @tp.valid_request?(request)
102
+ authorized = @tp.valid_request?(request)
70
103
  end
71
104
  logger.info 'not authorized on tp valid request' if !authorized
72
105
  authorized = authorized && (roles.count == 0 || (roles & lti_roles).count > 0)
73
106
  logger.info 'not authorized on roles' if !authorized
74
107
  authorized = authorized && @lti_account.validate_nonce(params['oauth_nonce'], DateTime.strptime(params['oauth_timestamp'], '%s'))
75
108
  logger.info 'not authorized on nonce' if !authorized
76
- if !authorized
77
- render :text => 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized
78
- end
79
- authorized = authorized && check_for_iframes_problem if authorized
109
+ render :text => 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
80
110
  authorized
81
111
  end
82
112
 
83
- # code for method taken from panda_pal v 4.0.8
84
- # used for safari workaround
85
- def sanitize_params
86
- sanitized_params = request.request_parameters
87
- # These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out.
88
- safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url"]
89
- safe_unexpected_params.each do |p|
90
- sanitized_params.delete(p)
91
- end
92
- sanitized_params
93
- end
94
-
95
113
  def lti_editor_button_response(return_type, return_params)
96
- valid_return_types = [:image_url, :iframe, :url]
114
+ valid_return_types = [:image_url, :iframe, :url, :lti_launch_url]
97
115
  raise "invalid editor button return type #{return_type}" unless valid_return_types.include?(return_type)
98
116
  return_params[:return_type] = return_type.to_s
99
117
  return_url = "#{params['launch_presentation_return_url']}?#{return_params.to_query}"
@@ -133,35 +151,19 @@ module CoalescingPanda
133
151
  end
134
152
 
135
153
  def session_check
136
- logger.warn 'session_check is deprecated. Functionality moved to canvas_oauth2.'
137
- end
138
-
139
- def check_for_iframes_problem
140
- if cookies_need_iframe_fix?
141
- fix_iframe_cookies
142
- return false
143
- end
144
- # For safari we may have been launched temporarily full-screen by canvas. This allows us to set the session cookie.
145
- # In this case, we should make sure the session cookie is fixed and redirect back to canvas to properly launch the embedded LTI.
146
- if params[:platform_redirect_url]
147
- session[:safari_cookie_fixed] = true
148
- redirect_to params[:platform_redirect_url]
149
- return false
150
- end
151
- true
152
- end
153
- def cookies_need_iframe_fix?
154
- @browser ||= Browser.new(request.user_agent)
155
- @browser.safari? && !request.referrer.include?('sessionless_launch') && !session[:safari_cookie_fixed] && !params[:platform_redirect_url]
156
- end
157
- def fix_iframe_cookies
158
- if params[:safari_cookie_fix].present?
159
- session[:safari_cookie_fixed] = true
160
- redirect_to params[:return_to]
161
- else
162
- use_secure_headers_override(:safari_override)
163
- render 'coalescing_panda/lti/iframe_cookie_fix', layout: false
154
+ user_agent = UserAgent.parse(request.user_agent) # Uses useragent gem!
155
+ if user_agent.browser == 'Safari' && !request.referrer.include?('sessionless_launch') # we apply the fix..
156
+ return if session[:safari_cookie_fixed] # it is already fixed.. continue
157
+ if params[:safari_cookie_fix].present? # we should be top window and able to set cookies.. so fix the issue :)
158
+ session[:safari_cookie_fixed] = true
159
+ redirect_to params[:return_to]
160
+ else
161
+ # Redirect the top frame to your server..
162
+ query = params.to_query
163
+ render :text => "<script>var referrer = document.referrer; top.window.location='?safari_cookie_fix=true&return_to='.concat(encodeURI(referrer));</script>"
164
+ end
164
165
  end
165
166
  end
167
+
166
168
  end
167
169
  end
@@ -1,7 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
- require 'secure_headers'
4
-
5
1
  module CoalescingPanda
6
2
  class Engine < ::Rails::Engine
7
3
  config.autoload_once_paths += Dir["#{config.root}/lib/**/"]
@@ -9,80 +5,30 @@ module CoalescingPanda
9
5
 
10
6
  initializer :append_migrations do |app|
11
7
  unless app.root.to_s.match root.to_s
12
- config.paths['db/migrate'].expanded.each do |expanded_path|
13
- app.config.paths['db/migrate'] << expanded_path
8
+ config.paths["db/migrate"].expanded.each do |expanded_path|
9
+ app.config.paths["db/migrate"] << expanded_path
14
10
  end
15
11
  end
16
12
  end
17
13
 
18
- initializer 'coalescing_panda.app_controller' do |_app|
14
+ initializer 'coalescing_panda.app_controller' do |app|
19
15
  OAUTH_10_SUPPORT = true
20
16
  ActiveSupport.on_load(:action_controller) do
21
17
  include CoalescingPanda::ControllerHelpers
22
18
  end
23
19
  end
24
20
 
25
- initializer 'cloaescing_panda.route_helper' do |_route|
26
- ActionDispatch::Routing::Mapper.include CoalescingPanda::RouteHelpers
21
+ initializer 'cloaescing_panda.route_helper' do |route|
22
+ ActionDispatch::Routing::Mapper.send :include, CoalescingPanda::RouteHelpers
27
23
  end
28
24
 
29
- initializer 'coalescing_panda.route_options', after: :disable_dependency_loading do |_app|
25
+ initializer 'coalescing_panda.route_options', :after => :disable_dependency_loading do |app|
30
26
  ActiveSupport.on_load(:action_controller) do
31
- # force the routes to load
27
+ #force the routes to load
32
28
  Rails.application.reload_routes!
33
- CoalescingPanda.propagate_lti_navigation
29
+ CoalescingPanda::propagate_lti_navigation
34
30
  end
35
31
  end
36
32
 
37
- initializer :secure_headers do |_app|
38
- connect_src = %w[self]
39
- script_src = %w[self]
40
- if Rails.env.development?
41
- # Allow webpack-dev-server to work
42
- connect_src << 'http://localhost:3035'
43
- connect_src << 'ws://localhost:3035'
44
- # Allow stuff like rack-mini-profiler to work in development:
45
- # https://github.com/MiniProfiler/rack-mini-profiler/issues/327
46
- # DON'T ENABLE THIS FOR PRODUCTION!
47
- script_src << "'unsafe-eval'"
48
- end
49
- begin
50
- SecureHeaders::Configuration.default do |config|
51
- # The default cookie headers aren't compatable with PandaPal cookies currenntly
52
- config.cookies = { samesite: { none: true } }
53
- # Need to allow LTI iframes
54
- config.x_frame_options = 'ALLOWALL'
55
- config.x_content_type_options = 'nosniff'
56
- config.x_xss_protection = '1; mode=block'
57
- config.referrer_policy = %w[origin-when-cross-origin strict-origin-when-cross-origin]
58
- config.csp = {
59
- default_src: %w[self],
60
- script_src: script_src,
61
- # Certain CSS-in-JS libraries inline the CSS, so we need to use unsafe-inline for them
62
- style_src: %w[self unsafe-inline blob: https://fonts.googleapis.com],
63
- font_src: %w[self data: https://fonts.gstatic.com],
64
- connect_src: connect_src
65
- }
66
- end
67
- rescue AlreadyConfiguredError
68
- Rails.logger.warn 'Could not set default secure headers configuration when there is one already, continuing with previously defined configuration'
69
- end
70
- SecureHeaders::Configuration.override(:safari_override) do |config|
71
- config.cookies = SecureHeaders::OPT_OUT
72
- # Need to allow LTI iframes
73
- config.x_frame_options = 'ALLOWALL'
74
- config.x_content_type_options = 'nosniff'
75
- config.x_xss_protection = '1; mode=block'
76
- config.referrer_policy = %w[origin-when-cross-origin strict-origin-when-cross-origin]
77
- config.csp = {
78
- default_src: %w[self],
79
- script_src: script_src,
80
- # Certain CSS-in-JS libraries inline the CSS, so we need to use unsafe-inline for them
81
- style_src: %w[self unsafe-inline blob: https://fonts.googleapis.com],
82
- font_src: %w[self data: https://fonts.gstatic.com],
83
- connect_src: connect_src
84
- }
85
- end
86
- end
87
33
  end
88
34
  end
@@ -1,3 +1,3 @@
1
1
  module CoalescingPanda
2
- VERSION = '1.1.21.1'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe CoalescingPanda::LtiController do
3
+ describe CoalescingPanda::LtiController, type: :controller do
4
4
  routes { CoalescingPanda::Engine.routes }
5
5
 
6
6
  describe '#lti_config' do
@@ -41,4 +41,4 @@ describe CoalescingPanda::LtiController do
41
41
  end
42
42
 
43
43
 
44
- end
44
+ end
@@ -1,15 +1,20 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe CoalescingPanda::Oauth2Controller do
3
+ describe CoalescingPanda::Oauth2Controller, type: :controller do
4
4
  routes { CoalescingPanda::Engine.routes }
5
+ let(:account) { FactoryGirl.create(:account, settings: {base_url: 'foo.com'}) }
5
6
 
6
7
  describe "#redirect" do
7
8
  it 'creates a token in the db' do
8
9
  ENV['OAUTH_PROTOCOL'] = 'http'
9
- Bearcat::Client.any_instance.stub(retrieve_token: 'foobar')
10
- get :redirect, {user_id: 1, api_domain:'foo.com', code:'bar'}
10
+ Bearcat::Client.any_instance.stub(retrieve_token: { 'access_token' => 'token', 'refresh_token' => 'token', 'expires_in' => 3600 })
11
+ session[:state] = 'test'
12
+ get :redirect, {user_id: 1, api_domain: 'foo.com', code: 'bar', key: account.key, state: 'test'}
11
13
  auth = CoalescingPanda::CanvasApiAuth.find_by_user_id_and_api_domain(1, 'foo.com')
12
14
  auth.should_not == nil
15
+ expect(auth.api_token).to eql 'token'
16
+ expect(auth.refresh_token).to eql 'token'
17
+ expect(auth.expires_at).to be > 50.minutes.from_now
13
18
  end
14
19
 
15
20
  it "doesn't create a token in the db" do
@@ -19,4 +24,4 @@ describe CoalescingPanda::Oauth2Controller do
19
24
 
20
25
  end
21
26
 
22
- end
27
+ end