atomic_lti 1.3.1 → 1.5.1

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.
@@ -0,0 +1,95 @@
1
+ .aj-centered-message {
2
+ max-width: 600px;
3
+ margin: 48px auto 0;
4
+ border: 1px solid #ccc;
5
+ padding: 24px 32px;
6
+ border-radius: 10px;
7
+ }
8
+
9
+ .aj-icon {
10
+ width: 24px;
11
+ color: #333;
12
+ }
13
+
14
+ .aj-title {
15
+ font-family: 'Lato', 'Helvetica Nue', Helvetica, Arial sans-serif;
16
+ font-weight: 400;
17
+ font-size: 20px;
18
+ line-height: 1;
19
+ gap: 12px;
20
+ color: #333;
21
+ display: flex;
22
+ align-items: center;
23
+ -webkit-font-smoothing: antialiased;
24
+ -moz-osx-font-smoothing: grayscale;
25
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004);
26
+ }
27
+
28
+ .u-flex {
29
+ display: flex;
30
+ gap: 12px;
31
+ margin-top: 12px;
32
+ }
33
+
34
+ .u-flex > * {
35
+ margin: 0;
36
+ }
37
+
38
+ .aj-text.aj-text--small {
39
+ font-weight: 400;
40
+ font-size: 13px;
41
+ margin-top: 20px;
42
+ }
43
+
44
+ .aj-text {
45
+ font-family: 'Lato', 'Helvetica Nue', Helvetica, Arial sans-serif;
46
+ font-weight: 400;
47
+ font-size: 16px;
48
+ line-height: 1.4;
49
+ color: #333;
50
+ max-width: 600px;
51
+ -webkit-font-smoothing: antialiased;
52
+ -moz-osx-font-smoothing: grayscale;
53
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004);
54
+ }
55
+
56
+ .aj-btn.aj-btn--blue:hover:enabled, a.aj-btn.aj-btn--blue:hover:enabled {
57
+ box-shadow: 0 3px 4px rgba(0, 50, 150, 0.3);
58
+ background-color: #2D7DAE;
59
+ }
60
+
61
+ .aj-btn:hover:enabled, a.aj-btn:hover:enabled {
62
+ cursor: pointer;
63
+ background-color: #efefef;
64
+ }
65
+
66
+ .aj-btn.aj-btn--blue:disabled {
67
+ opacity: 0.5;
68
+ }
69
+
70
+ .aj-btn.aj-btn--blue, a.aj-btn.aj-btn--blue {
71
+ font-family: "Lato", "Helvetica Nue", Helvetica, Arial sans-serif;
72
+ font-weight: 700;
73
+ background-color: #2D7DAE;
74
+ border: none;
75
+ font-size: 16px;
76
+ line-height: 27px;
77
+ text-align: left;
78
+ color: #ffffff;
79
+ border-radius: none;
80
+ border: 2px solid #2D7DAE;
81
+ }
82
+
83
+ .aj-btn, a.aj-btn {
84
+ height: 40px;
85
+ display: inline-flex;
86
+ align-items: center;
87
+ gap: 12px;
88
+ justify-content: center;
89
+ border-radius: 5px;
90
+ white-space: nowrap;
91
+ position: relative;
92
+ isolation: isolate;
93
+ padding: 0 12px;
94
+ text-decoration: none;
95
+ }
@@ -0,0 +1,3 @@
1
+ import { InitOIDCLaunch } from '@atomicjolt/lti-client/src/init';
2
+
3
+ InitOIDCLaunch(window.SETTINGS);
@@ -5,10 +5,10 @@ module AtomicLti
5
5
 
6
6
  AUTHORIZATION_TRIES = 3
7
7
  # Validates a token provided by an LTI consumer
8
- def self.validate_token(token)
8
+ def self.validate_token(id_token)
9
9
  # Get the iss value from the original request during the oidc call.
10
10
  # Use that value to figure out which jwk we should use.
11
- decoded_token = JWT.decode(token, nil, false)
11
+ decoded_token = JWT.decode(id_token, nil, false)
12
12
 
13
13
  iss = decoded_token.dig(0, "iss")
14
14
 
@@ -31,8 +31,8 @@ module AtomicLti
31
31
  jwks
32
32
  end
33
33
 
