panda_pal 5.1.0 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +139 -93
  3. data/app/controllers/panda_pal/lti_controller.rb +0 -18
  4. data/app/controllers/panda_pal/lti_v1_p0_controller.rb +34 -0
  5. data/app/controllers/panda_pal/lti_v1_p3_controller.rb +98 -0
  6. data/app/lib/lti_xml/base_platform.rb +4 -4
  7. data/app/lib/panda_pal/launch_url_helpers.rb +69 -0
  8. data/app/lib/panda_pal/lti_jwt_validator.rb +88 -0
  9. data/app/lib/panda_pal/misc_helper.rb +11 -0
  10. data/app/models/panda_pal/organization.rb +6 -3
  11. data/app/models/panda_pal/{organization → organization_concerns}/settings_validation.rb +21 -5
  12. data/app/models/panda_pal/{organization → organization_concerns}/task_scheduling.rb +32 -0
  13. data/app/models/panda_pal/platform.rb +40 -0
  14. data/app/views/panda_pal/lti_v1_p3/login.html.erb +1 -0
  15. data/app/views/panda_pal/partials/_auto_submit_form.html.erb +9 -0
  16. data/config/dev_lti_key.key +27 -0
  17. data/config/routes.rb +12 -2
  18. data/db/migrate/20160412205931_create_panda_pal_organizations.rb +1 -1
  19. data/db/migrate/20160413135653_create_panda_pal_sessions.rb +1 -1
  20. data/db/migrate/20160425130344_add_panda_pal_organization_to_session.rb +1 -1
  21. data/db/migrate/20170106165533_add_salesforce_id_to_organizations.rb +1 -1
  22. data/db/migrate/20171205183457_encrypt_organization_settings.rb +1 -1
  23. data/db/migrate/20171205194657_remove_old_organization_settings.rb +8 -3
  24. data/lib/panda_pal.rb +28 -15
  25. data/lib/panda_pal/engine.rb +8 -39
  26. data/lib/panda_pal/helpers.rb +1 -0
  27. data/lib/panda_pal/helpers/controller_helper.rb +138 -44
  28. data/lib/panda_pal/helpers/route_helper.rb +8 -8
  29. data/lib/panda_pal/helpers/secure_headers.rb +79 -0
  30. data/lib/panda_pal/version.rb +1 -1
  31. data/panda_pal.gemspec +3 -2
  32. metadata +32 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a2c4fed3544a682a44b4e9acfc30742ab7f7bcdf2ec22db97d90df80af31e04
4
- data.tar.gz: f0ee1ba262458c9a5fee095c17c1c673e58e90cc821ea92d29b59cee2afcd1e4
3
+ metadata.gz: 581c5acd9f7a7ade63f59c20f00f2dcbe7ee4e77670a6e5b98e3b29491f2edc9
4
+ data.tar.gz: d42d100b86c391585fa5f45d23368711487c78eff96916aa9868ba1e22f868c1
5
5
  SHA512:
6
- metadata.gz: adcb31a168a7c5eb09c5ac4ac4c28de5f45ba480873210e309bf5ba60c4c29b4f172648ed03ccee54cfc0e585eac0a25c56be3686dadf582d11e8b95c24a2870
7
- data.tar.gz: b06936171d9f3424d2de2cac678726e33d081084a0a99195c6c6d4b7d4286d0a88bb91c9e1ea269ccccbb3f3ea9698a60096b3b7baf8b162f5db751f7b45521c
6
+ metadata.gz: 3ec8c2b412e14dcfc0398fbb496d68d62aec0b897a3f30b5471df867d32b3db8279bd774c0b620a074b204cbbcf5f51c4edb80cfc92285205849f77c11d68122
7
+ data.tar.gz: 397d0cfccec6719e1ca201925fd7b5d23e87dff5a4170169c9f7aafd741a2956c8540abcf19dcbbff672faa3e85c54d0e81bc73d033a5e7d5f1ba21d5437577e
data/README.md CHANGED
@@ -11,7 +11,12 @@ PandaPal.lti_properties = {oauth_compliant: 'true'}
11
11
  PandaPal.lti_environments = { domain: ENV['PROD_DOMAIN'], beta_domain: ENV['BETA_DOMAIN'], test_domain: ENV['TEST_DOMAIN'] }
