atomic_lti 1.1.0 → 1.3.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: 1001c55b8a026812a60d322798ed5838ccfa9f9aebe538563b5f33befc7f339b
4
- data.tar.gz: 2b19bd4852d7a04517677331951c2a0c9968c79d4e7121e0ffe07892e8f8474a
3
+ metadata.gz: a219867c2a1f19737d0222b0896e30a28144310a6032f1f6419443af37006304
4
+ data.tar.gz: ee72503b3f1066f5e3a5ea710da6d818eb2680d79d60e805c828b8f34ef95917
5
5
  SHA512:
6
- metadata.gz: fa00363f1e618f46db5c5bcb0a0efab5b5b4ebb470b316f3e3a10367e8e5b2dd6b7dca7309c17c59576a086367f99b8109ea7b9e6cc77252c98cfe9f312fa236
7
- data.tar.gz: 4b537933b0208bdb34b88e893fd5817a6b5b21bea6a0ac6d2457ed7ba8ac6d71ecd721054e0638a2666c4d559973cf21ecfe05827bd5575bf36f50d956b70b1a
6
+ metadata.gz: 49fd684fa99b65a02402d968b2c53741af0ca19594f0a9ab083cc6d9e3e3a2780caeba0f4de3b31db200dcb8fa745d9b1771b0eae1ef7c025ad68baff5e49fb6
7
+ data.tar.gz: 8670e8c13dec604b3af371ad7bff7fb7102be8aac32e32ae0e3ebca80718546e1cab5dfb559f18455bddcd2535032310e6be6b12ad568e9d74b4dc1c29ba25c6
data/README.md CHANGED
@@ -1,22 +1,42 @@
1
1
  # AtomicLti
2
2
  Atomic LTI implements the LTI Advantage specification.
3
3
 
4
- ## Usage
5
- Add the gem:
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
- `gem 'atomic_lti', git: 'https://github.com/atomicjolt/atomic_lti.git', tag: '1.0.9'`
16
+ Or install it yourself as:
17
+ ```bash
18
+ $ gem install atomic_tenant
19
+ ```
8
20
 
9
- Add an initializer
10
- `config/initializers/atomic_lti.rb`
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
- # # Create a jwt to sign a response to the platform
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
- UrlHelper.safe_host(host)
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} and deployment_id #{deployment_id}"
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
@@ -16,6 +16,19 @@ module AtomicLti
16
16
  errors.push("LTI token is missing required field sub")
17
17
  end
18
18
 
19
+ if decoded_token["aud"].blank?
20
+ errors.push("LTI token is missing required field aud")
21
+ end
22
+
23
+ if decoded_token["aud"].is_a?(Array) && decoded_token["aud"].length > 1
24
+ # OpenID Connect spec specifies the AZP should exist and be an AUD
25
+ if decoded_token["azp"].blank?
26
+ errors.push("LTI token has multiple aud and is missing required field azp")
27
+ elsif decoded_token["aud"].exclude?(decoded_token["azp"])
28
+ errors.push("LTI token azp is not one of the aud's")
29
+ end
30
+ end
31
+
19
32
  if decoded_token[AtomicLti::Definitions::DEPLOYMENT_ID].blank?
20
33
  errors.push(
21
34
  "LTI token is missing required field #{AtomicLti::Definitions::DEPLOYMENT_ID}"
@@ -90,5 +103,17 @@ module AtomicLti
90
103
  false
91
104
  end
92
105
  end
106
+
107
+ def self.client_id(decoded_token)
108
+ if decoded_token["aud"]&.is_a?(Array)
109
+ if decoded_token["aud"].length > 1
110
+ decoded_token["azp"]
111
+ else
112
+ decoded_token["aud"][0]
113
+ end
114
+ else
115
+ decoded_token["aud"]
116
+ end
117
+ end
93
118
  end
94
- end
119
+ end
@@ -27,7 +27,7 @@ module AtomicLti
27
27
  tag: tag,
28
28
  startDateTime: start_date_time,
29
29
  endDateTime: end_date_time,
30
- }
30
+ }.compact
31
31
  attrs["resourceLinkId"] = resource_link_id if resource_link_id