34
- lti_token, _keys = JWT.decode(token, nil, true, { algorithms: ["RS256"], jwks: jwk_loader })
35
- lti_token
34
+ id_token_decoded, _keys = JWT.decode(id_token, nil, true, { algorithms: ["RS256"], jwks: jwk_loader })
35
+ id_token_decoded
36
36
  end
37
37
 
38
38
  def self.sign_tool_jwt(payload)
@@ -73,19 +73,26 @@ module AtomicLti
73
73
  sign_tool_jwt(payload)
74
74
  end
75
75
 
76
- def self.request_token(iss:, deployment_id:)
76
+ def self.request_token(iss:, deployment_id:, scopes: nil)
77
77
  deployment = AtomicLti::Deployment.find_by(iss: iss, deployment_id: deployment_id)
78
78
 
79
79
  raise AtomicLti::Exceptions::NoLTIDeployment.new(iss: iss, deployment_id: deployment_id) if deployment.nil?
80
80
 
81
- cache_key = "#{deployment.cache_key_with_version}/services_authorization"
81
+ scopestr = if scopes
82
+ scopes.sort.join(" ")
83
+ else
84
+ AtomicLti.scopes
85
+ end
86
+
87
+ # Token is cached based on deployment id and requested scopes
88
+ cache_key = "#{deployment.cache_key}/#{Digest::SHA1.hexdigest(scopestr)}/services_authorization"
82
89
  tries = 1
83
90
 
84
91
  begin
85
92
  authorization = Rails.cache.read(cache_key)
86
93
  return authorization if authorization.present?
87
94
 
88
- authorization = request_token_uncached(iss: iss, deployment_id: deployment_id)
95
+ authorization = request_token_uncached(iss: iss, deployment_id: deployment_id, scopes: scopestr)
89
96
 
90
97
  # Subtract a few seconds so we don't use an expired token
91
98
  expires_in = authorization["expires_in"].to_i - 10
@@ -109,13 +116,13 @@ module AtomicLti
109
116
  authorization
110
117
  end
111
118
 
112
- def self.request_token_uncached(iss:, deployment_id:)
119
+ def self.request_token_uncached(iss:, deployment_id:, scopes:)
113
120
  # Details here:
114
121
  # https://www.imsglobal.org/spec/security/v1p0/#using-json-web-tokens-with-oauth-2-0-client-credentials-grant
115
122
  body = {
116
123
  grant_type: "client_credentials",
117
124
  client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
118
- scope: AtomicLti.scopes,
125
+ scope: scopes,
119
126
  client_assertion: client_assertion(iss: iss, deployment_id: deployment_id),
120
127
  }
121
128
  headers = {
@@ -67,13 +67,13 @@ module AtomicLti
67
67
  ]
68
68
  end
69
69
 
70
- CANVAS_PUBLIC_LTI_KEYS_URL = "https://canvas.instructure.com/api/lti/security/jwks".freeze
71
- CANVAS_OIDC_URL = "https://canvas.instructure.com/api/lti/authorize_redirect".freeze
72
- CANVAS_AUTH_TOKEN_URL = "https://canvas.instructure.com/login/oauth2/token".freeze
70
+ CANVAS_PUBLIC_LTI_KEYS_URL = "https://sso.canvaslms.com/api/lti/security/jwks".freeze
71
+ CANVAS_OIDC_URL = "https://sso.canvaslms.com/api/lti/authorize_redirect".freeze
72
+ CANVAS_AUTH_TOKEN_URL = "https://sso.canvaslms.com/login/oauth2/token".freeze
73
73
 
74
- CANVAS_BETA_PUBLIC_LTI_KEYS_URL = "https://canvas.beta.instructure.com/api/lti/security/jwks".freeze
75
- CANVAS_BETA_AUTH_TOKEN_URL = "https://canvas.beta.instructure.com/login/oauth2/token".freeze
76
- CANVAS_BETA_OIDC_URL = "https://canvas.beta.instructure.com/api/lti/authorize_redirect".freeze
74
+ CANVAS_BETA_PUBLIC_LTI_KEYS_URL = "https://sso.beta.canvaslms.com/api/lti/security/jwks".freeze
75
+ CANVAS_BETA_OIDC_URL = "https://sso.beta.canvaslms.com/api/lti/authorize_redirect".freeze
76
+ CANVAS_BETA_AUTH_TOKEN_URL = "https://sso.beta.canvaslms.com/login/oauth2/token".freeze
77
77
 
