panda_pal 5.2.4 → 5.3.5

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: 1172960b6e10f42eb328a83126a990d94d897e5b53587ad2a9fbcf2ddbcef46e
4
- data.tar.gz: 3ca6cc0d6f3e621741bf3891d858b6f8917618d31270f83aca8b0dcdd6c0ffa8
3
+ metadata.gz: 751203d046fbd547e8f194feabb335a81a08be96f6a557712f1673d139576a29
4
+ data.tar.gz: b06b642432ce0d1e4a42a4bc93520b3782c6d8cce801e91508b296be60501482
5
5
  SHA512:
6
- metadata.gz: 746e87df1af33d9391a8b7ea2d90e244c720d86100dd033a68e2b5e21a6a3d0ac057d58c44acd178e5585751d1f6e11ff006df5da10ebb910eec7712e57e1cdb
7
- data.tar.gz: b20a4ac6fc5b389c8dfd6df682d7c3c67c8fec4551bfc0fb2ada48dd2f2ac8a0806673a5a9e926bf9af961291a476978fb7bee4da32cc0f8915313c033ebd73f
6
+ metadata.gz: cc338a137cec8f7abb900b801ace89f1588b265d534babf98f6bb940307289b8a3175a9ce0d5d70a5ddeb520f3525d13e1ba0f7e52df34e75c6fda40ca03a3de
7
+ data.tar.gz: 717e7355ae2cfb990f40c589df6c29c138f55d660fa0095367a275e000a7041e98e99ec631f6dd1bf9e9f3068c7a01006d4a3580ac017cb358a18b9c9711a87f
data/README.md CHANGED
@@ -369,8 +369,30 @@ You will want to watch out for a few scenarios:
369
369
  3) If you use `link_to` and navigate in your LTI (apps that are not single page)
370
370
  make sure you include the `link_nonce` like so:
371
371
  ```ruby
372
- link_to "Link Name", somewhere_else_path(session_token: link_nonce)
372
+ link_to "Link Name", somewhere_else_path(arg, session_token: link_nonce)
373
373
  ```
374
+ NB: As of PandaPal 5.2.6, you can instead use
375
+ ```ruby
376
+ link_to "Name", url_with_session(:somewhere_else_path, arg, kwarg: 1)
377
+ ```
378
+
379
+ Persistent sessions have session_tokens as a way to safely communicate a session key in a way that is hopefully not too persistent in case it is logged somewhere.
380
+ Options for communicating session_token -
381
+ :nonce (default) - each nonce is good for exactly one communication with the backend server. Once the nonce is used, it is no longer valid.
382
+ :fixed_ip - each session_token is good until it expires. It must be used from the same ip the LTI launched from.
383
+ :expiring - this is the least secure. Each token is good until it expires.
384
+
385
+ For :fixed_ip and :expiring tokens you can override the default expiration period of 15 minutes.
386
+
387
+ See the following example of how to override the link_nonce_type and token expiration length.
388
+
389
+ class ApplicationController < ActionController::Base
390
+ link_nonce_type :fixed_ip
391
+ def session_expiration_period_minutes
392
+ 120
393
+ end
394
+ ...
395
+ end
374
396
 
375
397
  ### Previous Safari Instructions
376
398
  Safari is weird and you'll potentially run into issues getting `POST` requests to properly validate CSRF if you don't do the following:
@@ -2,8 +2,6 @@ require_dependency "panda_pal/application_controller"
2
2
 
3
3
  module PandaPal
4
4
  class LtiV1P3Controller < ApplicationController
5
- skip_before_action :verify_authenticity_token
6
-
7
5
  before_action :validate_launch!, only: [:resource_link_request]
8
6
  around_action :switch_tenant, only: [:resource_link_request]
9
7
 
@@ -37,6 +35,12 @@ module PandaPal
37
35
 
38
36
  redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(params[:launch_type])}_url", route_context: main_app)
39
37
  end
38
+ # render json: {
39
+ # launch_type: params[:launch_type],
40
+ # final_url: LaunchUrlHelpers.launch_url(params[:launch_type]),
41
+ # final_route: LaunchUrlHelpers.launch_route(params[:launch_type]),
42
+ # decoded_jwt: @decoded_lti_jwt,
43
+ # }
40
44
  end
41
45
 
