-
Notifications
You must be signed in to change notification settings - Fork 107
Expand file tree
/
Copy pathLTI_Message_Launch.php
More file actions
371 lines (313 loc) · 13.6 KB
/
Copy pathLTI_Message_Launch.php
File metadata and controls
371 lines (313 loc) · 13.6 KB
1
2
3
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
74
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
<?php
namespace IMSGlobal\LTI;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
JWT::$leeway = 5;
class LTI_Message_Launch {
private $db;
private $cache;
private $request;
private $cookie;
private $jwt;
private $registration;
private $launch_id;
/**
* Constructor
*
* @param Database $database Instance of the database interface used for looking up registrations and deployments.
* @param Cache $cache Instance of the Cache interface used to loading and storing launches. If non is provided launch data will be store in $_SESSION.
* @param Cookie $cookie Instance of the Cookie interface used to set and read cookies. Will default to using $_COOKIE and setcookie.
*/
function __construct(Database $database, Cache $cache = null, Cookie $cookie = null) {
$this->db = $database;
$this->launch_id = uniqid("lti1p3_launch_", true);
if ($cache === null) {
$cache = new Cache();
}
$this->cache = $cache;
if ($cookie === null) {
$cookie = new Cookie();
}
$this->cookie = $cookie;
}
/**
* Static function to allow for method chaining without having to assign to a variable first.
*/
public static function new(Database $database, Cache $cache = null, Cookie $cookie = null) {
return new static($database, $cache, $cookie);
}
/**
* Load an LTI_Message_Launch from a Cache using a launch id.
*
* @param string $launch_id The launch id of the LTI_Message_Launch object that is being pulled from the cache.
* @param Database $database Instance of the database interface used for looking up registrations and deployments.
* @param Cache $cache Instance of the Cache interface used to loading and storing launches. If non is provided launch data will be store in $_SESSION.
*
* @throws LTI_Exception Will throw an LTI_Exception if validation fails or launch cannot be found.
* @return LTI_Message_Launch A populated and validated LTI_Message_Launch.
*/
public static function from_cache($launch_id, Database $database, Cache $cache = null) {
$new = new static($database, $cache, null);
$new->launch_id = $launch_id;
$new->jwt = [ 'body' => $new->cache->get_launch_data($launch_id) ];
return $new->validate_registration();
}
/**
* Validates all aspects of an incoming LTI message launch and caches the launch if successful.
*
* @param array|string $request An array of post request parameters. If not set will default to $_POST.
*
* @throws LTI_Exception Will throw an LTI_Exception if validation fails.
* @return LTI_Message_Launch Will return $this if validation is successful.
*/
public function validate(array $request = null) {
if ($request === null) {
$request = $_POST;
}
$this->request = $request;
return $this->validate_state()
->validate_jwt_format()
->validate_nonce()
->validate_registration()
->validate_jwt_signature()
->validate_deployment()
->validate_message()
->cache_launch_data();
}
/**
* Returns whether or not the current launch can use the names and roles service.
*
* @return boolean Returns a boolean indicating the availability of names and roles.
*/
public function has_nrps() {
return !empty($this->jwt['body']['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice']['context_memberships_url']);
}
/**
* Fetches an instance of the names and roles service for the current launch.
*
* @return LTI_Names_Roles_Provisioning_Service An instance of the names and roles service that can be used to make calls within the scope of the current launch.
*/
public function get_nrps() {
return new LTI_Names_Roles_Provisioning_Service(
new LTI_Service_Connector($this->registration),
$this->jwt['body']['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice']);
}
/**
* Returns whether or not the current launch can use the groups service.
*
* @return boolean Returns a boolean indicating the availability of groups.
*/
public function has_gs() {
return !empty($this->jwt['body']['https://purl.imsglobal.org/spec/lti-gs/claim/groupsservice']['context_groups_url']);
}
/**
* Fetches an instance of the groups service for the current launch.
*
* @return LTI_Course_Groups_Service An instance of the groups service that can be used to make calls within the scope of the current launch.
*/
public function get_gs() {
return new LTI_Course_Groups_Service(
new LTI_Service_Connector($this->registration),
$this->jwt['body']['https://purl.imsglobal.org/spec/lti-gs/claim/groupsservice']);
}
/**
* Returns whether or not the current launch can use the assignments and grades service.
*
* @return boolean Returns a boolean indicating the availability of assignments and grades.
*/
public function has_ags() {
return !empty($this->jwt['body']['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint']);
}
/**
* Fetches an instance of the assignments and grades service for the current launch.
*
* @return LTI_Assignments_Grades_Service An instance of the assignments an grades service that can be used to make calls within the scope of the current launch.
*/
public function get_ags() {
return new LTI_Assignments_Grades_Service(
new LTI_Service_Connector($this->registration),
$this->jwt['body']['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint']);
}
/**
* Fetches a deep link that can be used to construct a deep linking response.
*
* @return LTI_Deep_Link An instance of a deep link to construct a deep linking response for the current launch.
*/
public function get_deep_link() {
return new LTI_Deep_Link(
$this->registration,
$this->jwt['body']['https://purl.imsglobal.org/spec/lti/claim/deployment_id'],
$this->jwt['body']['https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings']);
}
/**
* Returns whether or not the current launch is a deep linking launch.
*
* @return boolean Returns true if the current launch is a deep linking launch.
*/
public function is_deep_link_launch() {
return $this->jwt['body']['https://purl.imsglobal.org/spec/lti/claim/message_type'] === 'LtiDeepLinkingRequest';
}
/**
* Returns whether or not the current launch is a submission review launch.
*
* @return boolean Returns true if the current launch is a submission review launch.
*/
public function is_submission_review_launch() {
return $this->jwt['body']['https://purl.imsglobal.org/spec/lti/claim/message_type'] === 'LtiSubmissionReviewRequest';
}
/**
* Returns whether or not the current launch is a resource launch.
*
* @return boolean Returns true if the current launch is a resource launch.
*/
public function is_resource_launch() {
return $this->jwt['body']['https://purl.imsglobal.org/spec/lti/claim/message_type'] === 'LtiResourceLinkRequest';
}
/**
* Fetches the decoded body of the JWT used in the current launch.
*
* @return array|object Returns the decoded json body of the launch as an array.
*/
public function get_launch_data() {
return $this->jwt['body'];
}
/**
* Get the unique launch id for the current launch.
*
* @return string A unique identifier used to re-reference the current launch in subsequent requests.
*/
public function get_launch_id() {
return $this->launch_id;
}
private function get_public_key() {
$key_set_url = $this->registration->get_key_set_url();
// Download key set
$public_key_set = json_decode(file_get_contents($key_set_url), true);
if (empty($public_key_set)) {
// Failed to fetch public keyset from URL.
throw new LTI_Exception("Failed to fetch public key", 1);
}
// Find key used to sign the JWT (matches the KID in the header)
foreach ($public_key_set['keys'] as $key) {
if ($key['kid'] == $this->jwt['header']['kid']) {
try {
return openssl_pkey_get_details(JWK::parseKey($key));
} catch(\Exception $e) {
return false;
}
}
}
// Could not find public key with a matching kid and alg.
throw new LTI_Exception("Unable to find public key", 1);
}
private function cache_launch_data() {
$this->cache->cache_launch_data($this->launch_id, $this->jwt['body']);
return $this;
}
private function validate_state() {
// Check State for OIDC.
if ($this->cookie->get_cookie('lti1p3_' . $this->request['state']) !== $this->request['state']) {
// Error if state doesn't match
throw new LTI_Exception("State not found", 1);
}
return $this;
}
private function validate_jwt_format() {
$jwt = $this->request['id_token'];
if (empty($jwt)) {
throw new LTI_Exception("Missing id_token", 1);
}
// Get parts of JWT.
$jwt_parts = explode('.', $jwt);
if (count($jwt_parts) !== 3) {
// Invalid number of parts in JWT.
throw new LTI_Exception("Invalid id_token, JWT must contain 3 parts", 1);
}
// Decode JWT headers.
$this->jwt['header'] = json_decode(JWT::urlsafeB64Decode($jwt_parts[0]), true);
// Decode JWT Body.
$this->jwt['body'] = json_decode(JWT::urlsafeB64Decode($jwt_parts[1]), true);
return $this;
}
private function validate_nonce() {
if (!$this->cache->check_nonce($this->jwt['body']['nonce'])) {
throw new LTI_Exception("Invalid Nonce");
}
return $this;
}
private function validate_registration() {
// Find registration.
$this->registration = $this->db->find_registration_by_issuer($this->jwt['body']['iss']);
if (empty($this->registration)) {
throw new LTI_Exception("Registration not found.", 1);
}
// Check client id.
$client_id = is_array($this->jwt['body']['aud']) ? $this->jwt['body']['aud'][0] : $this->jwt['body']['aud'];
if ( $client_id !== $this->registration->get_client_id()) {
// Client not registered.
throw new LTI_Exception("Client id not registered for this issuer", 1);
}
return $this;
}
private function validate_jwt_signature() {
// Fetch public key.
$public_key = $this->get_public_key();
// Validate JWT signature
try {
JWT::decode($this->request['id_token'], $public_key['key'], array('RS256'));
} catch(\Exception $e) {
// Error validating signature.
throw new LTI_Exception("Invalid signature on id_token", 1);
}
return $this;
}
private function validate_deployment() {
// Find deployment.
$deployment = $this->db->find_deployment($this->jwt['body']['iss'], $this->jwt['body']['https://purl.imsglobal.org/spec/lti/claim/deployment_id']);
if (empty($deployment)) {
// deployment not recognized.
throw new LTI_Exception("Unable to find deployment", 1);
}
return $this;
}
private function validate_message() {
if (empty($this->jwt['body']['https://purl.imsglobal.org/spec/lti/claim/message_type'])) {
// Unable to identify message type.
throw new LTI_Exception("Invalid message type", 1);
}
// Do message type validation
// Import all validators
foreach (glob(__DIR__ . "/message_validators/*.php") as $filename) {
include_once $filename;
}
// Create instances of all validators
$classes = get_declared_classes();
$validators = array();
foreach ($classes as $class_name) {
// Check the class implements message validator
$reflect = new \ReflectionClass($class_name);
if ($reflect->implementsInterface('\IMSGlobal\LTI\Message_Validator')) {
// Create instance of class
$validators[] = new $class_name();
}
}
$message_validator = false;
foreach ($validators as $validator) {
if ($validator->can_validate($this->jwt['body'])) {
if ($message_validator !== false) {
// Can't have more than one validator apply at a time.
throw new LTI_Exception("Validator conflict", 1);
}
$message_validator = $validator;
}
}
if ($message_validator === false) {
throw new LTI_Exception("Unrecognized message type.", 1);
}
if (!$message_validator->validate($this->jwt['body'])) {
throw new LTI_Exception("Message validation failed.", 1);
}
return $this;
}
}
?>