78
78
  CANVAS_SUBMISSION_TYPE = "https://canvas.instructure.com/lti/submission_type".freeze
79
79
 
@@ -115,6 +115,38 @@ module AtomicLti
115
115
  MEMBER_CONTEXT_ROLE = "http://purl.imsglobal.org/vocab/lis/v2/membership#Member".freeze
116
116
  OFFICER_CONTEXT_ROLE = "http://purl.imsglobal.org/vocab/lis/v2/membership#Officer".freeze
117
117
 
118
+ ROLES = [
119
+ ADMINISTRATOR_SYSTEM_ROLE,
120
+ NONE_SYSTEM_ROLE,
121
+ ACCOUNT_ADMIN_SYSTEM_ROLE,
122
+ CREATOR_SYSTEM_ROLE,
123
+ SYS_ADMIN_SYSTEM_ROLE,
124
+ SYS_SUPPORT_SYSTEM_ROLE,
125
+ USER_SYSTEM_ROLE,
126
+ ADMINISTRATOR_INSTITUTION_ROLE,
127
+ FACULTY_INSTITUTION_ROLE,
128
+ GUEST_INSTITUTION_ROLE,
129
+ NONE_INSTITUTION_ROLE,
130
+ OTHER_INSTITUTION_ROLE,
131
+ STAFF_INSTITUTION_ROLE,
132
+ STUDENT_INSTITUTION_ROLE,
133
+ ALUMNI_INSTITUTION_ROLE,
134
+ INSTRUCTOR_INSTITUTION_ROLE,
135
+ LEARNER_INSTITUTION_ROLE,
136
+ MEMBER_INSTITUTION_ROLE,
137
+ MENTOR_INSTITUTION_ROLE,
138
+ OBSERVER_INSTITUTION_ROLE,
139
+ PROSPECTIVE_STUDENT_INSTITUTION_ROLE,
140
+ ADMINISTRATOR_CONTEXT_ROLE,
141
+ CONTENT_DEVELOPER_CONTEXT_ROLE,
142
+ INSTRUCTOR_CONTEXT_ROLE,
143
+ LEARNER_CONTEXT_ROLE,
144
+ MENTOR_CONTEXT_ROLE,
145
+ MANAGER_CONTEXT_ROLE,
146
+ MEMBER_CONTEXT_ROLE,
147
+ OFFICER_CONTEXT_ROLE,
148
+ ].freeze
149
+
118
150
  ADMINISTRATOR_ROLES = [
119
151
  ADMINISTRATOR_SYSTEM_ROLE,
120
152
  ACCOUNT_ADMIN_SYSTEM_ROLE,
@@ -1,7 +1,7 @@
1
1
  module AtomicLti
2
2
  module Exceptions
3
3
 
4
- # General exceptions
4
+ # LTI data related exceptions
5
5
  class AtomicLtiException < StandardError
6
6
  end
7
7
 
@@ -20,15 +20,9 @@ module AtomicLti
20
20
  class StateError < AtomicLtiException
21
21
  end
22
22
 
23
- class OpenIDStateError < AtomicLtiException
24
- end
25
-
26
23
  class OpenIDRedirectError < AtomicLtiException
27
24
  end
28
25
 
29
- class JwtIssueError < AtomicLtiException
30
- end
31
-
32
26
  class LineItemMissing < LineItemError
33
27
  end
34
28
 
@@ -50,18 +44,28 @@ module AtomicLti
50
44
  end
51
45
  end
52
46
 
53
- class NoLTIToken < AtomicLtiException
54
- def initialize(msg = "No LTI token provided")
47
+ # Authorization errors
48
+ class AtomicLtiAuthException < StandardError
49
+ end
50
+
51
+ class InvalidLTIToken < AtomicLtiAuthException
52
+ def initialize(msg = "Invalid LTI token provided")
55
53
  super(msg)
56
54
  end
57
55
  end
58
56
 
59
- class InvalidLTIToken < AtomicLtiException
60
- def initialize(msg = "Invalid LTI token provided")
57
+ class JwtIssueError < AtomicLtiAuthException
58
+ end
59
+
60
+ class NoLTIToken < AtomicLtiAuthException
61
+ def initialize(msg = "No LTI token provided")
61
62
  super(msg)
62
63
  end
63
64
  end
64
65
 
66
+ class OpenIDStateError < AtomicLtiAuthException
67
+ end
68
+
65
69
  # Not found exceptions
66
70
  class AtomicLtiNotFoundException < StandardError
67
71
  end
@@ -12,7 +12,7 @@ module AtomicLti
12
12
  errors.push("LTI token is missing required field iss")
13
13
  end
14
14
 
15
- if decoded_token["sub"].blank?
15
+ if decoded_token["sub"].blank? && !AtomicLti.allow_anonymous_user
16
16
  errors.push("LTI token is missing required field sub")
17
17
  end
18
18
 
@@ -51,6 +51,14 @@ module AtomicLti
51
51
  )