12
12
  PandaPal.lti_custom_params = { custom_canvas_role: '$Canvas.membership.roles' }
13
13
  # :user_navigation, :course_navigation for user context and course context endpoints respectively
14
- PandaPal::stage_navigation(:account_navigation, { enabled: true, text: 'Teacher Reports', visibility: 'admins' })
14
+ PandaPal::stage_navigation(:account_navigation, {
15
+ enabled: true,
16
+ # url: :account_index, # Optional if using lti_nav
17
+ text: 'Teacher Reports',
18
+ visibility: 'admins',
19
+ })
15
20
  ```
16
21
 
17
22
  Configuration data for an installation can be set by creating a `PandaPal::Organization` record. Due to the nature of the data segregation, once created, the name of the organization should not be changed (and will raise a validation error).
@@ -62,16 +67,16 @@ module PandaPal
62
67
  end
63
68
  ```
64
69
 
65
- # Organization Attributes
66
- id: Primary Key
67
- name: Name of the organization. Used to on requests to select the tenant
68
- key: Key field from CanvasLMS
69
- secret: Secret field from CanvasLMS
70
- canvas_account_id: ID of the corresponding Canvas account.
71
- settings: Hash of settings for this Organization
72
- salesforce_id: ID of this organization in Salesforce
70
+ ### Organization Attributes
71
+ `id`: Primary Key
72
+ `name`: Name of the organization. Used to on requests to select the tenant
73
+ `key`: Key field from CanvasLMS
74
+ `secret`: Secret field from CanvasLMS
75
+ `canvas_account_id`: ID of the corresponding Canvas account.
76
+ `settings`: Hash of settings for this Organization
77
+ `salesforce_id`: ID of this organization in Salesforce
73
78
 
74
- XML for an installation can be generated by visiting /lti/config in your application.
79
+ XML for an installation can be generated by visiting `/lti/config` in your application.
75
80
 
76
81
  ### Routing
77
82
 
@@ -80,7 +85,7 @@ The following routes should be added to the routes.rb file of the implementing L
80
85
  ```ruby
81
86
  # config/routes.rb
82
87
  mount PandaPal::Engine, at: '/lti'
83
- lti_nav account_navigation: 'accounts#launch'
88
+ lti_nav account_navigation: 'accounts#launch' # Use lti_nav to provide a custom Launch implementation, otherwise use the url: param of stage_navigation to let PandaPal handle launch.
84
89
  root to: 'panda_pal/lti#launch'
85
90
  ```
86
91
 
@@ -115,9 +120,10 @@ It is also possible to switch tenants by implementing `around_action :switch_ten
115
120
  However, it should be noted that calling `switch_tenant` must be used in an `around_action` hook (or given a block), and is only intended to be used for LTI launches, and not subsequent requests.
116
121
 
117
122
  ### Rake tasks and jobs
118
- # Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs.
123
+ **Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs.**
124
+
119
125
  The Apartment Gem makes it so background jobs need to be run within the current tenant. If using sidekiq, there is a gem that
120
- does this automatically called apartment-sidkiq. If Delayed Jobs, see how it's done in version 1 of PandaPal.
126
+ does this automatically called `apartment-sidkiq`. If Delayed Jobs, see how it's done in version 1 of PandaPal.
121
127
 
122
128
  For rake tasks, the current tenant will be set to `public`, and thus special handling must be taken when running the task or queueing jobs as part of it.
123
129
 
@@ -159,25 +165,52 @@ Controllers will automatically have access to a number of methods that can be he
159
165
  * `save_session` - Saves the session given by `current_session` to the database.
160
166
 
161
167
  ## Sessions and `current_session`
162
- A session_key returned by `current_session` is essentially an access token, and while we can't prevent users from sharing if they're so inclined, it should still be kept hidden as much as possible (namely by not including it as a URL param at any time).
163
- A good way to pass the `session_key` if using ajax or something similar, is to define it as a header in `$.ajaxSetup`. This is particularly useful if a javascript framework such as React is being utilized.
168
+ Because of the ever-increasing security around `<iframe>`s and cookies, a custom session implementation has been rolled into `PandaPal`.
169
+ The implementation is mostly passive, but requires some changes and caveats as opposed to the native Rails implementation.
164
170
 
165
- PandaPal provides a number of ways to find a session, shown in the example below
171
+ The current session object can be accessed from any controller using `current_session`. This object isn't the actual data container (as `session` _is_ in Rails).
172
+ Session data can be accessed and modified using `current_session.data[:key]` or `current_session_data[:key]`.
166
173
 
167
- ```ruby
168
- def session_key
169
- params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]
170
- end
174
+ Because cookies are unreliable, it becomes the responsibility of the LTI developer to ensure that a custom "cookie" is passed to the frontend with each response
175
+ and returned to the backend with each request, otherwise the backend won't be able to access the current session. There are a few ways to do this.
171
176
 
