Class: AtomicLti::Lti

Inherits:
Object
  • Object
show all
Defined in:
app/lib/atomic_lti/lti.rb

Class Method Summary collapse

Class Method Details

.client_id(decoded_token) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
# File 'app/lib/atomic_lti/lti.rb', line 117

def self.client_id(decoded_token)
  if decoded_token["aud"]&.is_a?(Array)
    if decoded_token["aud"].length > 1
      decoded_token["azp"]
    else
      decoded_token["aud"][0]
    end
  else
    decoded_token["aud"]
  end
end

.matching_uri?(target, actual, ignore_host:) ⇒ Boolean

Returns:

  • (Boolean)


129
130
131
132
133
134
135
136
137
138
# File 'app/lib/atomic_lti/lti.rb', line 129

def self.matching_uri?(target, actual, ignore_host:)
  t = URI.parse(target)
  a = URI.parse(actual)

  t.scheme == a.scheme &&
    t.path == a.path &&
    t.query == a.query &&
    t.fragment == a.fragment &&
    (ignore_host || t.host == a.host)
end

.valid_version?(decoded_token) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
113
114
115
# File 'app/lib/atomic_lti/lti.rb', line 109

def self.valid_version?(decoded_token)
  if decoded_token[AtomicLti::Definitions::LTI_VERSION]
    decoded_token[AtomicLti::Definitions::LTI_VERSION].starts_with?("1.3")
  else
    false
  end
end

.validate!(decoded_token, requested_target_link_uri = nil, validate_target_link_url = false) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'app/lib/atomic_lti/lti.rb', line 4

def self.validate!(decoded_token, requested_target_link_uri = nil, validate_target_link_url = false)
  if decoded_token.blank?
    raise AtomicLti::Exceptions::InvalidLTIToken
  end

  errors = []

  if decoded_token["iss"].blank?
    errors.push("LTI token is missing required field iss")
  end

  if decoded_token["sub"].blank? && !AtomicLti.allow_anonymous_user
    errors.push("LTI token is missing required field sub")
  end

  if decoded_token["aud"].blank?
    errors.push("LTI token is missing required field aud")
  end

  if decoded_token["aud"].is_a?(Array) && decoded_token["aud"].length > 1
    # OpenID Connect spec specifies the AZP should exist and be an AUD
    if decoded_token["azp"].blank?
      errors.push("LTI token has multiple aud and is missing required field azp")
    elsif decoded_token["aud"].exclude?(decoded_token["azp"])
      errors.push("LTI token azp is not one of the aud's")
    end
  end

  if decoded_token[AtomicLti::Definitions::DEPLOYMENT_ID].blank?
    errors.push(
      "LTI token is missing required field #{AtomicLti::Definitions::DEPLOYMENT_ID}"
    )
  end

  if decoded_token[AtomicLti::Definitions::MESSAGE_TYPE].blank?
    errors.push(
      "LTI token is missing required claim #{AtomicLti::Definitions::MESSAGE_TYPE}"
    )
  end

  if decoded_token[AtomicLti::Definitions::MESSAGE_TYPE] === "LtiResourceLinkRequest"
    errors.concat(validate_resource_link_request(decoded_token, requested_target_link_uri, validate_target_link_url))
  end

  if decoded_token[AtomicLti::Definitions::ROLES_CLAIM].blank?
    errors.push(
      "LTI token is missing required claim #{AtomicLti::Definitions::ROLES_CLAIM}"
    )
  end

  roles = decoded_token[AtomicLti::Definitions::ROLES_CLAIM]
  if AtomicLti.role_enforcement_mode == AtomicLti::RoleEnforcementMode::STRICT && roles.is_a?(Array) && !roles.empty?
    invalid_roles = roles - AtomicLti::Definitions::ROLES
    if invalid_roles.length == roles.length
      errors.push("LTI token has invalid roles: #{invalid_roles.join(", ")}")
    end
  end

  if errors.length > 0
    raise AtomicLti::Exceptions::InvalidLTIToken.new(errors.join(" "))
  end

  if decoded_token[AtomicLti::Definitions::LTI_VERSION].blank?
    raise AtomicLti::Exceptions::NoLTIVersion
  end

  raise AtomicLti::Exceptions::InvalidLTIVersion unless valid_version?(decoded_token)

  true
end


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'app/lib/atomic_lti/lti.rb', line 75

def self.validate_resource_link_request(decoded_token, requested_target_link_uri = nil, validate_target_link_url = false)
  errors = []

  if decoded_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM].blank?
    errors.push(
      "LTI token is missing required claim #{AtomicLti::Definitions::TARGET_LINK_URI_CLAIM}",
    )
  end

  # Validate that we are at the target_link_uri
  target_link_uri = decoded_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM]

  if validate_target_link_url &&
      !matching_uri?(target_link_uri, requested_target_link_uri, ignore_host: AtomicLti.update_target_link_host)
    errors.push(
      "LTI token target link uri '#{target_link_uri}' doesn't match url '#{requested_target_link_uri}'",
    )
  end

  if decoded_token[AtomicLti::Definitions::RESOURCE_LINK_CLAIM].blank?
    errors.push(
      "LTI token is missing required claim #{AtomicLti::Definitions::RESOURCE_LINK_CLAIM}",
    )
  end

  if decoded_token.dig(AtomicLti::Definitions::RESOURCE_LINK_CLAIM, "id").blank?
    errors.push(
      "LTI token is missing required field id from the claim #{AtomicLti::Definitions::RESOURCE_LINK_CLAIM}",
    )
  end

  errors
end