52
52
  end
53
53
 
54
+ roles = decoded_token[AtomicLti::Definitions::ROLES_CLAIM]
55
+ if AtomicLti.role_enforcement_mode == AtomicLti::RoleEnforcementMode::STRICT && roles.is_a?(Array) && !roles.empty?
56
+ invalid_roles = roles - AtomicLti::Definitions::ROLES
57
+ if invalid_roles.length == roles.length
58
+ errors.push("LTI token has invalid roles: #{invalid_roles.join(", ")}")
59
+ end
60
+ end
61
+
54
62
  if errors.length > 0
55
63
  raise AtomicLti::Exceptions::InvalidLTIToken.new(errors.join(" "))
56
64
  end
@@ -69,7 +77,7 @@ module AtomicLti
69
77
 
70
78
  if decoded_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM].blank?
71
79
  errors.push(
72
- "LTI token is missing required claim #{AtomicLti::Definitions::TARGET_LINK_URI_CLAIM}"
80
+ "LTI token is missing required claim #{AtomicLti::Definitions::TARGET_LINK_URI_CLAIM}",
73
81
  )
74
82
  end
75
83
 
@@ -77,19 +85,19 @@ module AtomicLti
77
85
  target_link_uri = decoded_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM]
78
86
  if validate_target_link_url && target_link_uri != requested_target_link_uri
79
87
  errors.push(
80
- "LTI token target link uri '#{target_link_uri}' doesn't match url '#{requested_target_link_uri}'"
88
+ "LTI token target link uri '#{target_link_uri}' doesn't match url '#{requested_target_link_uri}'",
81
89
  )
82
90
  end
83
91
 
84
92
  if decoded_token[AtomicLti::Definitions::RESOURCE_LINK_CLAIM].blank?
85
93
  errors.push(
86
- "LTI token is missing required claim #{AtomicLti::Definitions::RESOURCE_LINK_CLAIM}"
94
+ "LTI token is missing required claim #{AtomicLti::Definitions::RESOURCE_LINK_CLAIM}",
87
95
  )
88
96
  end
89
97
 
90
98
  if decoded_token.dig(AtomicLti::Definitions::RESOURCE_LINK_CLAIM, "id").blank?
91
99
  errors.push(
92
- "LTI token is missing required field id from the claim #{AtomicLti::Definitions::RESOURCE_LINK_CLAIM}"
100
+ "LTI token is missing required field id from the claim #{AtomicLti::Definitions::RESOURCE_LINK_CLAIM}",
93
101
  )
94
102
  end
95
103
 
@@ -1,22 +1,34 @@
1
1
  module AtomicLti
2
2
  class OpenId
3
- def self.validate_open_id_state(state)
4
- state = AtomicLti::AuthToken.decode(state)[0]
5
- if open_id_state = AtomicLti::OpenIdState.find_by(nonce: state["nonce"])
6
- open_id_state.destroy
7
- true
8
- else
9
- false
3
+ def self.validate_state(nonce, state)
4
+ if state.blank?
5
+ return false
10
6
  end
11
- rescue StandardError => e
12
- Rails.logger.info("Error decoding token: #{e} - #{e.backtrace}")
13
- false
7
+
8
+ open_id_state = AtomicLti::OpenIdState.find_by(state: state)
9
+ if !open_id_state
10
+ return false
11
+ end
12
+
13
+ open_id_state.destroy
14
+
15
+ # Check that the state hasn't expired
16
+ if open_id_state.created_at < 10.minutes.ago
17
+ return false
18
+ end
19
+
20
+ if nonce != open_id_state.nonce
21
+ return false
22
+ end
23
+
24
+ true
14
25
  end
