atomic_lti 1.1.0 → 1.3.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 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: []