diff --git a/README.md b/README.md
index b0331d2c..58faccd1 100644
--- a/README.md
+++ b/README.md
@@ -199,7 +199,7 @@ $resource = LTI\LTI_Deep_Link_Resource::new()
Everything is set to return the resource to the platform. There are two methods of doing this.
-The following method will output the html for an aut-posting form for you.
+The following method will output the html for an auto-posting form for you.
```php
$dl->output_response_form([$resource]);
```
@@ -209,6 +209,12 @@ Alternatively you can just request the signed JWT that will need posting back to
$dl->get_response_jwt([$resource]);
```
+If you've created a JWKS endpoint with `LTI\JWKS_Endpoint::new()`, the kid used in the endpoint can be provided as an additional parameter.
+```php
+$dl->get_response_jwt([$resource], 'a_unique_KID');
+
+```
+
## Calling Services
### Names and Roles Service
diff --git a/composer.json b/composer.json
index 804cdc3b..26135b70 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
"name": "imsglobal/lti-1p3-tool",
"type": "library",
"require": {
- "fproject/php-jwt": "^4.0",
+ "firebase/php-jwt": "^6",
"phpseclib/phpseclib": "^2.0"
},
"autoload": {
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 00000000..83ae3127
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,19 @@
+parameters:
+ parallel:
+ maximumNumberOfProcesses: 1
+ tmpDir: /tmp/phpstan-%env.USER%
+ ignoreErrors:
+ - '%Access to constant PUBLIC_FORMAT_PKCS8 on an unknown class phpseclib\\Crypt\\RSA\.%'
+ - '%Access to property \$modulus on an unknown class phpseclib\\Crypt\\RSA%'
+ - '%Access to property \$publicExponent on an unknown class phpseclib\\Crypt\\RSA.%'
+ - '%Access to static property \$leeway on an unknown class Firebase\\JWT\\JWT\.%'
+ - '%Call to method loadKey\(\) on an unknown class phpseclib\\Crypt\\RSA\.%'
+ - '%Call to method setHash\(\) on an unknown class phpseclib\\Crypt\\RSA\.%'
+ - '%Call to method setPublicKey\(\) on an unknown class phpseclib\\Crypt\\RSA\.%'
+ - '%Call to static method decode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
+ - '%Call to static method encode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
+ - '%Call to static method parseKey\(\) on an unknown class Firebase\\JWT\\JWK\.%'
+ - '%Call to static method urlsafeB64Decode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
+ - '%Call to static method urlsafeB64Encode\(\) on an unknown class Firebase\\JWT\\JWT\.%'
+ - '%Instantiated class Firebase\\JWT\\Key not found\.%'
+ - '%Instantiated class phpseclib\\Crypt\\RSA not found\.%'
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 00000000..c9eafc0e
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ src/lti
+ src/lti/message_validators
+
+
+
+
+ tests
+
+
+
diff --git a/src/lti/Cache.php b/src/lti/Cache.php
index 0d865711..166a99bc 100644
--- a/src/lti/Cache.php
+++ b/src/lti/Cache.php
@@ -3,44 +3,57 @@
class Cache {
- private $cache;
+ /** @var array $cache */
+ private array $cache;
- public function get_launch_data($key) {
+ public function get_launch_data(string $key): mixed {
$this->load_cache();
return $this->cache[$key];
}
- public function cache_launch_data($key, $jwt_body) {
+ /**
+ * @param array $jwt_body
+ */
+ public function cache_launch_data(string $key, array $jwt_body): self {
$this->cache[$key] = $jwt_body;
$this->save_cache();
return $this;
}
- public function cache_nonce($nonce) {
- $this->cache['nonce'][$nonce] = true;
+ public function cache_nonce(string $nonce): self {
+ $this->cache['nonce'][$nonce] = false;
$this->save_cache();
return $this;
}
- public function check_nonce($nonce) {
+ public function check_nonce(string $nonce): bool {
$this->load_cache();
if (!isset($this->cache['nonce'][$nonce])) {
return false;
}
+ if ($this->cache['nonce'][$nonce]) {
+ return false;
+ }
+ $this->cache['nonce'][$nonce] = true;
+ $this->save_cache();
return true;
}
- private function load_cache() {
+ private function load_cache(): void {
$cache = file_get_contents(sys_get_temp_dir() . '/lti_cache.txt');
- if (empty($cache)) {
+ if ($cache === false) {
file_put_contents(sys_get_temp_dir() . '/lti_cache.txt', '{}');
$this->cache = [];
+ return;
+ }
+ $data = json_decode($cache, true);
+ if ($data === false) {
+ throw new \Exception("Failed to decode cache contents.");
}
- $this->cache = json_decode($cache, true);
+ $this->cache = $data;
}
- private function save_cache() {
+ private function save_cache(): void {
file_put_contents(sys_get_temp_dir() . '/lti_cache.txt', json_encode($this->cache));
}
}
-?>
\ No newline at end of file
diff --git a/src/lti/Cookie.php b/src/lti/Cookie.php
index e18700b6..97de03f5 100644
--- a/src/lti/Cookie.php
+++ b/src/lti/Cookie.php
@@ -2,7 +2,10 @@
namespace IMSGlobal\LTI;
class Cookie {
- public function get_cookie($name) {
+ /**
+ * @return string|false
+ */
+ public function get_cookie(string $name) {
if (isset($_COOKIE[$name])) {
return $_COOKIE[$name];
}
@@ -13,7 +16,10 @@ public function get_cookie($name) {
return false;
}
- public function set_cookie($name, $value, $exp = 3600, $options = []) {
+ /**
+ * @param array $options
+ */
+ public function set_cookie(string $name, string $value, int $exp = 3600, array $options = []): self {
$cookie_options = [
'expires' => time() + $exp
];
@@ -31,4 +37,3 @@ public function set_cookie($name, $value, $exp = 3600, $options = []) {
return $this;
}
}
-?>
diff --git a/src/lti/Database.php b/src/lti/Database.php
index 945afd67..68348631 100644
--- a/src/lti/Database.php
+++ b/src/lti/Database.php
@@ -2,8 +2,6 @@
namespace IMSGlobal\LTI;
interface Database {
- public function find_registration_by_issuer($iss);
- public function find_deployment($iss, $deployment_id);
+ public function find_registration_by_issuer(string $iss, ?string $client_id): ?LTI_Registration;
+ public function find_deployment(string $iss, string $deployment_id): ?LTI_Deployment;
}
-
-?>
\ No newline at end of file
diff --git a/src/lti/JWKS_Endpoint.php b/src/lti/JWKS_Endpoint.php
index 8e5771b8..e9685b8d 100644
--- a/src/lti/JWKS_Endpoint.php
+++ b/src/lti/JWKS_Endpoint.php
@@ -6,32 +6,51 @@
class JWKS_Endpoint {
- private $keys;
+ /** @var array $keys */
+ private array $keys;
+ /**
+ * @param array $keys
+ */
public function __construct(array $keys) {
$this->keys = $keys;
}
- public static function new($keys) {
+ /**
+ * @param array $keys
+ */
+ public static function new(array $keys): self {
return new JWKS_Endpoint($keys);
}
- public static function from_issuer(Database $database, $issuer) {
- $registration = $database->find_registration_by_issuer($issuer);
- return new JWKS_Endpoint([$registration->get_kid() => $registration->get_tool_private_key()]);
+ public static function from_issuer(
+ Database $database,
+ string $issuer,
+ string $client_id
+ ): self {
+ $registration = $database->find_registration_by_issuer($issuer, $client_id);
+ if ($registration === null) {
+ throw new LTI_Exception("Could not find registration");
+ }
+ return new self([$registration->get_kid() => $registration->get_tool_private_key()]);
}
- public static function from_registration(LTI_Registration $registration) {
- return new JWKS_Endpoint([$registration->get_kid() => $registration->get_tool_private_key()]);
+ public static function from_registration(
+ LTI_Registration $registration
+ ): self {
+ return new self([$registration->get_kid() => $registration->get_tool_private_key()]);
}
- public function get_public_jwks() {
+ /**
+ * @return array
+ */
+ public function get_public_jwks(): array {
$jwks = [];
foreach ($this->keys as $kid => $private_key) {
$key = new RSA();
$key->setHash("sha256");
$key->loadKey($private_key);
- $key->setPublicKey(false, RSA::PUBLIC_FORMAT_PKCS8);
+ $key->setPublicKey("", RSA::PUBLIC_FORMAT_PKCS8);
if ( !$key->publicExponent ) {
continue;
}
@@ -48,8 +67,8 @@ public function get_public_jwks() {
return ['keys' => $jwks];
}
- public function output_jwks() {
+ public function output_jwks(): void {
echo json_encode($this->get_public_jwks());
}
-}
\ No newline at end of file
+}
diff --git a/src/lti/LTI_Assignments_Grades_Service.php b/src/lti/LTI_Assignments_Grades_Service.php
index ffd1cde0..a06ac6df 100644
--- a/src/lti/LTI_Assignments_Grades_Service.php
+++ b/src/lti/LTI_Assignments_Grades_Service.php
@@ -2,16 +2,22 @@
namespace IMSGlobal\LTI;
class LTI_Assignments_Grades_Service {
+ private LTI_Service_Connector $service_connector;
+ /** @var array $service_data */
+ private array $service_data;
- private $service_connector;
- private $service_data;
-
- public function __construct(LTI_Service_Connector $service_connector, $service_data) {
+ /**
+ * @param array $service_data
+ */
+ public function __construct(LTI_Service_Connector $service_connector, array $service_data) {
$this->service_connector = $service_connector;
$this->service_data = $service_data;
}
- public function put_grade(LTI_Grade $grade, LTI_Lineitem $lineitem = null) {
+ /**
+ * @return array
+ */
+ public function put_grade(LTI_Grade $grade, ?LTI_Lineitem $lineitem = null): array {
if (!in_array("https://purl.imsglobal.org/spec/lti-ags/scope/score", $this->service_data['scope'])) {
throw new LTI_Exception('Missing required scope', 1);
}
@@ -24,7 +30,7 @@ public function put_grade(LTI_Grade $grade, LTI_Lineitem $lineitem = null) {
} else {
$lineitem = LTI_Lineitem::new()
->set_label('default')
- ->set_score_maximum(100);
+ ->set_score_maximum('100');
$lineitem = $this->find_or_create_lineitem($lineitem);
$score_url = $lineitem->get_id();
}
@@ -41,7 +47,7 @@ public function put_grade(LTI_Grade $grade, LTI_Lineitem $lineitem = null) {
);
}
- public function find_or_create_lineitem(LTI_Lineitem $new_line_item) {
+ public function find_or_create_lineitem(LTI_Lineitem $new_line_item): LTI_Lineitem {
if (!in_array("https://purl.imsglobal.org/spec/lti-ags/scope/lineitem", $this->service_data['scope'])) {
throw new LTI_Exception('Missing required scope', 1);
}
@@ -50,7 +56,7 @@ public function find_or_create_lineitem(LTI_Lineitem $new_line_item) {
'GET',
$this->service_data['lineitems'],
null,
- null,
+ 'application/json',
'application/vnd.ims.lis.v2.lineitemcontainer+json'
);
foreach ($line_items['body'] as $line_item) {
@@ -71,7 +77,10 @@ public function find_or_create_lineitem(LTI_Lineitem $new_line_item) {
return new LTI_Lineitem($created_line_item['body']);
}
- public function get_grades(LTI_Lineitem $lineitem) {
+ /**
+ * @return array
+ */
+ public function get_grades(LTI_Lineitem $lineitem): array {
$lineitem = $this->find_or_create_lineitem($lineitem);
// Place '/results' before url params
$pos = strpos($lineitem->get_id(), '?');
@@ -81,11 +90,10 @@ public function get_grades(LTI_Lineitem $lineitem) {
'GET',
$results_url,
null,
- null,
+ 'application/json',
'application/vnd.ims.lis.v2.resultcontainer+json'
);
return $scores['body'];
}
}
-?>
\ No newline at end of file
diff --git a/src/lti/LTI_Course_Groups_Service.php b/src/lti/LTI_Course_Groups_Service.php
index b9a878c3..16490d46 100644
--- a/src/lti/LTI_Course_Groups_Service.php
+++ b/src/lti/LTI_Course_Groups_Service.php
@@ -3,15 +3,22 @@
class LTI_Course_Groups_Service {
- private $service_connector;
- private $service_data;
-
- public function __construct(LTI_Service_Connector $service_connector, $service_data) {
+ private LTI_Service_Connector $service_connector;
+ /** @var array $service_data */
+ private array $service_data;
+
+ /**
+ * @param array $service_data
+ */
+ public function __construct(LTI_Service_Connector $service_connector, array $service_data) {
$this->service_connector = $service_connector;
$this->service_data = $service_data;
}
- public function get_groups() {
+ /**
+ * @return array
+ */
+ public function get_groups(): array {
$groups = [];
@@ -23,7 +30,7 @@ public function get_groups() {
'GET',
$next_page,
null,
- null,
+ 'application/json',
'application/vnd.ims.lti-gs.v1.contextgroupcontainer+json'
);
@@ -41,7 +48,10 @@ public function get_groups() {
}
- public function get_sets() {
+ /**
+ * @return array
+ */
+ public function get_sets(): array {
$sets = [];
@@ -58,7 +68,7 @@ public function get_sets() {
'GET',
$next_page,
null,
- null,
+ 'application/json',
'application/vnd.ims.lti-gs.v1.contextgroupcontainer+json'
);
@@ -76,6 +86,9 @@ public function get_sets() {
}
+ /**
+ * @return array
+ */
public function get_groups_by_set() {
$groups = $this->get_groups();
$sets = $this->get_sets();
@@ -107,4 +120,3 @@ public function get_groups_by_set() {
return $groups_by_set;
}
}
-?>
\ No newline at end of file
diff --git a/src/lti/LTI_Deep_Link.php b/src/lti/LTI_Deep_Link.php
index c87cb0da..cfcabde7 100644
--- a/src/lti/LTI_Deep_Link.php
+++ b/src/lti/LTI_Deep_Link.php
@@ -2,22 +2,34 @@
namespace IMSGlobal\LTI;
use \Firebase\JWT\JWT;
+
class LTI_Deep_Link {
- private $registration;
- private $deployment_id;
- private $deep_link_settings;
+ private LTI_Registration $registration;
+ private string $deployment_id;
+ /** @var array $deep_link_settings */
+ private array $deep_link_settings;
- public function __construct($registration, $deployment_id, $deep_link_settings) {
+ /**
+ * @param array $deep_link_settings
+ */
+ public function __construct(
+ LTI_Registration $registration,
+ string $deployment_id,
+ array $deep_link_settings
+ ) {
$this->registration = $registration;
$this->deployment_id = $deployment_id;
$this->deep_link_settings = $deep_link_settings;
}
- public function get_response_jwt($resources) {
+ /**
+ * @param array $resources
+ */
+ public function get_response_jwt(array $resources, ?string $kid = null): string {
$message_jwt = [
"iss" => $this->registration->get_client_id(),
- "aud" => [$this->registration->get_issuer()],
+ "aud" => $this->registration->get_issuer(),
"exp" => time() + 600,
"iat" => time(),
"nonce" => 'nonce' . hash('sha256', random_bytes(64)),
@@ -27,11 +39,20 @@ public function get_response_jwt($resources) {
"https://purl.imsglobal.org/spec/lti-dl/claim/content_items" => array_map(function($resource) { return $resource->to_array(); }, $resources),
"https://purl.imsglobal.org/spec/lti-dl/claim/data" => $this->deep_link_settings['data'],
];
- return JWT::encode($message_jwt, $this->registration->get_tool_private_key(), 'RS256', $this->registration->get_kid());
+
+ return JWT::encode(
+ $message_jwt,
+ $this->registration->get_tool_private_key(),
+ 'RS256',
+ is_null($kid) ? $this->registration->get_kid() : $kid
+ );
}
- public function output_response_form($resources) {
- $jwt = $this->get_response_jwt($resources);
+ /**
+ * @param array $resources
+ */
+ public function output_response_form(array $resources, ?string $kid = null): void {
+ $jwt = $this->get_response_jwt($resources, $kid);
?>