atomic_lti 1.1.0 → 1.2.0
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 +27 -7
- data/app/lib/atomic_lti/auth_token.rb +2 -2
- data/app/lib/atomic_lti/config.rb +4 -4
- data/app/lib/atomic_lti/deep_linking.rb +6 -6
- data/app/lib/atomic_lti/definitions.rb +6 -4
- data/app/lib/atomic_lti/exceptions.rb +14 -8
- data/app/lib/atomic_lti/services/names_and_roles.rb +1 -1
- data/lib/atomic_lti/open_id_middleware.rb +30 -28
- data/lib/atomic_lti/version.rb +1 -1
- data/lib/atomic_lti.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7381dbf24886137688f92e773144dd5194746ad605b426d743221a7ba2ec25ba
|
4
|
+
data.tar.gz: ebc4d85849a13db6df43c0b6d906af6fe32b453fbbf3cf34934ff647a64d68c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 129f9b79ae60f4bb7a00db8199c83ee0348a6d2484280f895736e89e9d26cf3531fe9b557072cd07011234675ce658a8dfe6e1227e81442c2cdbcb2dccd4e685
|
7
|
+
data.tar.gz: 955d5ff4465790e5106d9b551fdcd8b52b8a93cd41c919b16ac1d2c6b31cfd97f2637b8bd64aa5b26cd702e54c90ecb578749c9bc6ce8486057cb163d5978027
|
data/README.md
CHANGED
@@ -1,22 +1,42 @@
|
|
1
1
|
# AtomicLti
|
2
2
|
Atomic LTI implements the LTI Advantage specification.
|
3
3
|
|
4
|
-
##
|
5
|
-
Add
|
4
|
+
## Installation
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem "atomic_lti"
|
9
|
+
```
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
```bash
|
13
|
+
$ bundle
|
14
|
+
```
|
6
15
|
|
7
|
-
|
16
|
+
Or install it yourself as:
|
17
|
+
```bash
|
18
|
+
$ gem install atomic_tenant
|
19
|
+
```
|
8
20
|
|
9
|
-
|
10
|
-
|
21
|
+
Then install the migrations:
|
22
|
+
./bin/rails atomic_lti:install:migrations
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
Create a new initializer:
|
26
|
+
```
|
27
|
+
config/initializers/atomic_lti.rb
|
28
|
+
```
|
11
29
|
|
12
30
|
with the following contents. Adjust paths as needed.
|
13
31
|
|
14
|
-
|
32
|
+
```
|
15
33
|
AtomicLti.oidc_init_path = "/oidc/init"
|
16
34
|
AtomicLti.oidc_redirect_path = "/oidc/redirect"
|
17
35
|
AtomicLti.target_link_path_prefixes = ["/lti_launches"]
|
18
36
|
AtomicLti.default_deep_link_path = "/lti_launches"
|
19
37
|
AtomicLti.jwt_secret = Rails.application.secrets.auth0_client_secret
|
20
38
|
AtomicLti.scopes = AtomicLti::Definitions.scopes.join(" ")
|
21
|
-
|
39
|
+
```
|
22
40
|
|
41
|
+
## License
|
42
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -13,7 +13,7 @@ module AtomicLti
|
|
13
13
|
payload["aud"] = aud || Rails.application.secrets.auth0_client_id
|
14
14
|
JWT.encode(
|
15
15
|
payload,
|
16
|
-
AtomicLti.jwt_secret,
|
16
|
+
secret || AtomicLti.jwt_secret,
|
17
17
|
ALGORITHM,
|
18
18
|
header_fields,
|
19
19
|
)
|
@@ -26,7 +26,7 @@ module AtomicLti
|
|
26
26
|
def self.decode(token, secret = nil, validate = true, algorithm = ALGORITHM)
|
27
27
|
JWT.decode(
|
28
28
|
token,
|
29
|
-
AtomicLti.jwt_secret,
|
29
|
+
secret || AtomicLti.jwt_secret,
|
30
30
|
validate,
|
31
31
|
{ algorithm: algorithm },
|
32
32
|
)
|
@@ -33,12 +33,12 @@ module AtomicLti
|
|
33
33
|
# Converts old lti into lti advantage
|
34
34
|
# NOTE this is a work in progress and may not correctly convert all LTI configs.
|
35
35
|
def self.lti_to_lti_advantage(jwk, domain, args = {})
|
36
|
-
raise ::Exceptions::LtiConfigMissing, "Please provide an LTI launch url" if args[:launch_url].blank?
|
37
|
-
raise ::Exceptions::LtiConfigMissing, "Please provide an LTI secure launch url" if args[:secure_launch_url].blank?
|
36
|
+
raise AtomicLti::Exceptions::LtiConfigMissing, "Please provide an LTI launch url" if args[:launch_url].blank?
|
37
|
+
raise AtomicLti::Exceptions::LtiConfigMissing, "Please provide an LTI secure launch url" if args[:secure_launch_url].blank?
|
38
38
|
|
39
39
|
if args[:content_migration].present?
|
40
|
-
raise Exceptions::LtiConfigMissing, "Please provide an IMS export url" if args[:export_url].blank?
|
41
|
-
raise Exceptions::LtiConfigMissing, "Please provide an IMS import url" if args[:import_url].blank?
|
40
|
+
raise AtomicLti::Exceptions::LtiConfigMissing, "Please provide an IMS export url" if args[:export_url].blank?
|
41
|
+
raise AtomicLti::Exceptions::LtiConfigMissing, "Please provide an IMS import url" if args[:import_url].blank?
|
42
42
|
end
|
43
43
|
|
44
44
|
{
|
@@ -2,15 +2,15 @@ module AtomicLti
|
|
2
2
|
|
3
3
|
module DeepLinking
|
4
4
|
|
5
|
-
#
|
6
|
-
#
|
5
|
+
# ###########################################################
|
6
|
+
# Create a jwt to sign a response to the platform
|
7
7
|
def self.create_deep_link_jwt(iss:, deployment_id:, content_items:, deep_link_claim_data: nil)
|
8
8
|
deployment = AtomicLti::Deployment.find_by(iss: iss, deployment_id: deployment_id)
|
9
9
|
|
10
|
-
raise AtomicLti::Exceptions::NoLTIDeployment(iss, deployment_id) if deployment.nil?
|
10
|
+
raise AtomicLti::Exceptions::NoLTIDeployment.new(iss: iss, deployment_id: deployment_id) if deployment.nil?
|
11
11
|
|
12
12
|
install = deployment.install
|
13
|
-
raise AtomicLti::Exceptions::NoLTIInstall(iss, deployment_id) if install.nil?
|
13
|
+
raise AtomicLti::Exceptions::NoLTIInstall.new(iss: iss, deployment_id: deployment_id) if install.nil?
|
14
14
|
|
15
15
|
payload = {
|
16
16
|
iss: install.client_id, # A unique identifier for the entity that issued the JWT
|
@@ -23,7 +23,7 @@ module AtomicLti
|
|
23
23
|
AtomicLti::Definitions::MESSAGE_TYPE => "LtiDeepLinkingResponse",
|
24
24
|
AtomicLti::Definitions::LTI_VERSION => "1.3.0",
|
25
25
|
AtomicLti::Definitions::DEPLOYMENT_ID => deployment_id,
|
26
|
-
AtomicLti::Definitions::CONTENT_ITEM_CLAIM => content_items
|
26
|
+
AtomicLti::Definitions::CONTENT_ITEM_CLAIM => content_items,
|
27
27
|
}
|
28
28
|
|
29
29
|
if deep_link_claim_data.present?
|
@@ -33,4 +33,4 @@ module AtomicLti
|
|
33
33
|
AtomicLti::Authorization.sign_tool_jwt(payload)
|
34
34
|
end
|
35
35
|
end
|
36
|
-
end
|
36
|
+
end
|
@@ -134,7 +134,7 @@ module AtomicLti
|
|
134
134
|
|
135
135
|
OBSERVER_ROLES = [
|
136
136
|
MENTOR_INSTITUTION_ROLE,
|
137
|
-
#NON_CREDIT_LEARNER,
|
137
|
+
# NON_CREDIT_LEARNER,
|
138
138
|
].freeze
|
139
139
|
|
140
140
|
def self.lms_host(payload)
|
@@ -143,7 +143,9 @@ module AtomicLti
|
|
143
143
|
else
|
144
144
|
payload.dig(AtomicLti::Definitions::LAUNCH_PRESENTATION, "return_url")
|
145
145
|
end
|
146
|
-
|
146
|
+
|
147
|
+
host = "https://#{host}" unless host&.start_with?("http")
|
148
|
+
URI.parse(host).host
|
147
149
|
end
|
148
150
|
|
149
151
|
def self.lms_url(payload)
|
@@ -155,14 +157,14 @@ module AtomicLti
|
|
155
157
|
end
|
156
158
|
|
157
159
|
def self.names_and_roles_launch?(payload)
|
158
|
-
return false unless payload[AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM]
|
160
|
+
return false unless payload[AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM].present?
|
159
161
|
|
160
162
|
payload[AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM]["service_versions"] ==
|
161
163
|
AtomicLti::Definitions::NAMES_AND_ROLES_SERVICE_VERSIONS
|
162
164
|
end
|
163
165
|
|
164
166
|
def self.assignment_and_grades_launch?(payload)
|
165
|
-
payload[AtomicLti::Definitions::AGS_CLAIM]
|
167
|
+
payload[AtomicLti::Definitions::AGS_CLAIM].present?
|
166
168
|
end
|
167
169
|
|
168
170
|
end
|
@@ -35,26 +35,29 @@ module AtomicLti
|
|
35
35
|
class RateLimitError < AtomicLtiException
|
36
36
|
end
|
37
37
|
|
38
|
+
class LtiConfigMissing < AtomicLtiException
|
39
|
+
end
|
40
|
+
|
38
41
|
class InvalidLTIVersion < AtomicLtiException
|
39
|
-
def initialize(msg="Invalid LTI version")
|
42
|
+
def initialize(msg = "Invalid LTI version")
|
40
43
|
super(msg)
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
44
47
|
class NoLTIVersion < AtomicLtiException
|
45
|
-
def initialize(msg="No LTI Version provided")
|
48
|
+
def initialize(msg = "No LTI Version provided")
|
46
49
|
super(msg)
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
50
53
|
class NoLTIToken < AtomicLtiException
|
51
|
-
def initialize(msg="No LTI token provided")
|
54
|
+
def initialize(msg = "No LTI token provided")
|
52
55
|
super(msg)
|
53
56
|
end
|
54
57
|
end
|
55
58
|
|
56
59
|
class InvalidLTIToken < AtomicLtiException
|
57
|
-
def initialize(msg="Invalid LTI token provided")
|
60
|
+
def initialize(msg = "Invalid LTI token provided")
|
58
61
|
super(msg)
|
59
62
|
end
|
60
63
|
end
|
@@ -65,21 +68,24 @@ module AtomicLti
|
|
65
68
|
|
66
69
|
class NoLTIDeployment < AtomicLtiNotFoundException
|
67
70
|
def initialize(iss:, deployment_id:)
|
68
|
-
msg="No LTI Deployment found for iss: #{iss} and deployment_id #{deployment_id}"
|
71
|
+
msg = "No LTI Deployment found for iss: #{iss} and deployment_id #{deployment_id}"
|
69
72
|
super(msg)
|
70
73
|
end
|
71
74
|
end
|
72
75
|
|
73
76
|
class NoLTIInstall < AtomicLtiNotFoundException
|
74
77
|
def initialize(iss:, deployment_id:)
|
75
|
-
msg="No LTI Install found for iss: #{iss} and deployment_id #{deployment_id}"
|
78
|
+
msg = "No LTI Install found for iss: #{iss} and deployment_id #{deployment_id}"
|
76
79
|
super(msg)
|
77
80
|
end
|
78
81
|
end
|
79
82
|
|
80
83
|
class NoLTIPlatform < AtomicLtiNotFoundException
|
81
|
-
def initialize(iss:, deployment_id:)
|
82
|
-
msg="No LTI Platform associated with the LTI Install. iss: #{iss}
|
84
|
+
def initialize(iss:, deployment_id: nil)
|
85
|
+
msg = "No LTI Platform associated with the LTI Install. iss: #{iss}"
|
86
|
+
if iss && deployment_id
|
87
|
+
msg = "No LTI Platform associated with the LTI Install. iss: #{iss} and deployment_id #{deployment_id}"
|
88
|
+
end
|
83
89
|
super(msg)
|
84
90
|
end
|
85
91
|
end
|
@@ -6,13 +6,13 @@ module AtomicLti
|
|
6
6
|
|
7
7
|
def init_paths
|
8
8
|
[
|
9
|
-
AtomicLti.oidc_init_path
|
9
|
+
AtomicLti.oidc_init_path,
|
10
10
|
]
|
11
11
|
end
|
12
12
|
|
13
13
|
def redirect_paths
|
14
14
|
[
|
15
|
-
AtomicLti.oidc_redirect_path
|
15
|
+
AtomicLti.oidc_redirect_path,
|
16
16
|
]
|
17
17
|
end
|
18
18
|
|
@@ -47,7 +47,7 @@ module AtomicLti
|
|
47
47
|
target_link_uri = lti_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM] ||
|
48
48
|
File.join("#{uri.scheme}://#{uri.host}", AtomicLti.default_deep_link_path)
|
49
49
|
|
50
|
-
|
50
|
+
redirect_params = {
|
51
51
|
state: request.params["state"],
|
52
52
|
id_token: request.params["id_token"],
|
53
53
|
}
|
@@ -63,6 +63,7 @@ module AtomicLti
|
|
63
63
|
|
64
64
|
def matches_redirect?(request)
|
65
65
|
raise AtomicLti::Exceptions::ConfigurationError.new("AtomicLti.oidc_redirect_path is not configured") if AtomicLti.oidc_redirect_path.blank?
|
66
|
+
|
66
67
|
redirect_uri = URI.parse(AtomicLti.oidc_redirect_path)
|
67
68
|
redirect_path_params = if redirect_uri.query
|
68
69
|
CGI.parse(redirect_uri.query)
|
@@ -99,13 +100,13 @@ module AtomicLti
|
|
99
100
|
update_deployment(id_token: decoded_jwt)
|
100
101
|
update_lti_context(id_token: decoded_jwt)
|
101
102
|
|
102
|
-
errors = decoded_jwt.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM,
|
103
|
-
if errors.present? && !errors[
|
103
|
+
errors = decoded_jwt.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "errors")
|
104
|
+
if errors.present? && !errors["errors"].empty?
|
104
105
|
Rails.logger.error("Detected errors in lti launch: #{errors}, id_token: #{id_token}")
|
105
106
|
end
|
106
107
|
|
107
|
-
env[
|
108
|
-
env[
|
108
|
+
env["atomic.validated.decoded_id_token"] = decoded_jwt
|
109
|
+
env["atomic.validated.id_token"] = id_token
|
109
110
|
|
110
111
|
@app.call(env)
|
111
112
|
else
|
@@ -134,18 +135,19 @@ module AtomicLti
|
|
134
135
|
protected
|
135
136
|
|
136
137
|
def update_platform_instance(id_token:)
|
137
|
-
if id_token[AtomicLti::Definitions::TOOL_PLATFORM_CLAIM].present? &&
|
138
|
-
|
139
|
-
|
140
|
-
|
138
|
+
if id_token[AtomicLti::Definitions::TOOL_PLATFORM_CLAIM].present? &&
|
139
|
+
id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "guid").present?
|
140
|
+
name = id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "name")
|
141
|
+
version = id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "version")
|
142
|
+
product_family_code = id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "product_family_code")
|
141
143
|
|
142
144
|
AtomicLti::PlatformInstance.create_with(
|
143
145
|
name: name,
|
144
146
|
version: version,
|
145
147
|
product_family_code: product_family_code,
|
146
148
|
).find_or_create_by!(
|
147
|
-
iss: id_token[
|
148
|
-
guid: id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM,
|
149
|
+
iss: id_token["iss"],
|
150
|
+
guid: id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "guid"),
|
149
151
|
).update!(
|
150
152
|
name: name,
|
151
153
|
version: version,
|
@@ -164,7 +166,7 @@ module AtomicLti
|
|
164
166
|
|
165
167
|
AtomicLti::Install.find_or_create_by!(
|
166
168
|
iss: iss,
|
167
|
-
client_id: client_id
|
169
|
+
client_id: client_id,
|
168
170
|
)
|
169
171
|
else
|
170
172
|
Rails.logger.info("No client_id recieved: #{id_token}")
|
@@ -172,13 +174,14 @@ module AtomicLti
|
|
172
174
|
end
|
173
175
|
|
174
176
|
def update_lti_context(id_token:)
|
175
|
-
if id_token[AtomicLti::Definitions::CONTEXT_CLAIM].present? &&
|
176
|
-
|
177
|
+
if id_token[AtomicLti::Definitions::CONTEXT_CLAIM].present? &&
|
178
|
+
id_token[AtomicLti::Definitions::CONTEXT_CLAIM]["id"].present?
|
179
|
+
iss = id_token["iss"]
|
177
180
|
deployment_id = id_token[AtomicLti::Definitions::DEPLOYMENT_ID]
|
178
|
-
context_id = id_token[AtomicLti::Definitions::CONTEXT_CLAIM][
|
179
|
-
label = id_token[AtomicLti::Definitions::CONTEXT_CLAIM][
|
180
|
-
title = id_token[AtomicLti::Definitions::CONTEXT_CLAIM][
|
181
|
-
types = id_token[AtomicLti::Definitions::CONTEXT_CLAIM][
|
181
|
+
context_id = id_token[AtomicLti::Definitions::CONTEXT_CLAIM]["id"]
|
182
|
+
label = id_token[AtomicLti::Definitions::CONTEXT_CLAIM]["label"]
|
183
|
+
title = id_token[AtomicLti::Definitions::CONTEXT_CLAIM]["title"]
|
184
|
+
types = id_token[AtomicLti::Definitions::CONTEXT_CLAIM]["type"]
|
182
185
|
|
183
186
|
AtomicLti::Context.create_with(
|
184
187
|
label: label,
|
@@ -187,7 +190,7 @@ module AtomicLti
|
|
187
190
|
).find_or_create_by!(
|
188
191
|
iss: iss,
|
189
192
|
deployment_id: deployment_id,
|
190
|
-
context_id: context_id
|
193
|
+
context_id: context_id,
|
191
194
|
).update!(
|
192
195
|
label: label,
|
193
196
|
title: title,
|
@@ -206,17 +209,16 @@ module AtomicLti
|
|
206
209
|
|
207
210
|
Rails.logger.debug("Associating deployment: #{iss}/#{deployment_id} with client_id: iss: #{iss} / client_id: #{client_id} / platform_guid: #{platform_guid}")
|
208
211
|
|
209
|
-
|
210
|
-
|
211
|
-
.create_with(
|
212
|
+
AtomicLti::Deployment.
|
213
|
+
create_with(
|
212
214
|
client_id: client_id,
|
213
|
-
platform_guid: platform_guid
|
215
|
+
platform_guid: platform_guid,
|
214
216
|
).find_or_create_by!(
|
215
217
|
iss: iss,
|
216
|
-
deployment_id: deployment_id
|
218
|
+
deployment_id: deployment_id,
|
217
219
|
).update!(
|
218
220
|
client_id: client_id,
|
219
|
-
platform_guid: platform_guid
|
221
|
+
platform_guid: platform_guid,
|
220
222
|
)
|
221
223
|
end
|
222
224
|
|
@@ -245,7 +247,7 @@ module AtomicLti
|
|
245
247
|
def build_oidc_response(request, state, nonce, redirect_uri)
|
246
248
|
platform = AtomicLti::Platform.find_by(iss: request.params["iss"])
|
247
249
|
if !platform
|
248
|
-
raise AtomicLti::Exceptions::NoLTIPlatform
|
250
|
+
raise AtomicLti::Exceptions::NoLTIPlatform(iss: request.params["iss"])
|
249
251
|
end
|
250
252
|
|
251
253
|
uri = URI.parse(platform.oidc_url)
|
data/lib/atomic_lti/version.rb
CHANGED
data/lib/atomic_lti.rb
CHANGED
@@ -3,7 +3,7 @@ require "atomic_lti/engine"
|
|
3
3
|
require "atomic_lti/open_id_middleware"
|
4
4
|
require "atomic_lti/error_handling_middleware"
|
5
5
|
require_relative "../app/lib/atomic_lti/definitions"
|
6
|
-
|
6
|
+
require_relative "../app/lib/atomic_lti/exceptions"
|
7
7
|
module AtomicLti
|
8
8
|
|
9
9
|
# Set this to true to scope context_id's to the ISS rather than
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: atomic_lti
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Petro
|
8
8
|
- Justin Ball
|
9
|
+
- Nick Benoit
|
9
10
|
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date: 2023-
|
13
|
+
date: 2023-03-21 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: rails
|
@@ -44,6 +45,7 @@ description: AtomicLti implements the LTI Advantage specification. This gem does
|
|
44
45
|
email:
|
45
46
|
- matt.petro@atomicjolt.com
|
46
47
|
- justin.ball@atomicjolt.com
|
48
|
+
- nick.benoit@atomicjolt.com
|
47
49
|
executables: []
|
48
50
|
extensions: []
|
49
51
|
extra_rdoc_files: []
|