atomic_lti 1.3.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>