15
26
 
16
- def self.state
27
+ def self.generate_state
17
28
  nonce = SecureRandom.hex(64)
18
- AtomicLti::OpenIdState.create!(nonce: nonce)
19
- AtomicLti::AuthToken.issue_token({ nonce: nonce })
29
+ state = SecureRandom.hex(32)
30
+ AtomicLti::OpenIdState.create!(nonce: nonce, state: state)
31
+ [nonce, state]
20
32
  end
21
33
  end
22
34
  end
@@ -3,8 +3,8 @@ module AtomicLti
3
3
  class Params
4
4
  attr_reader :token
5
5
 
6
- def initialize(lti_token)
7
- @token = lti_token.with_indifferent_access
6
+ def initialize(id_token_decoded)
7
+ @token = id_token_decoded.with_indifferent_access
8
8
  end
9
9
 
10
10
  def lti_advantage?
@@ -0,0 +1,8 @@
1
+ module AtomicLti
2
+ module RoleEnforcementMode
3
+ # Unkown roles are allowed to be the only role in the roles claim
4
+ DEFAULT = "DEFAULT".freeze
5
+ # Unkown roles are not allowed to be the only roles in the roles claim
6
+ STRICT = "STRICT".freeze
7
+ end
8
+ end
@@ -2,23 +2,24 @@ module AtomicLti
2
2
  module Services
3
3
  class Base
4
4
 
5
- def initialize(lti_token: nil, iss: nil, deployment_id: nil)
6
-
5
+ def initialize(id_token_decoded: nil, iss: nil, deployment_id: nil)
7
6
  token_iss = nil
8
7
  token_deployment_id = nil
9
8
 
10
- if lti_token.present?
11
- token_iss = lti_token.dig('iss')
12
- token_deployment_id = lti_token.dig(AtomicLti::Definitions::DEPLOYMENT_ID)
9
+ if id_token_decoded.present?
10
+ token_iss = id_token_decoded["iss"]
11
+ token_deployment_id = id_token_decoded[AtomicLti::Definitions::DEPLOYMENT_ID]
13
12
  end
14
13
 
15
- @lti_token = lti_token
14
+ @id_token_decoded = id_token_decoded
16
15
  @iss = iss || token_iss
17
16
  @deployment_id = deployment_id || token_deployment_id
18
17
  end
19
18
 
19
+ def scopes; end
20
+
20
21
  def headers(options = {})
21
- @token ||= AtomicLti::Authorization.request_token(iss: @iss, deployment_id: @deployment_id)
22
+ @token ||= AtomicLti::Authorization.request_token(iss: @iss, deployment_id: @deployment_id, scopes: scopes)
22
23
  {
23
24
  "Authorization" => "Bearer #{@token['access_token']}",
24
25
  }.merge(options)
@@ -3,12 +3,17 @@ module AtomicLti
3
3
  # Canvas API docs https://canvas.instructure.com/doc/api/line_items.html
4
4
  class LineItems < AtomicLti::Services::Base
5
5
 
6
- def endpoint(lti_token)
7
- url = lti_token.dig(AtomicLti::Definitions::AGS_CLAIM, "lineitems")
6
+ def endpoint(id_token_decoded)
7
+ url = id_token_decoded.dig(AtomicLti::Definitions::AGS_CLAIM, "lineitems")
8
8
  raise AtomicLti::Exceptions::LineItemError, "Unable to access line items" unless url.present?
9
+
9
10
  url
10
11
  end
11
12
 
13
+ def scopes
14
+ @id_token_decoded&.dig(AtomicLti::Definitions::AGS_CLAIM, "scope")
15
+ end
16
+
12
17
  # Helper method to generate a default set of attributes
13
18
  def self.generate(
14
19
  label:,
@@ -27,8 +32,8 @@ module AtomicLti
27
32
  tag: tag,
28
33
  startDateTime: start_date_time,
29
34
  endDateTime: end_date_time,
35
+ resourceLinkId: resource_link_id,
30
36
  }.compact