172
- def session_key_header
173
- if match = request.headers['Authorization'].try(:match, /token=(.+)/)
174
- match[1]
175
- end
176
- end
177
+ Generally, the method for passing the session "cookie" to the frontend looks something like:
178
+ ```html
179
+ <meta name="session_key" content="<%= current_session.session_key %>">
177
180
  ```
178
181
 
182
+ ### Returning to the backend
183
+ The `session_key` can be passed to the backend in multiple ways:
184
+ 1. (Recommended) Via the `Authorization` Header (usually defining it as a default in your AJAX/XHR/`fetch` library):
185
+ ```http
186
+ Authorization: token=SESSION_KEY_HERE
187
+ ```
188
+ 2. Via a `session_key` parameter in a `POST` request (Useful for native HTML `<form>`s).
189
+ 3. (Used for Dev, but not highly discouraged in Prod) via a `GET`/URL Query parameter: `?session_key=SESSION_KEY_HERE`.
190
+
191
+ The `session_key` does not contain any secrets itself, so it is safe to pass to the frontend, but it is encouraged to keep it away from the
192
+ end-user as much as possible because it should not be shared.
193
+
194
+ ### Redirecting and Multi-Page Applications
195
+ Special instructions must be followed when using `link_to` or `redirect_to` to ensure that the Session is passed correctly:
196
+
197
+ Add a `session_token:` (notice the use of _`_token`_ as opposed to _`_key`_!) parameter when using `link_to` or `redirect_to`:
198
+ ```ruby
199
+ link_to "Link Name", somewhere_else_path(session_token: link_nonce)
200
+ redirect_to somewhere_else_path(session_token: link_nonce)
201
+ ```
202
+ You can also use the `redirect_with_session_to` helper, which will automatically add `organization_id:` and `session_token:` params:
203
+ ```ruby
204
+ redirect_with_session_to :somewhere_else_path, other_param: 1
205
+ ```
206
+ For each request (not each call), `link_nonce` generates a nonce and stores it in the Session. The generated value can be used
207
+ for at-most-one future request. This means that browser back-forward navigation **will not work** (if it actually ever worked for
208
+ iframe-based LTIs in the first place).
209
+
179
210
  ### Persisting the session
180
- In order to reduce the number of sessions saved to the database, a session should only be saved if data has been added to it. A good way to accomplish this is to add `append_after_action :save_session, if: proc { session_changed? }` to the application_controller.rb
211
+ The session is automatically saved using an `after_action` callback. This callback is poised to run last, but if that causes issues
212
+ (eg some other callback running afterwards and needing to modify the session), `skip_after_action :auto_save_session` can be used
213
+ and a custom implementation can be supplemented.
181
214
 
182
215
  ### Session cleanup
183
216
  Over time, the sessions table will become bloated with sessions that are no longer active.
@@ -185,11 +218,6 @@ As such, `rake panda_pal:clean_sessions` should be run periodically to clean up
185
218
 
