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 +4 -4
- data/README.md +23 -1
- data/app/controllers/panda_pal/lti_v1_p3_controller.rb +6 -2
- data/app/lib/panda_pal/misc_helper.rb +2 -2
- data/config/initializers/apartment.rb +59 -11
- data/lib/panda_pal/helpers/controller_helper.rb +101 -188
- data/lib/panda_pal/helpers/session_replacement.rb +192 -0
- data/lib/panda_pal/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 751203d046fbd547e8f194feabb335a81a08be96f6a557712f1673d139576a29
|
4
|
+
data.tar.gz: b06b642432ce0d1e4a42a4bc93520b3782c6d8cce801e91508b296be60501482
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
7
|
+
ActiveRecord::Type::Boolean.new.type_cast_from_user(v)
|
8
8
|
else
|
9
|
-
ActiveRecord::Type::Boolean.new.deserialize(
|
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
|
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
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
70
|
-
|
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
|
4
|
-
|
4
|
+
module PandaPal::Helpers
|
5
|
+
module ControllerHelper
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include SessionReplacement
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
30
|
-
|
31
|
-
@
|
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
|
-
|
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
|
-
|
69
|
-
|
30
|
+
def validate_launch!
|
31
|
+
safari_override
|
70
32
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
53
|
+
if !authorized
|
54
|
+
render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
|
55
|
+
end
|
101
56
|
|
102
|
-
|
103
|
-
|
104
|
-
raise JSON::JWT::VerificationFailed, 'Unrecognized Organization' unless @organization.present?
|
57
|
+
authorized
|
58
|
+
end
|
105
59
|
|
106
|
-
|
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
|
-
|
109
|
-
|
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
|
-
|
112
|
-
raise JSON::JWT::VerificationFailed, jwt_verifier.errors unless jwt_verifier.valid?
|
68
|
+
decoded_jwt.verify!(current_lti_platform.public_jwks)
|
113
69
|
|
114
|
-
|
115
|
-
|
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
|
-
|
119
|
-
|
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
|
-
|
126
|
-
|
76
|
+
@decoded_lti_jwt = decoded_jwt
|
77
|
+
rescue JSON::JWT::VerificationFailed => e
|
78
|
+
payload = Array(e.message)
|
127
79
|
|
128
|
-
|
129
|
-
|
130
|
-
|
80
|
+
render json: {
|
81
|
+
message: [
|
82
|
+
{ errors: payload },
|
83
|
+
{ id_token: params.require(:id_token) },
|
84
|
+
],
|
85
|
+
}, status: :unauthorized
|
131
86
|
|
132
|
-
|
133
|
-
yield
|
87
|
+
false
|
134
88
|
end
|
135
|
-
end
|
136
89
|
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
}
|
99
|
+
def forbid_access_if_lacking_session
|
100
|
+
super
|
101
|
+
safari_override
|
102
|
+
end
|
194
103
|
|
195
|
-
|
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
|
-
|
114
|
+
def safari_override
|
115
|
+
use_secure_headers_override(:safari_override) if browser.safari?
|
116
|
+
end
|
200
117
|
|
201
|
-
|
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
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
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
|
data/lib/panda_pal/version.rb
CHANGED
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.
|
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-
|
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:
|