31
- attrs["resourceLinkId"] = resource_link_id if resource_link_id
32
37
  if external_tool_url
33
38
  attrs[AtomicLti::Definitions::CANVAS_SUBMISSION_TYPE] = {
34
39
  type: "external_tool",
@@ -42,14 +47,14 @@ module AtomicLti
42
47
  self.class.generate(**attrs)
43
48
  end
44
49
 
45
- def self.can_manage_line_items?(lti_token)
46
- lti_token.dig(AtomicLti::Definitions::AGS_CLAIM, "scope")&.
50
+ def self.can_manage_line_items?(id_token_decoded)
51
+ id_token_decoded.dig(AtomicLti::Definitions::AGS_CLAIM, "scope")&.
47
52
  include?(AtomicLti::Definitions::AGS_SCOPE_LINE_ITEM)
48
53
  end
49
54
 
50
- def self.can_query_line_items?(lti_token)
51
- can_manage_line_items?(lti_token) ||
52
- lti_token.dig(AtomicLti::Definitions::AGS_CLAIM, "scope").
55
+ def self.can_query_line_items?(id_token_decoded)
56
+ can_manage_line_items?(id_token_decoded) ||
57
+ id_token_decoded.dig(AtomicLti::Definitions::AGS_CLAIM, "scope").
53
58
  include?(AtomicLti::Definitions::AGS_SCOPE_LINE_ITEM_READONLY)
54
59
  end
55
60
 
@@ -57,7 +62,7 @@ module AtomicLti
57
62
  # Canvas: https://canvas.beta.instructure.com/doc/api/line_items.html#method.lti/ims/line_items.index
58
63
  def list(query = {})
59
64
  accept = { "Accept" => "application/vnd.ims.lis.v2.lineitemcontainer+json" }
60
- HTTParty.get(endpoint(@lti_token), headers: headers(accept), query: query)
65
+ HTTParty.get(endpoint(@id_token_decoded), headers: headers(accept), query: query)
61
66
  end
62
67
 
63
68
  # Get a specific line item
@@ -72,7 +77,7 @@ module AtomicLti
72
77
  # Canvas: https://canvas.beta.instructure.com/doc/api/line_items.html#method.lti/ims/line_items.create
73
78
  def create(attrs = nil)
74
79
  content_type = { "Content-Type" => "application/vnd.ims.lis.v2.lineitem+json" }
75
- HTTParty.post(endpoint(@lti_token), body: JSON.dump(attrs), headers: headers(content_type))
80
+ HTTParty.post(endpoint(@id_token_decoded), body: JSON.dump(attrs), headers: headers(content_type))
76
81
  end
77
82
 
78
83
  # Update a line item
@@ -2,12 +2,16 @@ module AtomicLti
2
2
  module Services
3
3
  class NamesAndRoles < AtomicLti::Services::Base
4
4
 
5
- def initialize(lti_token:)
6
- super(lti_token: lti_token)
5
+ def initialize(id_token_decoded:)
6
+ super(id_token_decoded: id_token_decoded)
7
+ end
8
+
9
+ def scopes
10
+ [AtomicLti::Definitions::NAMES_AND_ROLES_SCOPE]
7
11
  end
8
12
 
9
13
  def endpoint
10
- url = @lti_token.dig(AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM, "context_memberships_url")
14
+ url = @id_token_decoded.dig(AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM, "context_memberships_url")
11
15
  raise AtomicLti::Exceptions::NamesAndRolesError, "Unable to access names and roles" unless url.present?
12
16
 
13
17
  url
@@ -19,15 +23,15 @@ module AtomicLti
19
23
  url
20
24
  end
21
25
 
22
- def self.enabled?(lti_token)
23
- return false unless lti_token&.dig(AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM)
26
+ def self.enabled?(id_token_decoded)
27
+ return false unless id_token_decoded&.dig(AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM)
24
28
 
25
29
  (AtomicLti::Definitions::NAMES_AND_ROLES_SERVICE_VERSIONS &
26
- (lti_token.dig(AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM, "service_versions") || [])).present?
30
+ (id_token_decoded.dig(AtomicLti::Definitions::NAMES_AND_ROLES_CLAIM, "service_versions") || [])).present?
27
31
  end
28
32
 
29
33
  def valid?