186
219
  ## Organizations and `current_organization`
187
220
  Similar to `current_session`, `current_organization` can be returned with a number of methods, shown below
188
- ```ruby
189
- def organization_key
190
- params[:oauth_consumer_key] || params[:organization_id] || current_session_data[:organization_key] || session[:organization_key]
191
- end
192
- ```
193
221
 
194
222
  ## Security
195
223
 
@@ -213,49 +241,21 @@ It can be overridden on a action-by-action basis by using `skip_before_action`.
213
241
  skip_before_action :forbid_access_if_lacking_session, only: :launch
214
242
  ```
215
243
 
216
- ### Upgrading to version 3
217
-
218
- Before upgrading save existing settings somewhere safe in case you need to
219
- restore them for whatever reason.
220
-
221
- panda_pal v3 introduces an encrypted settings hash. This should provide more security in the case where a
222
- customer may have gained access to Organization information in the database. Settings cannot be decrypted
223
- without knowing the secret decryption key. For panda_pal, we are relying on the secret_key_base to be set
224
- (typically in config/secrets.yml), if that is not available we are falling back to directly use
225
- ENV['SECRET_KEY_BASE']. For production environments, config/secrets.yml should not have a plain-text secret,
226
- it should be referencing an ENV variable. Make sure your secret is not plain-text committed to a repository!
227
-
228
- The secret key is used to encrypt / decrypt the settings hash as necessary.
229
-
230
- Before upgrading to version 3, you should confirm that the secret key is set to a consistent value (if the
231
- value is lost your settings will be hosed).
232
-
233
- You should also rollback any local encryption you have done on the settings hash. The settings hash
234
- should just be plainly visible when you upgrade to V3. Otherwise the panda_pal migrations will
235
- probably fail.
236
-
237
- Once you have upgraded your gem, you will need to run migrations. Before doing that, I would store off your
238
- unencrypted settings (just in case).
239
-
240
- `rake db:migrate`
241
-
242
- If all goes well, you should be set! Log into console, and verify that you can still access settings:
243
-
244
- `PandaPal::Organization.first.settings`
245
-
246
- should show unencrypted settings in your console.
247
-
248
- If anything goes wrong, you should be able to rollback the change:
249
-
250
- `rake db:rollback STEP=2`
251
-
252
- If you need to give up on the change, just make sure to change your gem version for panda_pal to be < 3.
244
+ ### Secure Headers
245
+ PandaPal bundles the `secure_headers` Gem and provides a default config. The PandaPal default works for really basic apps, but you may need to provide
246
+ a custom configuration. This can be done by creating a `secure_headers.rb` initializer in you App like so:
247
+ ```ruby
248
+ SecureHeaders::Configuration.default do |config|
249
+ PandaPal::SecureHeaders.apply_defaults(config) # Optional, but recommended
250
+ # ...
251
+ end
252
+ ```
253
253
 
254
- ### Validating settings in your LTI.
254
+ ## Validating settings in your LTI.
255
255
 
256
256
  You can specify a structure that you would like to have enforced for your settings.
257
257
 
258
- In your panda_pal initializer (i.e. config/initializers/panda_pal.rb or config/initializers/lti.rb)
258
+ In your panda_pal initializer (i.e. `config/initializers/panda_pal.rb` or `config/initializers/lti.rb`)
259
259
 
260
260
  You can specify options that can include a structure for your settings. If specified, PandaPal will
261
261
  enforce this structure on any new / updated organizations.
@@ -339,9 +339,41 @@ This is determined by `PandaPal.lti_options[:platform]`.
339
339
  Set this to `platform: 'canvas.instructure.com'` (default)
340
340
  OR `platform: 'bridgeapp.com'`
341
341
 
342
- ### Safari Support
342
+ ## Development:
343
+ ### Running Specs:
344
+ Initialize the Specs DB:
345
+ `cd spec/dummy; bundle exec rake db:drop; bundle exec rake db:create; bundle exec rake db:schema:load`
346
+ Then `bundle exec rspec`
343
347
 
344
- Safari is weird, and you'll potentially run into issues getting `POST` requests to properly validate CSRF if you don't do the following:
348
+ ## Safari Support
349
+ Safari is weird (it blocks cookies in `<iframe>`s unless the site has set a cookie _outside_ of an `<iframe>` first), and you'll run into
350
+ issues with Rails-native Sessions and CSRF because of that.
351
+
352
+ This means that safari will likely refuse to send info about your rails session
353
+ back to the LTI, and the application will start up a new session each time the
354
+ browser navigates. This likely means a new session each time the LTI launches.
355
+
356
+ ### PandaPal 5
357
+ It has been a constant struggle to force safari to store and allow
358
+ access to a rails session while the application is embedded in Canvas.
359
+
360
+ As of PandaPal 5, a session cookie is no longer required by panda_pal.
361
+
362
+ See the Section on [Sessions and `current_session`](#sessions-and-current_session)
363
+
364
+ You will want to watch out for a few scenarios:
365
+ 1) Make sure you are using `redirect_with_session_to` if you need to redirect
366
+ and have your PandaPal session_key persisted server side.
367
+ 2) Use the `Authorization` header with `token={session_key}` to send your
368
+ PandaPal session info into api calls.
369
+ 3) If you use `link_to` and navigate in your LTI (apps that are not single page)
370
+ make sure you include the `link_nonce` like so:
371
+ ```ruby
372
+ link_to "Link Name", somewhere_else_path(session_token: link_nonce)
373
+ ```
374
+
375
+ ### Previous Safari Instructions
376
+ Safari is weird and you'll potentially run into issues getting `POST` requests to properly validate CSRF if you don't do the following:
345
377
 
346
378
  - Make sure you have both a `launch` controller and your normal controllers. The `launch` controller should call `before_action :validate_launch!` and then redirect
347
379
  to your other controller.
@@ -349,28 +381,47 @@ Safari is weird, and you'll potentially run into issues getting `POST` requests
349
381
 
350
382
  This will allow `PandaPal` to apply an iframe cookie fix that will allow CSRF validation to work.
351
383
 
384
+ ## Migrating Major Versions
352
385
 
353
- ### PandaPal 5
386
+ ### Upgrading to version 3
354
387
 
355
- It has been a constant struggle to force safari to store and allow
356
- access to a rails session while the application is embedded in Canvas.
388
+ Before upgrading save existing settings somewhere safe in case you need to
389
+ restore them for whatever reason.
357
390
 
358
- As of PandaPal 5, a persistent session is no longer required by panda_pal.
391
+ panda_pal v3 introduces an encrypted settings hash. This should provide more security in the case where a
392
+ customer may have gained access to Organization information in the database. Settings cannot be decrypted
393
+ without knowing the secret decryption key. For panda_pal, we are relying on the secret_key_base to be set
394
+ (`typically in config/secrets.yml`), if that is not available we are falling back to directly use
395
+ `ENV['SECRET_KEY_BASE']`. For production environments, `config/secrets.yml` should not have a plain-text secret,
396
+ it should be referencing an ENV variable. Make sure your secret is not plain-text committed to a repository!
359
397
 
360
- This means that safari will likely refuse to send info about your rails session
361
- back to the LTI, and the application will start up a new session each time the
362
- browser navigates. This likely means a new session each time the LTI launches.
398
+ The secret key is used to encrypt / decrypt the settings hash as necessary.
363
399
 
364
- You will want to watch out for a few scenarios:
400
+ Before upgrading to version 3, you should confirm that the secret key is set to a consistent value (if the
401
+ value is lost your settings will be hosed).
365
402
 
366
- 1) Make sure you are using "redirect_with_session_to" if you need to redirect
367
- and have your PandaPal session_key persisted server side.
368
- 2) Use the "Authorization" header with "token={session_key}" to send your
369
- PandaPal session info into api calls.
370
- 3) If you use link_to and navigate in your LTI (apps that are not single page)
371
- make sure you include an encrypted_session_key parameter in your links.
403
+ You should also rollback any local encryption you have done on the settings hash. The settings hash
404
+ should just be plainly visible when you upgrade to V3. Otherwise the panda_pal migrations will
405
+ probably fail.
372
406
 
373
- # Upgrading from PandaPal 4 to 5:
407
+ Once you have upgraded your gem, you will need to run migrations. Before doing that, I would store off your
408
+ unencrypted settings (just in case).
409
+
410
+ `rake db:migrate`
411
+
412
+ If all goes well, you should be set! Log into console, and verify that you can still access settings:
413
+
414
+ `PandaPal::Organization.first.settings`
415
+
416
+ should show unencrypted settings in your console.
417
+
418
+ If anything goes wrong, you should be able to rollback the change:
419
+
420
+ `rake db:rollback STEP=2`
421
+
422
+ If you need to give up on the change, just make sure to change your gem version for panda_pal to be < 3.
423
+
424
+ ### Upgrading from PandaPal 4 to 5:
374
425
 
375
426
  If your tool is setup according to a pretty standard pattern (see pace_plans,
376
427
  canvas_group_enrollment, etc), you shouldn't have to do anything to upgrade.
@@ -380,7 +431,7 @@ using "redirect_with_session_to".
380
431
 
381
432
  Here is an example launch / account controller setup, assuming an account launch.
382
433
 
383
- ```
434
+ ```ruby
384
435
  class LaunchController < ApplicationController
385
436
  # We don't verify CSRF on launch because the LTI launch is done via a POST
386
437
  # request, and Canvas wouldn't know anything about the CSRF
@@ -407,8 +458,3 @@ class AccountController < ApplicationController
407
458
  end
408
459
  end
409
460
  ```
