atomic_lti 1.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +22 -0
- data/Rakefile +13 -0
- data/app/assets/config/atomic_lti_manifest.js +1 -0
- data/app/assets/stylesheets/atomic_lti/application.css +0 -0
- data/app/assets/stylesheets/atomic_lti/jwks.css +4 -0
- data/app/controllers/atomic_lti/jwks_controller.rb +17 -0
- data/app/helpers/atomic_lti/launch_helper.rb +4 -0
- data/app/jobs/atomic_lti/application_job.rb +4 -0
- data/app/lib/atomic_lti/auth_token.rb +35 -0
- data/app/lib/atomic_lti/authorization.rb +152 -0
- data/app/lib/atomic_lti/config.rb +213 -0
- data/app/lib/atomic_lti/deep_linking.rb +36 -0
- data/app/lib/atomic_lti/definitions.rb +169 -0
- data/app/lib/atomic_lti/exceptions.rb +87 -0
- data/app/lib/atomic_lti/lti.rb +94 -0
- data/app/lib/atomic_lti/open_id.rb +22 -0
- data/app/lib/atomic_lti/params.rb +135 -0
- data/app/lib/atomic_lti/services/base.rb +38 -0
- data/app/lib/atomic_lti/services/line_items.rb +90 -0
- data/app/lib/atomic_lti/services/names_and_roles.rb +74 -0
- data/app/lib/atomic_lti/services/results.rb +18 -0
- data/app/lib/atomic_lti/services/score.rb +69 -0
- data/app/lib/atomic_lti/services/score_canvas.rb +47 -0
- data/app/mailers/atomic_lti/application_mailer.rb +6 -0
- data/app/models/atomic_lti/application_record.rb +5 -0
- data/app/models/atomic_lti/context.rb +10 -0
- data/app/models/atomic_lti/deployment.rb +13 -0
- data/app/models/atomic_lti/install.rb +11 -0
- data/app/models/atomic_lti/jwk.rb +41 -0
- data/app/models/atomic_lti/oauth_state.rb +5 -0
- data/app/models/atomic_lti/open_id_state.rb +5 -0
- data/app/models/atomic_lti/platform.rb +5 -0
- data/app/models/atomic_lti/platform_instance.rb +8 -0
- data/app/views/atomic_lti/launches/index.html.erb +11 -0
- data/app/views/atomic_lti/shared/redirect.html.erb +15 -0
- data/app/views/layouts/atomic_lti/application.html.erb +14 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20220428175127_create_atomic_lti_platforms.rb +12 -0
- data/db/migrate/20220428175128_create_atomic_lti_platform_instances.rb +15 -0
- data/db/migrate/20220428175247_create_atomic_lti_installs.rb +11 -0
- data/db/migrate/20220428175305_create_atomic_lti_deployments.rb +13 -0
- data/db/migrate/20220428175336_create_atomic_lti_contexts.rb +15 -0
- data/db/migrate/20220428175423_create_atomic_lti_oauth_states.rb +10 -0
- data/db/migrate/20220503003528_create_atomic_lti_jwks.rb +12 -0
- data/db/migrate/20221010140920_create_open_id_state.rb +9 -0
- data/db/seeds.rb +29 -0
- data/lib/atomic_lti/engine.rb +9 -0
- data/lib/atomic_lti/error_handling_middleware.rb +33 -0
- data/lib/atomic_lti/open_id_middleware.rb +270 -0
- data/lib/atomic_lti/version.rb +3 -0
- data/lib/atomic_lti.rb +27 -0
- data/lib/tasks/atomic_lti_tasks.rake +4 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1001c55b8a026812a60d322798ed5838ccfa9f9aebe538563b5f33befc7f339b
|
4
|
+
data.tar.gz: 2b19bd4852d7a04517677331951c2a0c9968c79d4e7121e0ffe07892e8f8474a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fa00363f1e618f46db5c5bcb0a0efab5b5b4ebb470b316f3e3a10367e8e5b2dd6b7dca7309c17c59576a086367f99b8109ea7b9e6cc77252c98cfe9f312fa236
|
7
|
+
data.tar.gz: 4b537933b0208bdb34b88e893fd5817a6b5b21bea6a0ac6d2457ed7ba8ac6d71ecd721054e0638a2666c4d559973cf21ecfe05827bd5575bf36f50d956b70b1a
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 Matt Petro
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# AtomicLti
|
2
|
+
Atomic LTI implements the LTI Advantage specification.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
Add the gem:
|
6
|
+
|
7
|
+
`gem 'atomic_lti', git: 'https://github.com/atomicjolt/atomic_lti.git', tag: '1.0.9'`
|
8
|
+
|
9
|
+
Add an initializer
|
10
|
+
`config/initializers/atomic_lti.rb`
|
11
|
+
|
12
|
+
with the following contents. Adjust paths as needed.
|
13
|
+
|
14
|
+
`
|
15
|
+
AtomicLti.oidc_init_path = "/oidc/init"
|
16
|
+
AtomicLti.oidc_redirect_path = "/oidc/redirect"
|
17
|
+
AtomicLti.target_link_path_prefixes = ["/lti_launches"]
|
18
|
+
AtomicLti.default_deep_link_path = "/lti_launches"
|
19
|
+
AtomicLti.jwt_secret = Rails.application.secrets.auth0_client_secret
|
20
|
+
AtomicLti.scopes = AtomicLti::Definitions.scopes.join(" ")
|
21
|
+
`
|
22
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
|
+
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
require 'rspec/core/rake_task'
|
11
|
+
|
12
|
+
RSpec::Core::RakeTask.new(:spec)
|
13
|
+
task default: :spec
|
@@ -0,0 +1 @@
|
|
1
|
+
//= link_directory ../stylesheets/atomic_lti .css
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AtomicLti
|
2
|
+
class JwksController < ::ApplicationController
|
3
|
+
def index
|
4
|
+
respond_to do |format|
|
5
|
+
# Map is required or the outer to_json will show your private keys to the world
|
6
|
+
format.json { render json: { keys: jwks_from_domain.map(&:to_json) }.to_json }
|
7
|
+
format.text { render plain: jwks_from_domain.map(&:to_pem).join('\n') }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def jwks_from_domain
|
14
|
+
Jwk.where(domain: request.host_with_port).presence || Jwk.where(domain: nil)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "jwt"
|
2
|
+
|
3
|
+
module AtomicLti
|
4
|
+
module AuthToken
|
5
|
+
|
6
|
+
ALGORITHM = "HS512".freeze
|
7
|
+
|
8
|
+
# More information on jwt available at
|
9
|
+
# http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#rfc.section.4.1.6
|
10
|
+
def self.issue_token(payload, exp = 24.hours.from_now, secret = nil, aud = nil, header_fields = {})
|
11
|
+
payload["iat"] = DateTime.now.to_i # issued at claim
|
12
|
+
payload["exp"] = exp.to_i # Default expiration set to 24 hours.
|
13
|
+
payload["aud"] = aud || Rails.application.secrets.auth0_client_id
|
14
|
+
JWT.encode(
|
15
|
+
payload,
|
16
|
+
AtomicLti.jwt_secret,
|
17
|
+
ALGORITHM,
|
18
|
+
header_fields,
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.valid?(token, secret = nil, algorithm = ALGORITHM)
|
23
|
+
decode(token, secret, true, algorithm)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.decode(token, secret = nil, validate = true, algorithm = ALGORITHM)
|
27
|
+
JWT.decode(
|
28
|
+
token,
|
29
|
+
AtomicLti.jwt_secret,
|
30
|
+
validate,
|
31
|
+
{ algorithm: algorithm },
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require "jwt"
|
2
|
+
|
3
|
+
module AtomicLti
|
4
|
+
class Authorization
|
5
|
+
|
6
|
+
AUTHORIZATION_TRIES = 3
|
7
|
+
# Validates a token provided by an LTI consumer
|
8
|
+
def self.validate_token(token)
|
9
|
+
# Get the iss value from the original request during the oidc call.
|
10
|
+
# Use that value to figure out which jwk we should use.
|
11
|
+
decoded_token = JWT.decode(token, nil, false)
|
12
|
+
|
13
|
+
iss = decoded_token.dig(0, "iss")
|
14
|
+
|
15
|
+
raise AtomicLti::Exceptions::InvalidLTIToken.new("LTI token is missing iss") if iss.blank?
|
16
|
+
|
17
|
+
platform = Platform.find_by(iss: iss)
|
18
|
+
|
19
|
+
raise AtomicLti::Exceptions::NoLTIPlatform(iss: iss, deployment_id: decoded_token.dig(0, "deployment_id")) if platform.nil?
|
20
|
+
|
21
|
+
cache_key = "#{iss}_jwks"
|
22
|
+
|
23
|
+
jwk_loader = ->(options) do
|
24
|
+
jwks = Rails.cache.read(cache_key)
|
25
|
+
if options[:invalidate] || jwks.blank?
|
26
|
+
jwks = JSON.parse(
|
27
|
+
HTTParty.get(platform.jwks_url).body,
|
28
|
+
).deep_symbolize_keys
|
29
|
+
Rails.cache.write(cache_key, jwks, expires_in: 12.hours)
|
30
|
+
end
|
31
|
+
jwks
|
32
|
+
end
|
33
|
+
|
34
|
+
lti_token, _keys = JWT.decode(token, nil, true, { algorithms: ["RS256"], jwks: jwk_loader })
|
35
|
+
lti_token
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.sign_tool_jwt(payload)
|
39
|
+
jwk = Jwk.current_jwk
|
40
|
+
JWT.encode(payload, jwk.private_key, jwk.alg, kid: jwk.kid, typ: "JWT")
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.client_assertion(iss:, deployment_id:)
|
44
|
+
# https://www.imsglobal.org/spec/lti/v1p3/#token-endpoint-claim-and-services
|
45
|
+
# When requesting an access token, the client assertion JWT iss and sub must both be the
|
46
|
+
# OAuth 2 client_id of the tool as issued by the learning platform during registration.
|
47
|
+
# Additional information:
|
48
|
+
# https://www.imsglobal.org/spec/security/v1p0/#using-json-web-tokens-with-oauth-2-0-client-credentials-grant
|
49
|
+
|
50
|
+
# lti_install = lti_deployment.lti_install
|
51
|
+
|
52
|
+
deployment = AtomicLti::Deployment.find_by(iss: iss, deployment_id: deployment_id)
|
53
|
+
|
54
|
+
raise AtomicLti::Exceptions::NoLTIDeployment.new(iss: iss, deployment_id: deployment_id) if deployment.nil?
|
55
|
+
|
56
|
+
install = deployment.install
|
57
|
+
|
58
|
+
raise AtomicLti::Exceptions::NoLTIInstall.new(iss: iss, deployment_id: deployment_id) if install.nil?
|
59
|
+
|
60
|
+
platform = install.platform
|
61
|
+
|
62
|
+
raise AtomicLti::Exceptions::NoLTIPlatform.new(iss: iss, deployment_id: deployment_id) if platform.nil?
|
63
|
+
|
64
|
+
payload = {
|
65
|
+
iss: install.client_id, # A unique identifier for the entity that issued the JWT
|
66
|
+
sub: install.client_id, # "client_id" of the OAuth Client
|
67
|
+
aud: platform.token_url, # Authorization server identifier
|
68
|
+
iat: Time.now.to_i, # Timestamp for when the JWT was created
|
69
|
+
exp: Time.now.to_i + 300, # Timestamp for when the JWT should be treated as having expired
|
70
|
+
# (after allowing a margin for clock skew)
|
71
|
+
jti: SecureRandom.hex(10), # A unique (potentially reusable) identifier for the token
|
72
|
+
}
|
73
|
+
sign_tool_jwt(payload)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.request_token(iss:, deployment_id:)
|
77
|
+
deployment = AtomicLti::Deployment.find_by(iss: iss, deployment_id: deployment_id)
|
78
|
+
|
79
|
+
raise AtomicLti::Exceptions::NoLTIDeployment.new(iss: iss, deployment_id: deployment_id) if deployment.nil?
|
80
|
+
|
81
|
+
cache_key = "#{deployment.cache_key_with_version}/services_authorization"
|
82
|
+
tries = 1
|
83
|
+
|
84
|
+
begin
|
85
|
+
authorization = Rails.cache.read(cache_key)
|
86
|
+
return authorization if authorization.present?
|
87
|
+
|
88
|
+
authorization = request_token_uncached(iss: iss, deployment_id: deployment_id)
|
89
|
+
|
90
|
+
# Subtract a few seconds so we don't use an expired token
|
91
|
+
expires_in = authorization["expires_in"].to_i - 10
|
92
|
+
|
93
|
+
Rails.cache.write(
|
94
|
+
cache_key,
|
95
|
+
authorization,
|
96
|
+
expires_in: expires_in,
|
97
|
+
)
|
98
|
+
|
99
|
+
rescue AtomicLti::Exceptions::RateLimitError => e
|
100
|
+
if tries < AUTHORIZATION_TRIES
|
101
|
+
Rails.logger.warn("LTI Request token error: Rate limit exception, sleeping")
|
102
|
+
sleep rand(1.0..2.0)
|
103
|
+
tries += 1
|
104
|
+
retry
|
105
|
+
else
|
106
|
+
raise e
|
107
|
+
end
|
108
|
+
end
|
109
|
+
authorization
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.request_token_uncached(iss:, deployment_id:)
|
113
|
+
# Details here:
|
114
|
+
# https://www.imsglobal.org/spec/security/v1p0/#using-json-web-tokens-with-oauth-2-0-client-credentials-grant
|
115
|
+
body = {
|
116
|
+
grant_type: "client_credentials",
|
117
|
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
118
|
+
scope: AtomicLti.scopes,
|
119
|
+
client_assertion: client_assertion(iss: iss, deployment_id: deployment_id),
|
120
|
+
}
|
121
|
+
headers = {
|
122
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
123
|
+
}
|
124
|
+
|
125
|
+
deployment = AtomicLti::Deployment.find_by(iss: iss, deployment_id: deployment_id)
|
126
|
+
|
127
|
+
raise AtomicLti::Exceptions::NoLTIDeployment.new(iss: iss, deployment_id: deployment_id) if deployment.nil?
|
128
|
+
|
129
|
+
platform = deployment.platform
|
130
|
+
|
131
|
+
raise AtomicLti::Exceptions::NoLTIPlatform.new(iss: iss, deployment_id: deployment_id) if platform.nil?
|
132
|
+
|
133
|
+
result = HTTParty.post(
|
134
|
+
platform.token_url,
|
135
|
+
body: body,
|
136
|
+
headers: headers
|
137
|
+
)
|
138
|
+
|
139
|
+
if !result.success?
|
140
|
+
Rails.logger.warn(result.body)
|
141
|
+
|
142
|
+
# Canvas rate limit error
|
143
|
+
raise AtomicLti::Exceptions::RateLimitError if /rate limit/i.match?(result.body)
|
144
|
+
|
145
|
+
raise AtomicLti::Exceptions::JwtIssueError.new(result.body)
|
146
|
+
end
|
147
|
+
|
148
|
+
JSON.parse(result.body)
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
# https://canvas.instructure.com/doc/api/tools_xml.html
|
2
|
+
# LTI gem docs: https://github.com/instructure/ims-lti
|
3
|
+
|
4
|
+
# These are the available LTI placements in Canvas.
|
5
|
+
# Placements that are implemented:
|
6
|
+
# account_navigation
|
7
|
+
# course_navigation
|
8
|
+
# editor_button
|
9
|
+
# global_navigation
|
10
|
+
# link_selection
|
11
|
+
# post_grades
|
12
|
+
# resource_selection
|
13
|
+
# assignment_selection
|
14
|
+
# user_navigation
|
15
|
+
# assignment_configuration
|
16
|
+
# assignment_edit
|
17
|
+
# assignment_view
|
18
|
+
# assignment_menu
|
19
|
+
# collaboration
|
20
|
+
# course_home_sub_navigation
|
21
|
+
# course_settings_sub_navigation
|
22
|
+
# discussion_topic_menu
|
23
|
+
# file_menu
|
24
|
+
# homework_submission
|
25
|
+
# migration_selection
|
26
|
+
# module_menu
|
27
|
+
# quiz_menu
|
28
|
+
# tool_configuration
|
29
|
+
# wiki_page_menu
|
30
|
+
|
31
|
+
module AtomicLti
|
32
|
+
class Config
|
33
|
+
# Converts old lti into lti advantage
|
34
|
+
# NOTE this is a work in progress and may not correctly convert all LTI configs.
|
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?
|
38
|
+
|
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?
|
42
|
+
end
|
43
|
+
|
44
|
+
{
|
45
|
+
title: args[:title],
|
46
|
+
scopes: AtomicLti::Definitions.scopes,
|
47
|
+
icon: icon(domain, args),
|
48
|
+
target_link_uri: args[:launch_url],
|
49
|
+
oidc_initiation_url: "#{args[:launch_url]}/init",
|
50
|
+
public_jwk: jwk.to_json,
|
51
|
+
description: args[:description],
|
52
|
+
custom_fields: custom_fields_from_args(domain, args[:title], args),
|
53
|
+
extensions: [
|
54
|
+
{
|
55
|
+
"platform": "canvas.instructure.com",
|
56
|
+
"domain": "https://#{domain}",
|
57
|
+
"tool_id": "helloworld",
|
58
|
+
"settings": {
|
59
|
+
"privacy_level": "public",
|
60
|
+
"text": args[:title],
|
61
|
+
"icon_url": "https://#{domain}/atomicjolt.png",
|
62
|
+
"selection_width": 500,
|
63
|
+
"selection_height": 500,
|
64
|
+
"placements": placements(domain, args[:title], args),
|
65
|
+
},
|
66
|
+
},
|
67
|
+
],
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.placements(domain, text, args)
|
72
|
+
conf = []
|
73
|
+
conf << placement_from_args(:resource_selection, domain, text, args)
|
74
|
+
conf << placement_from_args(:global_navigation, domain, text, args)
|
75
|
+
conf << placement_from_args(:user_navigation, domain, text, args)
|
76
|
+
conf << placement_from_args(:course_navigation, domain, text, args)
|
77
|
+
conf << placement_from_args(:account_navigation, domain, text, args)
|
78
|
+
conf << placement_from_args(:post_grades, domain, text, args)
|
79
|
+
conf << placement_from_args(:assignment_configuration, domain, text, args)
|
80
|
+
conf << placement_from_args(:assignment_edit, domain, text, args)
|
81
|
+
conf << placement_from_args(:assignment_menu, domain, text, args)
|
82
|
+
conf << placement_from_args(:collaboration, domain, text, args)
|
83
|
+
conf << placement_from_args(:course_home_sub_navigation, domain, text, args)
|
84
|
+
conf << placement_from_args(:course_settings_sub_navigation, domain, text, args)
|
85
|
+
conf << placement_from_args(:discussion_topic_menu, domain, text, args)
|
86
|
+
conf << placement_from_args(:file_menu, domain, text, args)
|
87
|
+
conf << placement_from_args(:homework_submission, domain, text, args)
|
88
|
+
conf << placement_from_args(:migration_selection, domain, text, args)
|
89
|
+
conf << placement_from_args(:quiz_menu, domain, text, args)
|
90
|
+
conf << placement_from_args(:tool_configuration, domain, text, args)
|
91
|
+
conf << editor_button_from_args(domain, text, args)
|
92
|
+
conf << assignment_selection_from_args(domain, text, args)
|
93
|
+
conf << link_selection_from_args(domain, text, args)
|
94
|
+
conf << assignment_view_from_args(domain, text, args)
|
95
|
+
conf << module_menu_from_args(domain, text, args)
|
96
|
+
conf << wiki_page_menu_from_args(domain, text, args)
|
97
|
+
conf << content_migration_args(domain, text, args)
|
98
|
+
conf.compact
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.icon(domain, args)
|
102
|
+
if args[:icon].present?
|
103
|
+
args[:icon].include?("http") ? args[:icon] : "https://#{domain}/#{args[:icon]}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.custom_fields_from_args(_domain, _text, args = {})
|
108
|
+
custom_fields = {
|
109
|
+
custom_canvas_api_domain: "$Canvas.api.domain",
|
110
|
+
}
|
111
|
+
if args[:custom_fields].present?
|
112
|
+
custom_fields.merge(args[:custom_fields]).stringify_keys
|
113
|
+
else
|
114
|
+
custom_fields.stringify_keys
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.placement_from_args(placement, domain, text, args = {})
|
119
|
+
if args[placement].present?
|
120
|
+
default_configs_from_args(args, domain, text, placement).merge(
|
121
|
+
args[:placement].stringify_keys,
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.editor_button_from_args(domain, _text, args = {})
|
127
|
+
if args[:editor_button].present?
|
128
|
+
config = args[:editor_button].stringify_keys
|
129
|
+
config["icon_url"] = "https://#{domain}/#{args[:editor_button][:icon_url]}"
|
130
|
+
config.delete("icon")
|
131
|
+
selection_config_from_args!(args, config, :editor_button)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.assignment_selection_from_args(_domain, _text, args = {})
|
136
|
+
if args[:assignment_selection].present?
|
137
|
+
config = args[:assignment_selection].stringify_keys
|
138
|
+
selection_config_from_args!(args, config, :assignment_selection)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.link_selection_from_args(_domain, _text, args = {})
|
143
|
+
if args[:link_selection].present?
|
144
|
+
config = args[:link_selection].stringify_keys
|
145
|
+
selection_config_from_args!(args, config, :link_selection)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.assignment_view_from_args(domain, text, args = {})
|
150
|
+
if args[:assignment_view].present?
|
151
|
+
config = default_configs_from_args(args, domain, text, :assignment_view)
|
152
|
+
config["visibility"] ||= "admins"
|
153
|
+
config
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.module_menu_from_args(domain, text, args = {})
|
158
|
+
if args[:module_menu].present?
|
159
|
+
config["module_menu"] = args[:module_menu].stringify_keys
|
160
|
+
if config["module_menu"]["message_type"].present?
|
161
|
+
selection_config_from_args!(args, config, :module_menu)
|
162
|
+
end
|
163
|
+
default_configs_from_args(args, domain, text, :module_menu)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.wiki_page_menu_from_args(domain, text, args = {})
|
168
|
+
if args[:wiki_page_menu].present?
|
169
|
+
config = default_configs_from_args(args, domain, text, :wiki_page_menu).merge(
|
170
|
+
args[:wiki_page_menu].stringify_keys,
|
171
|
+
)
|
172
|
+
if args[:wiki_page_menu][:message_type].present?
|
173
|
+
selection_config_from_args!(args, config, :wiki_page_menu)
|
174
|
+
else
|
175
|
+
config
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.content_migration_args(_domain, _text, args = {})
|
181
|
+
if args[:content_migration].present?
|
182
|
+
config = args[:content_migration] || {}
|
183
|
+
config["export_start_url"] ||= args[:export_url]
|
184
|
+
config["import_start_url"] ||= args[:import_url]
|
185
|
+
config
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.selection_config_from_args!(args, config, placement)
|
190
|
+
config["placement"] = placement
|
191
|
+
config["message_type"] ||= "ContentItemSelectionRequest"
|
192
|
+
config["url"] ||= args[:launch_url]
|
193
|
+
config
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.assignment_configs_from_args!(args, config, key)
|
197
|
+
config[key] = args[key].stringify_keys
|
198
|
+
config[key]["url"] ||= args[:launch_url]
|
199
|
+
# launch_height and launch_width are optional. Include them in the LTI config to set to a specific value
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.default_configs_from_args(_args, _domain, text, placement)
|
203
|
+
{
|
204
|
+
"placement": placement,
|
205
|
+
"text": text,
|
206
|
+
"enabled": true,
|
207
|
+
"icon_url": "https://{domain}/atomicjolt.png",
|
208
|
+
"message_type": "LtiResourceLinkRequest",
|
209
|
+
"target_link_uri": "https://{domain}/lti_launches",
|
210
|
+
}
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module AtomicLti
|
2
|
+
|
3
|
+
module DeepLinking
|
4
|
+
|
5
|
+
# # ###########################################################
|
6
|
+
# # Create a jwt to sign a response to the platform
|
7
|
+
def self.create_deep_link_jwt(iss:, deployment_id:, content_items:, deep_link_claim_data: nil)
|
8
|
+
deployment = AtomicLti::Deployment.find_by(iss: iss, deployment_id: deployment_id)
|
9
|
+
|
10
|
+
raise AtomicLti::Exceptions::NoLTIDeployment(iss, deployment_id) if deployment.nil?
|
11
|
+
|
12
|
+
install = deployment.install
|
13
|
+
raise AtomicLti::Exceptions::NoLTIInstall(iss, deployment_id) if install.nil?
|
14
|
+
|
15
|
+
payload = {
|
16
|
+
iss: install.client_id, # A unique identifier for the entity that issued the JWT
|
17
|
+
aud: iss, # Authorization server identifier
|
18
|
+
iat: Time.now.to_i, # Timestamp for when the JWT was created
|
19
|
+
exp: Time.now.to_i + 300, # Timestamp for when the JWT should be treated as having expired
|
20
|
+
# (after allowing a margin for clock skew)
|
21
|
+
azp: install.client_id,
|
22
|
+
nonce: SecureRandom.hex(10),
|
23
|
+
AtomicLti::Definitions::MESSAGE_TYPE => "LtiDeepLinkingResponse",
|
24
|
+
AtomicLti::Definitions::LTI_VERSION => "1.3.0",
|
25
|
+
AtomicLti::Definitions::DEPLOYMENT_ID => deployment_id,
|
26
|
+
AtomicLti::Definitions::CONTENT_ITEM_CLAIM => content_items
|
27
|
+
}
|
28
|
+
|
29
|
+
if deep_link_claim_data.present?
|
30
|
+
payload[AtomicLti::Definitions::DEEP_LINKING_DATA_CLAIM] = deep_link_claim_data
|
31
|
+
end
|
32
|
+
|
33
|
+
AtomicLti::Authorization.sign_tool_jwt(payload)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|