42
46
  def tool_config
@@ -4,9 +4,9 @@ module PandaPal
4
4
 
5
5
  def self.to_boolean(v)
6
6
  if Rails.version < '5.0'
7
- ActiveRecord::Type::Boolean.new.type_cast_from_user("0")
7
+ ActiveRecord::Type::Boolean.new.type_cast_from_user(v)
8
8
  else
9
- ActiveRecord::Type::Boolean.new.deserialize('0')
9
+ ActiveRecord::Type::Boolean.new.deserialize(v)
10
10
  end
11
11
  end
12
12
  end
@@ -1,7 +1,8 @@
1
1
  require 'apartment/elevators/generic'
2
2
 
3
3
  Apartment.configure do |config|
4
- config.excluded_models = ['PandaPal::Organization', 'PandaPal::Session']
4
+ config.excluded_models ||= []
5
+ config.excluded_models |= ['PandaPal::Organization', 'PandaPal::Session']
5
6
 
6
7
  config.tenant_names = lambda {
7
8
  PandaPal::Organization.pluck(:name)
@@ -14,6 +15,21 @@ Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda {
14
15
  end
15
16
  }
16
17
 
18
+ module PandaPal::Plugins::ApartmentCache
19
+ private
20
+
21
+ if Rails.version >= '5.0'
22
+ def normalize_key(key, options)
23
+ "tenant:#{Apartment::Tenant.current}/#{super}"
24
+ end
25
+ else
26
+ def namespaced_key(*args)
27
+ "tenant:#{Apartment::Tenant.current}/#{super}"
28
+ end
29
+ end
30
+ end
31
+ ActiveSupport::Cache::Store.send(:prepend, PandaPal::Plugins::ApartmentCache)
32
+
17
33
  if defined?(ActionCable)
18
34
  module ActionCable
19
35
  module Channel
@@ -38,7 +54,7 @@ if defined?(ActionCable)
38
54
  end
39
55
  end
40
56
 
41
- module ActionCableApartment
57
+ module PandaPal::Plugins::ActionCableApartment
42
58
  module Connection
43
59
  def tenant=(name)
44
60
  @tenant = name
@@ -56,18 +72,50 @@ if defined?(ActionCable)
56
72
  end
57
73
  end
58
74
 
59
- ActionCable::Connection::Base.prepend(ActionCableApartment::Connection)
75
+ ActionCable::Connection::Base.prepend(PandaPal::Plugins::ActionCableApartment::Connection)
60
76
  end
61
77
 
62
- module ApartmentCache
63
- private
78
+ if defined?(Delayed)
79
+ module PandaPal::Plugins
80
+ class ApartmentDelayedJobsPlugin < ::Delayed::Plugin
81
+ callbacks do |lifecycle|
82
+ lifecycle.around(:enqueue) do |job, *args, &block|
83
+ current_tenant = Apartment::Tenant.current
64
84
 
65
- def normalize_key(key, options)
66
- "tenant:#{Apartment::Tenant.current}/#{super}"
67
- end
85
+ #make sure enqueue on public tenant unless we are testing since delayed job is set to run immediately
86
+ Apartment::Tenant.switch!('public') unless Rails.env.test?
87
+ job.tenant = current_tenant
88
+ begin
89
+ block.call(job, *args)
90
+ rescue Exception => e
91
+ Rails.logger.error("Error enqueing job #{job.to_s} - #{e.backtrace}")
92
+ ensure
93
+ #switch back to prev tenant
94
+ Apartment::Tenant.switch!(current_tenant)
95
+ end
96
+ end
97
+
98
+ lifecycle.before(:perform) do |worker, *args, &block|
99
+ tenant = args.first.tenant
100
+ Apartment::Tenant.switch!(tenant) if tenant.present?
101
+ Rails.logger.debug("Running job with tenant #{Apartment::Tenant.current}")
102
+ end
103
+
104
+ lifecycle.around(:invoke_job) do |job, *args, &block|
105
+ begin
106
+ block.call(job, *args)
107
+ ensure
108
+ Apartment::Tenant.switch!('public')
109
+ Rails.logger.debug("Resetting Tenant back to: #{Apartment::Tenant.current}")
110
+ end
111
+ end
68
112
 
69
- def namespaced_key(*args)
70
- normalize_key(*args)
113
+ lifecycle.after(:failure) do |job, *args|
114
+ Rails.logger.error("Job failed on tenant: #{Apartment::Tenant.current}")
115
+ end
116
+ end
117
+ end
71
118
  end
119
+
120
+ Delayed::Worker.plugins << PandaPal::Plugins::ApartmentDelayedJobsPlugin
72
121
  end
73
- ActiveSupport::Cache::Store.send :prepend, ApartmentCache
@@ -1,226 +1,139 @@
1
1
  require 'browser'
2
+ require_relative 'session_replacement'
2
3
 
3
- module PandaPal::Helpers::ControllerHelper
4
- extend ActiveSupport::Concern
4
+ module PandaPal::Helpers
5
+ module ControllerHelper
6
+ extend ActiveSupport::Concern
7
+ include SessionReplacement
5
8
 
6
- class SessionNonceMismatch < StandardError; end
7
-
8
- included do
9
- helper_method :link_nonce, :current_session
10
-
11
- after_action :auto_save_session
12
- end
13
-
14
- def save_session
15
- current_session.try(:save)
16
- end
17
-
18
- def current_session
19
- return @current_session if @current_session.present?
20
-
21
- if params[:session_token]
22
- payload = JSON.parse(panda_pal_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access
23
- matched_session = PandaPal::Session.find_by(session_key: payload[:session_key])
9
+ def current_organization
10
+ @organization ||= PandaPal::Organization.find_by!(key: organization_key) if organization_key
11
+ @organization ||= PandaPal::Organization.find_by(id: organization_id) if organization_id
12
+ @organization ||= PandaPal::Organization.find_by_name(Apartment::Tenant.current)
13
+ end
24
14
 
25
- if matched_session.present? && matched_session.data[:link_nonce] == payload[:nonce]
26
- @current_session = matched_session
27
- @current_session.data[:link_nonce] = nil
15
+ def current_lti_platform
16
+ return @current_lti_platform if @current_lti_platform.present?
17
+ # TODO: (Future) This could be expanded more to take better advantage of the LTI 1.3 Multi-Tenancy model.
18
+ if (canvas_url = current_organization&.settings&.dig(:canvas, :base_url)).present?
19
+ @current_lti_platform ||= PandaPal::Platform::Canvas.new(canvas_url)
28
20
  end
29
- raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
30
- elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
31
- @current_session = PandaPal::Session.find_by(session_key: session_key) if session_key.present?
21
+ @current_lti_platform ||= PandaPal::Platform::Canvas.new('http://localhost:3000') if Rails.env.development?
22
+ @current_lti_platform ||= PandaPal::Platform::CANVAS
23
+ @current_lti_platform
32
24
  end
33
25
 
34
- @current_session ||= PandaPal::Session.new(panda_pal_organization_id: current_organization.id)
35
-
36
- @current_session
37
- end
38
-
39
- def current_organization
40
- @organization ||= PandaPal::Organization.find_by!(key: organization_key) if organization_key
41
- @organization ||= PandaPal::Organization.find_by(id: organization_id) if organization_id
42
- @organization ||= PandaPal::Organization.find_by_name(Apartment::Tenant.current)
43
- end
44
-
45
- def current_lti_platform
46
- return @current_lti_platform if @current_lti_platform.present?
47
- # TODO: (Future) This could be expanded more to take better advantage of the LTI 1.3 Multi-Tenancy model.
48
- if (canvas_url = current_organization&.settings&.dig(:canvas, :base_url)).present?
49
- @current_lti_platform ||= PandaPal::Platform::Canvas.new(canvas_url)
26
+ def lti_launch_params
27
+ current_session_data[:launch_params]
50
28
  end
51
- @current_lti_platform ||= PandaPal::Platform::Canvas.new('http://localhost:3000') if Rails.env.development?
52
- @current_lti_platform ||= PandaPal::Platform::CANVAS
53
- @current_lti_platform
54
- end
55
-
56
- def current_session_data
57
- current_session.data
58
- end
59
-
60
- def lti_launch_params
61
- current_session_data[:launch_params]
62
- end
63
-
64
- def session_changed?
65
- current_session.changed? && current_session.changes[:data].present?
66
- end
67
29
 
68
- def validate_launch!
69
- safari_override
30
+ def validate_launch!
31
+ safari_override
70
32
 
71
- if params[:id_token].present?
72
- validate_v1p3_launch
73
- elsif params[:oauth_consumer_key].present?
74
- validate_v1p0_launch
75
- end
76
- end
77
-
78
- def validate_v1p0_launch
79
- authorized = false
80
- if @organization = params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
81
- sanitized_params = request.request_parameters
82
- # These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out.
83
- safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url", "dummy_param"]
84
- safe_unexpected_params.each do |p|
85
- sanitized_params.delete(p)
33
+ if params[:id_token].present?
34
+ validate_v1p3_launch
35
+ elsif params[:oauth_consumer_key].present?
36
+ validate_v1p0_launch
86
37
  end
87
- authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.original_url, sanitized_params, @organization.secret)
88
- authorized = authenticator.valid_signature?
89
38
  end
90
39
 
91
- if !authorized
92
- render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
93
- end
94
-
95
- authorized
96
- end
40
+ def validate_v1p0_launch
41
+ authorized = false
42
+ if @organization = params['oauth_consumer_key'] && PandaPal::Organization.find_by_key(params['oauth_consumer_key'])
43
+ sanitized_params = request.request_parameters
44
+ # These params come over with a safari-workaround launch. The authenticator doesn't like them, so clean them out.
45
+ safe_unexpected_params = ["full_win_launch_requested", "platform_redirect_url", "dummy_param"]
46
+ safe_unexpected_params.each do |p|
47
+ sanitized_params.delete(p)
48
+ end
49
+ authenticator = IMS::LTI::Services::MessageAuthenticator.new(request.original_url, sanitized_params, @organization.secret)
50
+ authorized = authenticator.valid_signature?
51
+ end
97
52
 
98
- def validate_v1p3_launch
99
- decoded_jwt = JSON::JWT.decode(params.require(:id_token), :skip_verification)
100
- raise JSON::JWT::VerificationFailed, 'error decoding id_token' if decoded_jwt.blank?
53
+ if !authorized
54
+ render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
55
+ end
101
56
 
102
- client_id = decoded_jwt['aud']
103
- @organization = PandaPal::Organization.find_by!(key: 'PandaPal') # client_id)
104
- raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
57
+ authorized
58
+ end
105
59
 
106
- decoded_jwt.verify!(current_lti_platform.public_jwks)
60
+ def validate_v1p3_launch
61
+ decoded_jwt = JSON::JWT.decode(params.require(:id_token), :skip_verification)
62
+ raise JSON::JWT::VerificationFailed, 'error decoding id_token' if decoded_jwt.blank?
107
63
 
108
- params[:session_key] = params[:state]
109
- raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce']
64
+ client_id = decoded_jwt['aud']
65
+ @organization = PandaPal::Organization.find_by!(key: client_id)
66
+ raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
110
67
 
111
- jwt_verifier = PandaPal::LtiJwtValidator.new(decoded_jwt, client_id)
112
- raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
68
+ decoded_jwt.verify!(current_lti_platform.public_jwks)
113
69
 
114
- @decoded_lti_jwt = decoded_jwt
115
- rescue JSON::JWT::VerificationFailed => e
116
- payload = Array(e.message)
70
+ params[:session_key] = params[:state]
71
+ raise JSON::JWT::VerificationFailed, 'State is invalid' unless current_session_data[:lti_oauth_nonce] == decoded_jwt['nonce']
117
72
 
118
- render json: {
119
- message: [
120
- { errors: payload },
121
- { id_token: params.require(:id_token) },
122
- ],
123
- }, status: :unauthorized
73
+ jwt_verifier = PandaPal::LtiJwtValidator.new(decoded_jwt, client_id)
74
+ raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
124
75
 
125
- false
126
- end
76
+ @decoded_lti_jwt = decoded_jwt
77
+ rescue JSON::JWT::VerificationFailed => e
78
+ payload = Array(e.message)
127
79
 
128
- def switch_tenant(organization = current_organization, &block)
129
- return unless organization
130
- raise 'This method should be called in an around_action callback' unless block_given?
80
+ render json: {
81
+ message: [
82
+ { errors: payload },
83
+ { id_token: params.require(:id_token) },
84
+ ],
85
+ }, status: :unauthorized
131
86
 
132
- Apartment::Tenant.switch(organization.name) do
133
- yield
87
+ false
134
88
  end
135
- end
136
89
 
137
- def forbid_access_if_lacking_session
138
- render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
139
- safari_override
140
- end
90
+ def switch_tenant(organization = current_organization, &block)
91
+ return unless organization
92
+ raise 'This method should be called in an around_action callback' unless block_given?
141
93
 
142
- def verify_authenticity_token
143
- # No need to check CSRF when no cookies were sent. This fixes CSRF failures in Browsers
144
- # that restrict Cookie setting within an IFrame.
145
- return unless request.cookies.keys.length > 0
146
- super
147
- end
148
-
149
- def valid_session?
150
- [
151
- current_session.persisted?,
152
- current_organization,
153
- current_session.panda_pal_organization_id == current_organization.id,
154
- Apartment::Tenant.current == current_organization.name
155
- ].all?
156
- rescue SessionNonceMismatch
157
- false
158
- end
159
-
160
- def safari_override
161
- use_secure_headers_override(:safari_override) if browser.safari?
162
- end
163
-
164
- # Redirect with the session key intact. In production,
165
- # handle this by adding a one-time use encrypted token to the URL.
166
- # Keeping it in the URL in development means that it plays
167
- # nicely with webpack-dev-server live reloading (otherwise
168
- # you get an access error everytime it tries to live reload).
169
-
170
- def redirect_with_session_to(location, params = {}, route_context: self, **rest)
171
- params.merge!(rest)
172
- if Rails.env.development?
173
- redirect_to route_context.send(location, {
174
- session_key: current_session.session_key,
175
- organization_id: current_organization.id,
176
- }.merge(params))
177
- else
178
- redirect_to route_context.send(location, {
179
- session_token: link_nonce,
180
- organization_id: current_organization.id,
181
- }.merge(params))
94
+ Apartment::Tenant.switch(organization.name) do
95
+ yield
96
+ end
182
97
  end
183
- end
184
-
185
- def link_nonce
186
- @link_nonce ||= begin
187
- current_session_data[:link_nonce] = SecureRandom.hex
188
98
 
189
- payload = {
190
- session_key: current_session.session_key,
191
- organization_id: current_organization.id,
192
- nonce: current_session_data[:link_nonce],
193
- }
99
+ def forbid_access_if_lacking_session
100
+ super
101
+ safari_override
102
+ end
194
103
 
195
- panda_pal_cryptor.encrypt_and_sign(payload.to_json)
104
+ def valid_session?
105
+ return false unless current_session(create_missing: false)&.persisted?
106
+ return false unless current_organization
107
+ return false unless current_session.panda_pal_organization_id == current_organization.id
108
+ return false unless Apartment::Tenant.current == current_organization.name
109
+ true
110
+ rescue SessionNonceMismatch
111
+ false
196
112
  end
197
- end
198
113
 
199
- private
114
+ def safari_override
115
+ use_secure_headers_override(:safari_override) if browser.safari?
116
+ end
200
117
 
201
- def organization_key
202
- org_key ||= params[:oauth_consumer_key]
203
- org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present?
204
- org_key ||= session[:organization_key]
205
- org_key
206
- end
118
+ private
207
119
 
208
- def organization_id
209
- params[:organization_id]
210
- end
211
-
212
- def session_key_header
213
- if match = request.headers['Authorization'].try(:match, /token=(.+)/)
214
- match[1]
120
+ def find_or_create_session(key:)
121
+ if key == :create
122
+ PandaPal::Session.new(panda_pal_organization_id: current_organization.id)
123
+ else
124
+ PandaPal::Session.find_by(session_key: key)
125
+ end
215
126
  end
216
- end
217
127
 
218
- def panda_pal_cryptor
219
- @panda_pal_cryptor ||= ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
220
- end
128
+ def organization_key
129
+ org_key ||= params[:oauth_consumer_key]
130
+ org_key ||= "#{params[:client_id]}/#{params[:deployment_id]}" if params[:client_id].present?
131
+ org_key ||= session[:organization_key]
132
+ org_key
133
+ end
221
134
 
222
- def auto_save_session
223
- yield if block_given?
224
- save_session if @current_session && session_changed?
135
+ def organization_id
136
+ params[:organization_id]
137
+ end
225
138
  end
226
139
  end
@@ -0,0 +1,192 @@
1
+ module PandaPal::Helpers
2
+ class SessionNonceMismatch < StandardError; end
3
+
4
+ module SessionReplacement
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ helper_method :link_nonce, :current_session, :current_session_data
9
+ helper_method :link_with_session_to, :url_with_session, :session_url_for
10
+
11
+ prepend_around_action :monkeypatch_flash
12
+ prepend_around_action :auto_save_session
13
+ end
14
+
15
+ class_methods do
16
+ def link_nonce_type(value = :not_given)
17
+ if value == :not_given
18
+ @link_nonce_type || superclass.try(:link_nonce_type) || :nonce
19
+ else
20
+ @link_nonce_type = value
21
+ end
22
+ end
23
+ end
24
+
25
+ def save_session
26
+ current_session.try(:save)
27
+ end
28
+
29
+ def current_session(create_missing: true)
30
+ return @current_session if @current_session.present?
31
+
32
+ if params[:session_token]
33
+ payload = JSON.parse(session_cryptor.decrypt_and_verify(params[:session_token])).with_indifferent_access
34
+ matched_session = find_or_create_session(key: payload[:session_key])
35
+ if matched_session.present?
36
+ if payload[:token_type] == 'nonce' && matched_session.data[:link_nonce] == payload[:nonce]
37
+ @current_session = matched_session
38
+ @current_session.data[:link_nonce] = nil
39
+ elsif payload[:token_type] == 'fixed_ip' && matched_session.data[:remote_ip] == request.remote_ip &&
40
+ DateTime.parse(matched_session.data[:last_ip_token_requested]) > session_expiration_period_minutes.minutes.ago
41
+ @current_session = matched_session
42
+ elsif payload[:token_type] == 'expiring' && DateTime.parse(matched_session.data[:last_token_requested]) > session_expiration_period_minutes.minutes.ago
43
+ @current_session = matched_session
44
+ end
45
+ end
46
+ raise SessionNonceMismatch, "Session Not Found" unless @current_session.present?
47
+ elsif (session_key = params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]).present?
48
+ @current_session = find_or_create_session(key: session_key)
49
+ end
50
+
51
+ @current_session ||= find_or_create_session(key: :create) if create_missing
52
+
53
+ @current_session
54
+ end
55
+
56
+ def current_session_data
57
+ current_session.data
58
+ end
59
+
60
+ def session_changed?
61
+ current_session.changed? && current_session.changes[:data].present?
62
+ end
63
+
64
+ def forbid_access_if_lacking_session
65
+ render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
66
+ end
67
+
68
+ def verify_authenticity_token
69
+ # No need to check CSRF when no cookies were sent. This fixes CSRF failures in Browsers
70
+ # that restrict Cookie setting within an IFrame.
71
+ return unless request.cookies.keys.length > 0
72
+ super
73
+ end
74
+
75
+ # Redirect with the session key intact. In production,
76
+ # handle this by adding a one-time use encrypted token to the URL.
77
+ # Keeping it in the URL in development means that it plays
78
+ # nicely with webpack-dev-server live reloading (otherwise
79
+ # you get an access error everytime it tries to live reload).
80
+
81
+ def redirect_with_session_to(*args)
82
+ redirect_to url_with_session(*args)
83
+ end
84
+
85
+ def link_with_session_to(*args)
86
+ helpers.link_to url_with_session(*args)
87
+ end
88
+
89
+ def session_url_for(*args)
90
+ url_for(build_session_url_params(*args))
91
+ end
92
+
93
+ def url_with_session(location, *args, route_context: self, **kwargs)
94
+ route_context.send(location, *build_session_url_params(*args, **kwargs))
95
+ end
96
+
97
+ def link_nonce(type: link_nonce_type)
98
+ type = instance_exec(&type) if type.is_a?(Proc)
99
+ type = type.to_s
100
+
101
+ @cached_link_nonces ||= {}
102
+ @cached_link_nonces[type] ||= begin
103
+ payload = {
104
+ token_type: type,
105
+ session_key: current_session.session_key,
106
+ organization_id: current_organization.id,
107
+ }
108
+
109
+ if type == 'nonce'
110
+ current_session_data[:link_nonce] = SecureRandom.hex
111
+ payload.merge!(nonce: current_session_data[:link_nonce])
112
+ elsif type == 'fixed_ip'
113
+ current_session_data[:remote_ip] ||= request.remote_ip
114
+ current_session_data[:last_ip_token_requested] = DateTime.now.iso8601
115
+ elsif type == 'expiring'
116
+ current_session_data[:last_token_requested] = DateTime.now.iso8601
117
+ else
118
+ raise StandardError, "Unsupported link_nonce_type: '#{type}'"
119
+ end
120
+
121
+ session_cryptor.encrypt_and_sign(payload.to_json)
122
+ end
123
+ end
124
+
125
+ def link_nonce_type
126
+ self.class.link_nonce_type
127
+ end
128
+
129
+ def session_expiration_period_minutes
130
+ 15
131
+ end
132
+
133
+ private
134
+
135
+ def session_cryptor
136
+ secret_key_base = Rails.application.try(:secret_key_base) || Rails.application.secrets.secret_key_base
137
+ @session_cryptor ||= ActiveSupport::MessageEncryptor.new(secret_key_base[0..31])
138
+ end
139
+
140
+ def session_key_header
141
+ if match = request.headers['Authorization'].try(:match, /token=(.+)/)
142
+ match[1]
143
+ end
144
+ end
145
+
146
+ def build_session_url_params(*args, nonce_type: link_nonce_type, **kwargs)
147
+ if args[-1].is_a?(Hash)
148
+ args[-1] = args[-1].dup
149
+ else
150
+ args.push({})
151
+ end
152
+
153
+ if Rails.env.development?
154
+ args[-1].merge!(
155
+ session_key: current_session.session_key,
156
+ organization_id: current_organization.id,
157
+ )
158
+ else
159
+ args[-1].merge!(
160
+ session_token: link_nonce(type: nonce_type),
161
+ organization_id: current_organization.id,
162
+ )
163
+ end
164
+
165
+ args[-1].merge!(kwargs)
166
+ args
167
+ end
168
+
169
+ def auto_save_session
170
+ yield if block_given?
171
+ save_session if @current_session && session_changed?
172
+ end
173
+
174
+ def monkeypatch_flash
175
+ if valid_session? && (value = current_session_data['flashes']).present?
176
+ flashes = value["flashes"]
177
+ if discard = value["discard"]
178
+ flashes.except!(*discard)
179
+ end
180
+ flash.replace(flashes)
181
+ flash.discard()
182
+ end
183
+
184
+ yield
185
+
186
+ if @current_session.present?
187
+ current_session_data['flashes'] = flash.to_session_value
188
+ flash.discard()
189
+ end
190
+ end
191
+ end
192
+ end
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.2.4"
2
+ VERSION = "5.3.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda_pal
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.4
4
+ version: 5.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure ProServe
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-24 00:00:00.000000000 Z
11
+ date: 2020-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -184,7 +184,7 @@ dependencies:
184
184
  - - ">="
185
185
  - !ruby/object:Gem::Version
186
186
  version: '0'
187
- description:
187
+ description:
188
188
  email:
189
189
  - pseng@instructure.com
190
190
  executables: []
@@ -237,6 +237,7 @@ files:
237
237
  - lib/panda_pal/helpers/controller_helper.rb
238
238
  - lib/panda_pal/helpers/route_helper.rb
239
239
  - lib/panda_pal/helpers/secure_headers.rb
240
+ - lib/panda_pal/helpers/session_replacement.rb
240
241
  - lib/panda_pal/plugins.rb
241
242
  - lib/panda_pal/version.rb
242
243
  - lib/tasks/panda_pal_tasks.rake
@@ -291,7 +292,7 @@ homepage: http://instructure.com
291
292
  licenses:
292
293
  - MIT
293
294
  metadata: {}
294
- post_install_message:
295
+ post_install_message:
295
296
  rdoc_options: []
296
297
  require_paths:
297
298
  - lib
@@ -307,7 +308,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
307
308
  version: '0'
308
309
  requirements: []
309
310
  rubygems_version: 3.1.2
310
- signing_key:
311
+ signing_key:
311
312
  specification_version: 4
312
313
  summary: LTI mountable engine
313
314
  test_files: