canvas_lti_third_party_cookies 0.3.3 → 0.4.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
2
  SHA256:
3
- metadata.gz: 8d622f96431568706549721c48a8cab10edd7f21790f739d612f2af1e649a686
4
- data.tar.gz: b0a008358a161f117a9c828975ac40e642a93af253cbd34b4ae3f2abf4890efb
3
+ metadata.gz: 8d232d4448ee62b75121b20f70143f70f75e0ea38a83408b3ed42bb51ce41247
4
+ data.tar.gz: 44605463e21036992d5e0dadc1ac7180384e3f4186917e0139fe586c9451c936
5
5
  SHA512:
6
- metadata.gz: 076670fa98327844ceee397d68e346179d326839b831454b80c94e68a1866c43a58873d9b1c1d6032258c21e0be38d44164bd831f7a4287d7a8d9daf519688ee
7
- data.tar.gz: 4d70af2daa01fc4fb262c01c186f581c81db4fe32c3bf7c025050fe4d9e149bd70c3430e7ce1f8f05cfaab245e3a4df4adae7a09b5b42ba6a13370dca237ed74
6
+ metadata.gz: 65a832d626b6062a7f4982417681a267bab2d49a6d070e4aef5e99b28d947fdb9d22ba73fc51badc1ceb4a9a7b59e45b3f08f8998fb1c2a286d6ec3bebd440f9
7
+ data.tar.gz: 47b3da8ab13ac2b3b9e9fae11ccaba87344b2cce53d98edbe6f96250c37c54e98210b6d0345180adbd57e1990516c89f6eed8a6a9304a8abcd2e9ae44a9fe4dc
data/README.md CHANGED
@@ -1,47 +1,11 @@
1
1
  # canvas_lti_third_party_cookies
2
2
 
3
- Safari blocks all 3rd-party cookies by default, which breaks LTI tools that rely on setting cookies when launched in an iframe. Instead, it exposes a new API for getting user-permitted access to set cookies from an iframe, which works though requires jumping through some fun hoops.
4
- See this article for a detailed explanation: https://community.canvaslms.com/t5/Developers-Group/Safari-13-1-and-LTI-Integration/ba-p/273051
3
+ Safari blocks all 3rd-party cookies by default, which breaks LTI tools that rely on setting cookies when launched in an iframe. Instead, it exposes a new API for getting user-permitted access to set cookies from an iframe, which unfortunately doesn't completely work for LTI use cases.
4
+ See this article for a detailed explanation of the Storage Access API and previous attempts to launch LTI tools in Safari: https://community.canvaslms.com/t5/Developers-Group/Safari-13-1-and-LTI-Integration/ba-p/273051
5
5
 
6
- This gem wraps the Safari cookie dance in a Rails engine that can be easily used with one before_action callback. It is built for use with Canvas,
7
- which is responsible for some parts of this cookie dance, including launching the tool in a full-window 1st-party context, and providing a
8
- redirect url for the relaunch after the full-window launch is complete.
9
-
10
- This gem won't work without being launched from Canvas, or a Tool Consumer that implements the same `window.postMessage` listener as Canvas
6
+ The current workaround that this gem implements is to relaunch the tool in a new tab, new window, or popup window, where it can set first-party cookies to it's heart's content. This gem won't work without being launched from Canvas, or a Tool Consumer that implements the same `window.postMessage` listener as Canvas
11
7
  does here: https://github.com/instructure/canvas-lms/blob/master/public/javascripts/lti/post_message/requestFullWindowLaunch.js
12
8
 
13
- ## Usage
14
-
15
- Choose the Rails controller action that's used to launch your tool and set cookies. Set the before_action callback
16
- below to run on that action, and pass the data needed.
17
-
18
- * the `launch_url` parameter is required, which should be the route
19
- that launches the tool.
20
- * the `launch_params` parameter is optional, and should contain
21
- all needed query parameters that the tool requires to launch.
22
- * the `launch_data` parameter is optional, and should contain
23
- all needed form data that the tool requires to launch.
24
-
25
- Usually, only query parameters *or* form data is needed, not both.
26
-
27
- ```ruby
28
- include CanvasLtiThirdPartyCookies::SafariLaunch
29
- #...
30
- before_action -> {
31
- handle_safari_launch(launch_url: action_url, launch_params: { foo: bar }, launch_data: { foo: baz })
32
- }
33
- ```
34
-
35
- This will launch the tool multiple times, and also redirect the user back to Canvas when needed. For more information on the detailed tool
36
- launches, see the comments in `app/controllers/concerns/canvas_lti_third_party_cookies/safari_launch.rb`.
37
-
38
- Note that the tool will be relaunched from within this method once Storage Access is granted and pass all parameters from the previous
39
- Canvas launch, which will break JWT nonce verification since it will detect the nonce has already been used.
40
-
41
- To combat this, this gem provides the `should_ignore_nonce?` method so that your tool can ignore the nonce verification for that
42
- specific launch. Normally, ignoring a duplicate nonce can lead to replay attacks. This method will only return true if the request's
43
- `Referer` header matches the tool's domain, which only happens in this last internal redirect.
44
-
45
9
  ## Installation
46
10
  Add this line to your application's Gemfile:
47
11
 
@@ -55,6 +19,25 @@ And then execute:
55
19
  $ bundle install
56
20
  ```
57
21
 
22
+ ## Usage
23
+
24
+ Choose the Rails controller action that's used to launch your tool and set cookies. Set the before_action callback
25
+ below to run on that action, and pass the data needed.
26
+
27
+ * `placement`: (required) Should be the Canvas placement that the tool was launched from, taken from the decoded id_token's `https://www.instructure.com/placement` claim.
28
+ * `window_type`: (optional) Set to `:new_window` to open the tool in a new tab or window, or to `:popup` to open in a popup window.Defaults to `:new_window`.
29
+ * `width`: (optional) The width the popup window should be, in px. User has the discretion to ignore this. Only valid with window_type: popup. Defaults to 800px.
30
+ * `height`: (optional) The height the popup window should be, in px. User has the discretion to ignore this. Only valid with window_type: popup. Defaults to 600px.
31
+
32
+ ```ruby
33
+ include CanvasLtiThirdPartyCookies::SafariLaunch
34
+ #...
35
+ before_action -> {
36
+ placement = decoded_id_token['https://www.instructure.com/placement']
37
+ handle_safari_launch(placement: placement, window_type: :popup)
38
+ }
39
+ ```
40
+
58
41
  ## Testing
59
42
 
60
43
  ```bash
@@ -5,72 +5,56 @@ module CanvasLtiThirdPartyCookies::SafariLaunch
5
5
 
6
6
  # this needs to be called as a before_action on the route that launches the tool
7
7
  # and the tool is required to pass some parameters to this method.
8
- # the `launch_url` parameter is required, which should be the route
9
- # that launches the tool.
10
- # the `launch_params` parameter is optional, and should contain
11
- # all needed query parameters that the tool requires to launch.
12
- # the `launch_data` parameter is optional, and should contain
13
- # all needed form data that the tool requires to launch.
8
+ #
9
+ # `placement`: (required) Should be the Canvas placement that
10
+ # the tool was launched from, taken from the decoded id_token's
11
+ # `https://www.instructure.com/placement` claim.
12
+ #
13
+ # `window_type`: (optional) Set to `:new_window` to open the tool in a
14
+ # new tab or window, or to `:popup` to open in a popup window.
15
+ # Defaults to `:new_window`.
16
+ #
17
+ # `width`: (optional) The width the popup window should be, in px. User
18
+ # has the discretion to ignore this. Only valid with window_type: popup.
19
+ # Defaults to 800px.
20
+ #
21
+ # `height`: (optional) The height the popup window should be, in px. User
22
+ # has the discretion to ignore this. Only valid with window_type: popup.
23
+ # Defaults to 600px.
24
+ #
14
25
  # example:
15
26
  # include CanvasLtiThirdPartyCookies::SafariLaunch
16
27
  # ...
17
28
  # before_action -> {
18
- # handle_safari_launch(launch_url: action_url, launch_params: { foo: bar }, launch_data: { foo: baz })
29
+ # placement = decoded_id_token['https://www.instructure.com/placement']
30
+ # handle_safari_launch(placement: placement, window_type: :popup)
19
31
  # }
20
- def handle_safari_launch(launch_url:, launch_params: {}, launch_data: {})
32
+ def handle_safari_launch(placement:, window_type: :new_window, width: nil, height: nil)
33
+ raise ArgumentError.new("window_type must be either :new_window or :popup") unless [:new_window, :popup].include? window_type
21
34
  return unless is_safari?
35
+ return if is_full_window_launch?
22
36
 
23
- # Safari launch #4: Storage Access has been granted,
24
- # so launch the app normally. Note that this is not an actual LTI launch, but
25
- # just opaquely passing on the data from launch #3.
26
- return if params[:storage_access_status].present?
27
-
28
- # Safari launch #2: Full-window launch, solely for first-party user interaction.
29
- # During a full-window launch, Canvas provides a :platform_redirect_url that
30
- # will launch the tool again within an iframe in Canvas. (#3)
31
- if params[:platform_redirect_url].present?
32
- return render(
33
- 'canvas_lti_third_party_cookies/full_window_launch',
34
- locals: { platform_redirect_url: params[:platform_redirect_url] }
35
- )
36
- end
37
-
38
- # Safari launch #1: request Storage Access, then relaunch the tool. (#4)
39
- # If request fails, request a full window launch instead. (#2)
40
- # Safari launch #3: Relaunched by Canvas after full-window launch,
41
- # request Storage Access and then relaunch the tool. (#4)
42
- # Pass along any parameters provided by the tool that are needed to launch correctly,
43
- # and tell the tool that it has Storage Access.
44
37
  render(
45
- 'canvas_lti_third_party_cookies/request_storage_access',
38
+ 'canvas_lti_third_party_cookies/prep_for_full_window_launch',
46
39
  locals: {
47
- launch_url: launch_url,
48
- relaunch_url: relaunch_url(launch_url, launch_params),
49
- launch_data: launch_data.merge({ storage_access_status: "granted"})
40
+ launch_url: request.base_url + request.fullpath,
41
+ placement: placement,
42
+ window_type: window_type.to_s,
43
+ width: width || 800,
44
+ height: height || 600
50
45
  }
51
46
  )
52
47
  end
53
48
 
54
- # Safari launch #4 (described above) is actually an internal opaque redirect of launch #3
55
- # and not a real Canvas LTI launch, so the id_token (and specifically the nonce inside)
56
- # is exactly the same. Normally, ignoring the nonce is a Bad Idea since it can allow
57
- # replay attacks, but for this specific situation (the request is an internal redirect)
58
- # it's a sufficient hack.
59
- def should_ignore_nonce?
60
- referer = URI.parse(request.referer)
61
- is_safari? && params[:storage_access_status] == "granted" && referer.host == request.host && referer.port == request.port
62
- end
63
-
64
49
  private
65
50
 
51
+ def is_full_window_launch?
52
+ params[:full_win_launch_requested].present?
53
+ end
54
+
66
55
  def is_safari?
67
56
  browser = Browser.new(request.headers["User-Agent"])
68
57
  # detect both MacOS and iOS Safari
69
58
  browser.safari? || (browser.webkit? && browser.platform.ios?)
70
59
  end
71
-
72
- def relaunch_url(launch_url, launch_params)
73
- return launch_url if launch_params.empty?
74
- "#{launch_url}?#{launch_params.to_query}"
75
- end
76
60
  end
@@ -1,36 +1,25 @@
1
1
  <%= javascript_tag do -%>
2
- const requestStorageAccess = () => {
3
- document
4
- .requestStorageAccess()
5
- .then(() => redirectToSetCookies())
6
- .catch(() => requestFullWindowLaunch());
7
- };
8
-
9
2
  const requestFullWindowLaunch = () => {
3
+ console.log("clicked")
10
4
  window.parent.postMessage(
11
5
  {
12
6
  messageType: "requestFullWindowLaunch",
13
- data: "<%= launch_url %>",
7
+ data: {
8
+ url: "<%= launch_url %>",
9
+ placement: "<%= placement %>",
10
+ launchType: "<%= window_type %>",
11
+ launchOptions: {
12
+ width: <%= width %>,
13
+ height: <%= height %>
14
+ }
15
+ }
14
16
  },
15
17
  "*"
16
18
  );
17
19
  };
18
-
19
- const redirectToSetCookies = () => {
20
- const form = document.getElementById("relaunch");
21
- form.submit();
22
- };
23
-
24
20
  document.addEventListener("DOMContentLoaded", () => {
25
- document.getElementById("request").addEventListener("click", requestStorageAccess)
26
- document
27
- .hasStorageAccess()
28
- .then((hasStorageAccess) => {
29
- if (hasStorageAccess) {
30
- redirectToSetCookies();
31
- }
32
- })
33
- .catch((err) => console.error(err));
21
+ console.log("loaded")
22
+ document.getElementById("request").addEventListener("click", requestFullWindowLaunch)
34
23
  });
35
24
  <% end %>
36
25
  <style type="text/css">
@@ -40,7 +29,7 @@
40
29
  height: 100%;
41
30
  font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif;
42
31
  font-size: 1.1em;
43
- padding: 0 75px 0 75px;
32
+ padding: 0 24px;
44
33
  }
45
34
 
46
35
  .flex-item {
@@ -48,8 +37,8 @@
48
37
  text-align: center;
49
38
  }
50
39
 
51
- .first {
52
- margin-top: 50px;
40
+ .flex-container div:first-child {
41
+ margin-top: 24px;
53
42
  }
54
43
 
55
44
  p {
@@ -87,7 +76,7 @@
87
76
  </style>
88
77
 
89
78
  <div class="flex-container">
90
- <div class="flex-item first">
79
+ <div class="flex-item">
91
80
  <img id="safari-logo" src="https://upload.wikimedia.org/wikipedia/commons/5/52/Safari_browser_logo.svg" alt="Safari Logo" />
92
81
  </div>
93
82
 
@@ -96,20 +85,15 @@
96
85
  </div>
97
86
 
98
87
  <div class="flex-item">
99
- <p>Safari requires your interaction with this app before logging you in.</p>
100
- <p>A dialog may appear asking you to allow this app to use cookies while browsing Canvas.</p>
101
- <p>For the best experience, click Allow.</p>
102
- <p>A dialog may also appear asking you to navigate somewhere else.</p>
103
- <p>If it does, save your work first and then click Leave Page.</p>
88
+ <p>Safari blocks third-party cookies by default, which this app relies on for sign-in features.</p>
89
+ <p>Launching this app in a <%if window_type == "popup"%>popup window<%else%>new tab<%end%>, separate from Canvas, will allow the app to set its own cookies.</p>
90
+ <%if window_type == "popup"%>
91
+ <p>If the app doesn't appear, make sure Safari is not blocking popups.</p>
92
+ <%end%>
104
93
  </div>
105
94
 
106
95
  <div class="flex-item">
107
- <button id="request">Continue to App</button>
96
+ <button id="request">Open <%if window_type == "popup"%>Popup<%else%>in New Tab<%end%></button>
108
97
  </div>
109
98
  <div>
110
- </div>
111
- <form id="relaunch" method="POST" action="<%= relaunch_url %>">
112
- <% launch_data.each do |key, value| -%>
113
- <%= hidden_field_tag key, value %>
114
- <% end -%>
115
- </form>
99
+ </div>
@@ -1,3 +1,3 @@
1
1
  module CanvasLtiThirdPartyCookies
2
- VERSION = '0.3.3'
2
+ VERSION = '0.4.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canvas_lti_third_party_cookies
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Xander Moffatt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-10 00:00:00.000000000 Z
11
+ date: 2021-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -76,8 +76,7 @@ files:
76
76
  - README.md
77
77
  - Rakefile
78
78
  - app/controllers/concerns/canvas_lti_third_party_cookies/safari_launch.rb
79
- - app/views/canvas_lti_third_party_cookies/full_window_launch.erb
80
- - app/views/canvas_lti_third_party_cookies/request_storage_access.erb
79
+ - app/views/canvas_lti_third_party_cookies/prep_for_full_window_launch.erb
81
80
  - app/views/layouts/application.html.erb
82
81
  - lib/canvas_lti_third_party_cookies.rb
83
82
  - lib/canvas_lti_third_party_cookies/engine.rb
@@ -1,80 +0,0 @@
1
- <%= javascript_tag do -%>
2
- document.addEventListener("DOMContentLoaded", () => {
3
- document.getElementById("redirect").addEventListener("click", () => {
4
- window.location.replace("<%= platform_redirect_url %>");
5
- });
6
- });
7
- <% end %>
8
- <style type="text/css">
9
- .flex-container {
10
- display: flex;
11
- flex-direction: column;
12
- height: 100%;
13
- font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif;
14
- font-size: 1.1em;
15
- padding: 0 75px 0 75px;
16
- }
17
-
18
- .flex-item {
19
- margin-bottom: 10px;
20
- text-align: center;
21
- }
22
-
23
- .first {
24
- margin-top: 50px;
25
- }
26
-
27
- p {
28
- margin: 0 0 0.5em 0;
29
- }
30
-
31
- button {
32
- background: #008EE2;
33
- color: #ffffff;
34
- border: 1px solid;
35
- border-color: #0079C1;
36
- border-radius: 3px;
37
- transition: background-color 0.2s ease-in-out;
38
- display: inline-block;
39
- position: relative;
40
- padding: 8px 14px;
41
- margin-bottom: 0;
42
- font-size: 16px;
43
- font-size: 1rem;
44
- line-height: 20px;
45
- text-align: center;
46
- vertical-align: middle;
47
- cursor: pointer;
48
- text-decoration: none;
49
- overflow: hidden;
50
- text-shadow: none;
51
- -webkit-user-select: none;
52
- -moz-user-select: none;
53
- }
54
-
55
- #safari-logo {
56
- width: 100px;
57
- height: 100px;
58
- }
59
- </style>
60
-
61
- <div class="flex-container">
62
- <div class="flex-item first">
63
- <img id="safari-logo" src="https://upload.wikimedia.org/wikipedia/commons/5/52/Safari_browser_logo.svg" alt="Safari Logo" />
64
- </div>
65
-
66
- <div class="flex-item">
67
- <strong>It looks like you are using Safari.</strong>
68
- </div>
69
-
70
- <div class="flex-item">
71
- <p>Occasionally, Safari requires you to launch this app outside of Canvas before logging in.</p>
72
- <p>This setup is now complete, and Canvas can now relaunch this app.</p>
73
- <p>In some cases, you may need to relaunch this app yourself.</p>
74
- </div>
75
-
76
- <div class="flex-item">
77
- <button id="redirect">Relaunch App in Canvas</button>
78
- </div>
79
- <div>
80
- </div>