32
32
  if external_tool_url
33
33
  attrs[AtomicLti::Definitions::CANVAS_SUBMISSION_TYPE] = {
@@ -43,7 +43,7 @@ module AtomicLti
43
43
  else
44
44
  uri = Addressable::URI.parse(endpoint)
45
45
  uri.query_values = (uri.query_values || {}).merge(query)
46
- uri
46
+ uri.to_str
47
47
  end
48
48
  verify_received_user_names(
49
49
  HTTParty.get(
@@ -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
- redirect_params = {
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, 'errors')
103
- if errors.present? && !errors['errors'].empty?
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['atomic.validated.decoded_id_token'] = decoded_jwt
108
- env['atomic.validated.id_token'] = id_token
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? && id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, 'guid').present?
138
- name = id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, 'name')
139
- version = id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, 'version')
140
- product_family_code = id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, 'product_family_code')
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['iss'],
148
- guid: id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, 'guid')
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,
@@ -157,14 +159,14 @@ module AtomicLti
157
159
  end
158
160
 
159
161
  def update_install(id_token:)
160
- client_id = id_token["aud"]
162
+ client_id = AtomicLti::Lti.client_id(id_token)
161
163
  iss = id_token["iss"]
162
164
 
163
165
  if client_id.present? && iss.present?
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? && id_token[AtomicLti::Definitions::CONTEXT_CLAIM]['id'].present?
176
- iss = id_token['iss']
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]['id']
179
- label = id_token[AtomicLti::Definitions::CONTEXT_CLAIM]['label']
180
- title = id_token[AtomicLti::Definitions::CONTEXT_CLAIM]['title']
181
- types = id_token[AtomicLti::Definitions::CONTEXT_CLAIM]['type']
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,
@@ -199,24 +202,23 @@ module AtomicLti
199
202
  end
200
203
 
201
204
  def update_deployment(id_token:)
202
- client_id = id_token["aud"]
205
+ client_id = AtomicLti::Lti.client_id(id_token)
203
206
  iss = id_token["iss"]
204
207
  deployment_id = id_token[AtomicLti::Definitions::DEPLOYMENT_ID]
205
208
  platform_guid = id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "guid")
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
- AtomicLti::Deployment
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, "No LTI Platform found for iss #{request.params["iss"]}"
250
+ raise AtomicLti::Exceptions::NoLTIPlatform(iss: request.params["iss"])
249
251
  end
250
252
 
251
253
  uri = URI.parse(platform.oidc_url)
@@ -1,3 +1,3 @@
1
1
  module AtomicLti
2
- VERSION = '1.1.0'
2
+ VERSION = '1.3.0'
3
3
  end
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,49 +1,51 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic_lti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.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-02-27 00:00:00.000000000 Z
13
+ date: 2023-03-22 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
- name: rails
16
+ name: pg
16
17
  requirement: !ruby/object:Gem::Requirement
17
18
  requirements:
18
19
  - - "~>"
19
20
  - !ruby/object:Gem::Version
20
- version: 7.0.3
21
+ version: '1.3'
21
22
  type: :runtime
22
23
  prerelease: false
23
24
  version_requirements: !ruby/object:Gem::Requirement
24
25
  requirements:
25
26
  - - "~>"
26
27
  - !ruby/object:Gem::Version
27
- version: 7.0.3
28
+ version: '1.3'
28
29
  - !ruby/object:Gem::Dependency
29
- name: pg
30
+ name: rails
30
31
  requirement: !ruby/object:Gem::Requirement
31
32
  requirements:
32
- - - '='
33
+ - - "~>"
33
34
  - !ruby/object:Gem::Version
34
- version: 1.3.5
35
+ version: '7.0'
35
36
  type: :runtime
36
37
  prerelease: false
37
38
  version_requirements: !ruby/object:Gem::Requirement
38
39
  requirements:
39
- - - '='
40
+ - - "~>"
40
41
  - !ruby/object:Gem::Version
41
- version: 1.3.5
42
+ version: '7.0'
42
43
  description: AtomicLti implements the LTI Advantage specification. This gem does contain
43
44
  source code specific to other Atomic Jolt products
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: []