410
-
411
- ## Running Specs:
412
- Initialize the Specs DB:
413
- `cd spec/dummy; bundle exec rake db:drop; bundle exec rake db:create; bundle exec rake db:schema:load`
414
- Then `bundle exec rspec`
@@ -2,23 +2,5 @@ require_dependency "panda_pal/application_controller"
2
2
 
3
3
  module PandaPal
4
4
  class LtiController < ApplicationController
5
- def tool_config
6
- if PandaPal.lti_environments.empty?
7
- render plain: 'Domains must be set in lti_environments'
8
- return
9
- end
10
- platform = PandaPal.lti_options.delete(:platform) || 'canvas.instructure.com'
11
- request_url = "#{request.scheme}://#{request.host_with_port}"
12
- case platform
13
- when 'canvas.instructure.com'
14
- xml_config = LtiXml::CanvasPlatform.new(platform, request_url, main_app)
15
- when 'bridgeapp.com'
16
- xml_config = LtiXml::BridgePlatform.new(platform, request_url, main_app)
17
- else
18
- render plain: 'platform must be set under lti_options'
19
- return
20
- end
21
- render xml: xml_config.xml
22
- end
23
5
  end
24
6
  end
@@ -0,0 +1,34 @@
1
+ require_dependency "panda_pal/application_controller"
2
+
3
+ module PandaPal
4
+ class LtiV1P0Controller < ApplicationController
5
+ def launch
6
+ current_session_data.merge!({
7
+ lti_version: 'v1p0',
8
+ lti_launch_placement: params[:launch_type],
9
+ launch_params: params.to_unsafe_h,
10
+ })
11
+
12
+ redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(params[:launch_type])}_url", route_context: main_app)
13
+ end
14
+
15
+ def tool_config
16
+ if PandaPal.lti_environments.empty?
17
+ render plain: 'Domains must be set in lti_environments'
18
+ return
19
+ end
20
+ platform = PandaPal.lti_options.delete(:platform) || 'canvas.instructure.com'
21
+ request_url = "#{request.scheme}://#{request.host_with_port}"
22
+ case platform
23
+ when 'canvas.instructure.com'
24
+ xml_config = LtiXml::CanvasPlatform.new(platform, request_url, main_app)
25
+ when 'bridgeapp.com'
26
+ xml_config = LtiXml::BridgePlatform.new(platform, request_url, main_app)
27
+ else
28
+ render plain: 'platform must be set under lti_options'
29
+ return
30
+ end
31
+ render xml: xml_config.xml
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,98 @@
1
+ require_dependency "panda_pal/application_controller"
2
+
3
+ module PandaPal
4
+ class LtiV1P3Controller < ApplicationController
5
+ skip_before_action :verify_authenticity_token
6
+
7
+ before_action :validate_launch!, only: [:resource_link_request]
8
+ around_action :switch_tenant, only: [:resource_link_request]
9
+
10
+ def login
11
+ current_session_data[:lti_oauth_nonce] = SecureRandom.uuid
12
+
13
+ @form_action = current_lti_platform.authentication_redirect_url
14
+ @method = :post
15
+ @form_data = {
16
+ scope: 'openid',
17
+ response_type: 'id_token',
18
+ response_mode: 'form_post',
19
+ prompt: 'none',
20
+ redirect_uri: params[:target_link_uri] || v1p3_resource_link_request_url,
21
+ client_id: params.require(:client_id),
22
+ login_hint: params.require(:login_hint),
23
+ lti_message_hint: params.require(:lti_message_hint),
24
+ state: current_session.session_key,
25
+ nonce: current_session_data[:lti_oauth_nonce]
26
+ }
27
+ end
28
+
29
+ def resource_link_request
30
+ # Redirect to correct region/env?
31
+ if params[:launch_type]
32
+ current_session_data.merge!({
33
+ lti_version: 'v1p3',
34
+ lti_launch_placement: params[:launch_type],
35
+ launch_params: @decoded_lti_jwt,
36
+ })
37
+
38
+ redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(params[:launch_type])}_url", route_context: main_app)
39
+ end
40
+ end
41
+
42
+ def tool_config
43
+ if PandaPal.lti_environments.empty?
44
+ render plain: 'Domains must be set in lti_environments'
45
+ return
46
+ end
47
+
48
+ platform = PandaPal.lti_options.delete(:platform) || 'canvas.instructure.com'
49
+ request_url = "#{request.scheme}://#{request.host_with_port}"
50
+ parsed_request_url = URI.parse(request_url)
51
+
52
+ mapped_placements = PandaPal.lti_paths.map do |k, opts|
53
+ opts = opts.dup
54
+ opts.delete(:route_helper_key)
55
+ opts.merge!({
56
+ placement: k,
57
+ target_link_uri: LaunchUrlHelpers.absolute_launch_url(k.to_sym, host: parsed_request_url, launch_handler: v1p3_resource_link_request_path),
58
+ })
59
+ opts
60
+ end
61
+
62
+ config_json = {
63
+ title: PandaPal.lti_options[:title],
64
+ scopes: [],
65
+ public_jwk_url: v1p3_public_jwks_url,
66
+ description: PandaPal.lti_options[:description] || 'PandaPal LTI',
67
+ target_link_uri: v1p3_resource_link_request_url, #app_url(:resource_link_request, request),
68
+ oidc_initiation_url: v1p3_oidc_login_url,
69
+ extensions: [{
70
+ platform: platform,
71
+ privacy_level: "public",
72
+ settings: {
73
+ placements: mapped_placements,
74
+ environments: PandaPal.lti_environments,
75
+ },
76
+ }],
77
+ custom_fields: PandaPal.lti_custom_params, # PandaPal.lti_options[:custom_fields],
78
+ }
79
+
80
+ render json: config_json
81
+ end
82
+
83
+ def public_jwks
84
+ render json: {
85
+ keys: [JWT::JWK.new(PandaPal.lti_private_key).export]
86
+ }
87
+ end
88
+
89
+ private
90
+
91
+ def auth_redirect_query
92
+ return unless params[:target_link_uri]&.include? 'platform_redirect_url='
93
+
94
+ platform_redirect_url = Rack::Utils.parse_query(URI(params[:target_link_uri]).query)&.dig('platform_redirect_url')
95
+ "?platform_redirect_url=#{platform_redirect_url}"
96
+ end
97
+ end
98
+ end