coalescing_panda 1.1.21.1 → 1.2.0

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