30
- self.class.enabled?(@lti_token)
34
+ self.class.enabled?(@id_token_decoded)
31
35
  end
32
36
 
33
37
  # List names and roles
@@ -3,6 +3,10 @@ module AtomicLti
3
3
  # Canvas API docs: https://canvas.instructure.com/doc/api/result.html
4
4
  class Results < AtomicLti::Services::Base
5
5
 
6
+ def scopes
7
+ [AtomicLti::Definitions::AGS_SCOPE_RESULT]
8
+ end
9
+
6
10
  def list(line_item_id)
7
11
  url = "#{line_item_id}/results"
8
12
  HTTParty.get(url, headers: headers)
@@ -5,18 +5,22 @@ module AtomicLti
5
5
 
6
6
  attr_accessor :id
7
7
 
8
- def initialize(lti_token: nil, iss:nil, deployment_id: nil, id: nil)
9
- super(lti_token: lti_token, iss: iss, deployment_id: deployment_id)
8
+ def initialize(id_token_decoded: nil, iss: nil, deployment_id: nil, id: nil)
9
+ super(id_token_decoded: id_token_decoded, iss: iss, deployment_id: deployment_id)
10
10
  @id = id
11
11
  end
12
12
 
13
+ def scopes
14
+ [AtomicLti::Definitions::AGS_SCOPE_SCORE]
15
+ end
16
+
13
17
  def endpoint
14
18
  if id.blank?
15
19
  raise ::AtomicLti::Exceptions::ScoreError,
16
20
  "Invalid id or no id provided. Unable to access scores. id should be in the form of a url."
17
21
  end
18
22
  uri = URI(id)
19
- uri.path = uri.path+'/scores'
23
+ uri.path = "#{uri.path}/scores"
20
24
  uri
21
25
  end
22
26
 
@@ -52,7 +56,7 @@ module AtomicLti
52
56
  # values will require no action. Possible values are NotReady, Failed, Pending,
53
57
  # PendingManual, FullyGraded
54
58
  gradingProgress: grading_progress,
55
- }
59
+ }.compact
56
60
  end
57
61
 
58
62
  def send(attrs)
@@ -1,6 +1,6 @@
1
1
  module AtomicLti
2
2
  class Jwk < ApplicationRecord
3
- before_create :generate_keys
3
+ before_create :ensure_keys_exist
4
4
 
5
5
  def generate_keys
6
6
  pkey = OpenSSL::PKey::RSA.generate(2048)
@@ -37,5 +37,14 @@ module AtomicLti
37
37
  def self.current_jwk
38
38
  self.last
39
39
  end
40
+
41
+ private
42
+
43
+ def ensure_keys_exist
44
+ if kid.blank?
45
+ generate_keys
46
+ end
47
+ end
48
+
40
49
  end
41
50
  end
@@ -1,5 +1,6 @@
1
1
  module AtomicLti
2
2
  class OpenIdState < ApplicationRecord
3
3
  validates :nonce, presence: true, uniqueness: true
4
+ validates :state, presence: true, uniqueness: true
4
5
  end
5
6
  end
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <%= stylesheet_link_tag "atomic_lti/launch" %>
5
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
6
+ </head>
7
+ <body>
8
+ <div class="aj-main">
9
+ <div class="aj-error">
10
+ <h1 class="aj-title">
11
+ <i class="material-icons-outlined aj-icon" aria-hidden="true">error</i>
12
+ <%= @message %>
13
+ </h1>
14
+ </div>
15
+ </div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <style>
5
+ .hidden { display: none !important; }
6
+ </style>
7
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
8
+ <%= stylesheet_link_tag "atomic_lti/launch" %>
9
+ <%= javascript_include_tag "atomic_lti/init_app" %>
10
+ </head>
11
+ <body>
12
+ <noscript>
13
+ <div class="u-flex">
14
+ <i class="material-icons-outlined aj-icon" aria-hidden="true">warning</i>
15
+ <p class="aj-text">
16
+ You must have javascript enabled to use this application.
17
+ </p>
18
+ </div>
19
+ </noscript>
20
+ <div id="main-content">
21
+ </div>
22
+ <script type="text/javascript">
23
+ InitOIDCLaunch(window.SETTINGS);
24
+ </script>
25
+ </body>
26
+ </html>