36 public $ltiVersion =
null;
50 public $secret =
null;
57 public $signatureMethod =
'HMAC-SHA1';
64 public $encryptionMethod =
'';
71 public $dataConnector =
null;
81 public $rsaKey =
null;
88 public $requiredScopes = array();
109 public $reason =
null;
116 public $details = array();
123 public $warnings = array();
130 public $debugMode =
false;
137 public $enabled =
false;
144 public $enableFrom =
null;
151 public $enableUntil =
null;
158 public $lastAccess =
null;
165 public $created =
null;
172 public $updated =
null;
179 protected $jwt =
null;
186 protected $rawParameters =
null;
193 protected $messageParameters =
null;
214 private $settings =
null;
221 private $settingsChanged =
false;
273 if (array_key_exists($name, $this->settings)) {
274 $value = $this->settings[$name];
290 $old_value = $this->getSetting($name);
291 if ($value !== $old_value) {
292 if (!empty($value)) {
293 $this->settings[$name] = $value;
295 unset($this->settings[$name]);
297 $this->settingsChanged =
true;
308 return $this->settings;
318 $this->settings = $settings;
328 if ($this->settingsChanged) {
344 return !empty($this->jwt) && $this->jwt->hasJwt();
364 if (is_null($this->rawParameters)) {
368 return $this->rawParameters;
380 $messageClaims =
null;
381 if (!is_null($this->messageParameters)) {
382 $messageParameters = $this->messageParameters;
384 if (!empty($messageParameters[
'lti_message_type'])) {
388 $messageType = $messageParameters[
'lti_message_type'];
390 if (!empty($messageParameters[
'accept_media_types'])) {
391 $mediaTypes = array_map(
'trim', explode(
',', $messageParameters[
'accept_media_types']));
392 $mediaTypes = array_filter($mediaTypes);
394 if (!empty($messageParameters[
'accept_types'])) {
395 $types = array_map(
'trim', explode(
',', $this->messageParameters[
'accept_types']));
396 $types = array_filter($types);
397 foreach ($mediaTypes as $mediaType) {
398 if (strpos($mediaType,
'application/vnd.ims.lti.') === 0) {
399 unset($mediaTypes[array_search($mediaType, $mediaTypes)]);
402 $messageParameters[
'accept_media_types'] = implode(
',', $mediaTypes);
404 foreach ($mediaTypes as $mediaType) {
405 if ($mediaType === Item::LTI_LINK_MEDIA_TYPE) {
406 unset($mediaTypes[array_search(Item::LTI_LINK_MEDIA_TYPE, $mediaTypes)]);
407 $messageParameters[
'accept_media_types'] = implode(
',', $mediaTypes);
408 $types[] = Item::TYPE_LTI_LINK;
409 } elseif ($mediaType === Item::LTI_ASSIGNMENT_MEDIA_TYPE) {
410 unset($mediaTypes[array_search(Item::LTI_ASSIGNMENT_MEDIA_TYPE, $mediaTypes)]);
411 $messageParameters[
'accept_media_types'] = implode(
',', $mediaTypes);
412 $types[] = Item::TYPE_LTI_ASSIGNMENT;
413 } elseif (substr($mediaType, 0, 6) ===
'image/') {
417 } elseif ($mediaType ===
'text/html') {
421 } elseif ($mediaType ===
'*/*') {
430 $types = array_unique($types);
431 $messageParameters[
'accept_types'] = implode(
',', $types);
434 if (!empty($messageParameters[
'accept_presentation_document_targets'])) {
435 $documentTargets = array_map(
'trim', explode(
',', $messageParameters[
'accept_presentation_document_targets']));
436 $documentTargets = array_filter($documentTargets);
438 foreach ($documentTargets as $documentTarget) {
439 switch ($documentTarget) {
446 $targets[] = $documentTarget;
450 $targets = array_unique($targets);
451 $messageParameters[
'accept_presentation_document_targets'] = implode(
',', $targets);
453 $messageClaims = array();
454 if (!empty($messageParameters[
'oauth_consumer_key'])) {
455 $messageClaims[
'aud'] = array($messageParameters[
'oauth_consumer_key']);
457 foreach ($messageParameters as $key => $value) {
465 if (isset($mapping[
'isObject']) && $mapping[
'isObject']) {
467 } elseif (isset($mapping[
'isArray']) && $mapping[
'isArray']) {
468 $value = array_map(
'trim', explode(
',', $value));
469 $value = array_filter($value);
471 } elseif (isset($mapping[
'isBoolean']) && $mapping[
'isBoolean']) {
472 $value = (is_bool($value)) ? $value : $value ===
'true';
473 } elseif (isset($mapping[
'isInteger']) && $mapping[
'isInteger']) {
474 $value = intval($value);
475 } elseif (is_bool($value)) {
476 $value = ($value) ?
'true' :
'false';
478 $value = strval($value);
482 if (!empty($mapping[
'suffix'])) {
483 $claim .=
"-{$mapping['suffix']}";
486 if (is_null($mapping[
'group'])) {
487 $claim = $mapping[
'claim'];
488 } elseif (empty($mapping[
'group'])) {
489 $claim .= $mapping[
'claim'];
491 $group = $claim . $mapping[
'group'];
492 $claim = $mapping[
'claim'];
494 } elseif (substr($key, 0, 7) ===
'custom_') {
496 $claim = substr($key, 7);
497 } elseif (substr($key, 0, 4) ===
'ext_') {
498 if ($key ===
'ext_d2l_username') {
499 $group =
'http://www.brightspace.com';
503 $claim = substr($key, 4);
505 } elseif (substr($key, 0, 7) ===
'lti1p1_') {
507 $claim = substr($key, 7);
512 if (!is_null($json)) {
520 if ($fullyQualified) {
522 $messageClaims = array_merge($messageClaims, self::fullyQualifyClaim($claim, $value));
524 $messageClaims = array_merge($messageClaims, self::fullyQualifyClaim(
"{$group}/{$claim}", $value));
526 } elseif (empty($group)) {
527 $messageClaims[$claim] = $value;
529 $messageClaims[$group][$claim] = $value;
533 if (!empty($messageParameters[
'unmapped_claims'])) {
535 foreach ($claims as $claim => $value) {
536 if ($fullyQualified) {
537 $messageClaims = array_merge($messageClaims, self::fullyQualifyClaim($claim, $value));
538 } elseif (!is_object($value)) {
539 $messageClaims[$claim] = $value;
540 } elseif (!isset($messageClaims[$claim])) {
541 $messageClaims[$claim] = $value;
543 $objVars = get_object_vars($value);
544 foreach ($objVars as $attrName => $attrValue) {
545 if (is_object($messageClaims[$claim])) {
546 $messageClaims[$claim]->{$attrName} = $attrValue;
548 $messageClaims[$claim][$attrName] = $attrValue;
556 return $messageClaims;
570 if (!is_array($roles)) {
571 $roles = array_map(
'trim', explode(
',', $roles));
572 $roles = array_filter($roles);
574 $parsedRoles = array();
575 foreach ($roles as $role) {
577 if ((substr($role, 0, 4) !==
'urn:') &&
578 (substr($role, 0, 7) !==
'http://') && (substr($role, 0, 8) !==
'https://')) {
579 switch ($ltiVersion) {
581 $role = str_replace(
'#',
'/', $role);
582 $role =
"urn:lti:role:ims/lis/{$role}";
586 $pos = strrpos($role,
'#');
587 if ($pos ===
false) {
592 $role =
"http://purl.imsglobal.org/vocab/lis/v2/membership{$sep}{$role}";
596 $systemRoles = array(
604 $institutionRoles = array(
616 'ProspectiveStudent',
620 switch ($ltiVersion) {
622 if (in_array(substr($role, 0, 53),
623 array(
'http://purl.imsglobal.org/vocab/lis/v2/system/person#',
624 'http://purl.imsglobal.org/vocab/lis/v2/system/person/'))) {
625 $role =
'urn:lti:sysrole:ims/lis/' . substr($role, 53);
626 } elseif (in_array(substr($role, 0, 58),
627 array(
'http://purl.imsglobal.org/vocab/lis/v2/institution/person#',
628 'http://purl.imsglobal.org/vocab/lis/v2/institution/person/'))) {
629 $role =
'urn:lti:instrole:ims/lis/' . substr($role, 58);
630 } elseif (substr($role, 0, 50) ===
'http://purl.imsglobal.org/vocab/lis/v2/membership#') {
631 $principalRole = substr($role, 50);
632 if (($principalRole ===
'Instructor') &&
633 (!empty(preg_grep(
'/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/Instructor#TeachingAssistant.*$/',
635 !empty(preg_grep(
'/^Instructor#TeachingAssistant.*$/', $roles)))) {
637 } elseif (!empty(preg_grep(
"/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/{$principalRole}#.*$/",
639 !empty(preg_grep(
'/^{$principalRole}#.*$/', $roles))) {
642 $role =
"urn:lti:role:ims/lis/{$principalRole}";
644 } elseif (substr($role, 0, 50) ===
'http://purl.imsglobal.org/vocab/lis/v2/membership/') {
645 $subroles = explode(
'#', substr($role, 50));
646 if (count($subroles) === 2) {
647 if (($subroles[0] ===
'Instructor') && ($subroles[1] ===
'TeachingAssistant')) {
648 $role =
'urn:lti:role:ims/lis/TeachingAssistant';
649 } elseif (($subroles[0] ===
'Instructor') && (substr($subroles[1], 0, 17) ===
'TeachingAssistant')) {
650 $role =
"urn:lti:role:ims/lis/TeachingAssistant#{$subroles[1]}";
652 $role =
"urn:lti:role:ims/lis/{$subroles[0]}/{$subroles[1]}";
655 $role =
'urn:lti:role:ims/lis/' . substr($role, 50);
657 } elseif (in_array(substr($role, 0, 46),
658 array(
'http://purl.imsglobal.org/vocab/lis/v2/person#',
659 'http://purl.imsglobal.org/vocab/lis/v2/person/'))) {
660 if (in_array(substr($role, 46), $systemRoles)) {
661 $role =
'urn:lti:sysrole:ims/lis/' . substr($role, 46);
662 } elseif (in_array(substr($role, 46), $institutionRoles)) {
663 $role =
'urn:lti:instrole:ims/lis/' . substr($role, 46);
665 } elseif (strpos($role,
'Instructor#TeachingAssistant') !==
false) {
666 if (substr($role, -28) ===
'Instructor#TeachingAssistant') {
667 $role = str_replace(
'Instructor#',
'', $role);
669 $role = str_replace(
'Instructor#',
'TeachingAssistant/', $role);
671 } elseif ((substr($role, -10) ===
'Instructor') &&
672 !empty(preg_grep(
'/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/Instructor#TeachingAssistant.*$/',
676 $role = str_replace(
'#',
'/', $role);
680 if (substr($role, 0, 24) ===
'urn:lti:sysrole:ims/lis/') {
681 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/person';
682 $role = substr($role, 24);
683 } elseif (substr($role, 0, 25) ===
'urn:lti:instrole:ims/lis/') {
684 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/person';
685 $role = substr($role, 25);
686 } elseif (substr($role, 0, 21) ===
'urn:lti:role:ims/lis/') {
687 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/membership';
688 $subroles = explode(
'/', substr($role, 21));
689 if (count($subroles) === 2) {
690 if (($subroles[0] ===
'Instructor') && ($subroles[1] ===
'TeachingAssistant')) {
691 $role =
'TeachingAssistant';
692 } elseif (($subroles[0] ===
'Instructor') && (substr($subroles[1], 0, 17) ===
'TeachingAssistant')) {
693 $role =
"TeachingAssistant#{$subroles[1]}";
695 $role =
"{$subroles[0]}#{$subroles[1]}";
697 } elseif ((count($subroles) === 1) && (!empty(preg_grep(
"/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/{$subroles[0]}#.*$/",
699 !empty(preg_grep(
'/^{$subroles[0]#.*$/', $roles)))) {
702 $role = substr($role, 21);
704 } elseif (in_array(substr($role, 0, 53),
705 array(
'http://purl.imsglobal.org/vocab/lis/v2/system/person#',
706 'http://purl.imsglobal.org/vocab/lis/v2/system/person/'))) {
707 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/person';
708 $role = substr($role, 53);
709 } elseif (in_array(substr($role, 0, 58),
710 array(
'http://purl.imsglobal.org/vocab/lis/v2/institution/person#',
711 'http://purl.imsglobal.org/vocab/lis/v2/institution/person/'))) {
712 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/person';
713 $role = substr($role, 58);
714 } elseif (substr($role, 0, 50) ===
'http://purl.imsglobal.org/vocab/lis/v2/membership#') {
715 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/membership';
716 $principalRole = substr($role, 50);
717 $principalRole2 = str_replace(
'/',
'\\/', $principalRole);
718 if (($principalRole ===
'Instructor') &&
719 (!empty(preg_grep(
'/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/Instructor#TeachingAssistant.*$/',
721 !empty(preg_grep(
'/^Instructor#TeachingAssistant.*$/', $roles)))) {
723 } elseif (!empty(preg_grep(
"/^http:\/\/purl.imsglobal.org\/vocab\/lis\/v2\/membership\/{$principalRole2}#.*$/",
725 !empty(preg_grep(
'/^{$principalRole2}#.*$/', $roles))) {
728 $role = $principalRole;
730 } elseif (substr($role, 0, 50) ===
'http://purl.imsglobal.org/vocab/lis/v2/membership/') {
731 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/membership';
732 $subroles = explode(
'#', substr($role, 50));
733 if (count($subroles) === 2) {
734 if (($subroles[0] ===
'Instructor') && ($subroles[1] ===
'TeachingAssistant')) {
735 $role =
'TeachingAssistant';
736 } elseif (($subroles[0] ===
'Instructor') && (substr($subroles[1], 0, 17) ===
'TeachingAssistant')) {
737 $role =
"TeachingAssistant#{$subroles[1]}";
739 $role =
"{$subroles[0]}#{$subroles[1]}";
742 $role = substr($role, 50);
746 $pos = strrpos($role,
'/');
747 if ((strpos($role,
'#') !==
false) || ($pos !==
false)) {
749 if ($pos !==
false) {
750 $role = substr($role, 0, $pos) .
'#' . substr($role, $pos + 1);
755 $role =
"{$prefix}{$role}";
760 if (substr($role, 0, 24) ===
'urn:lti:sysrole:ims/lis/') {
761 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/system/person';
762 $role = substr($role, 24);
763 } elseif (substr($role, 0, 25) ===
'urn:lti:instrole:ims/lis/') {
764 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/institution/person';
765 $role = substr($role, 25);
766 } elseif (substr($role, 0, 21) ===
'urn:lti:role:ims/lis/') {
767 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/membership';
768 $subroles = explode(
'/', substr($role, 21));
769 if (count($subroles) === 2) {
770 if ($subroles[0] ===
'TeachingAssistant') {
771 $role =
"Instructor#{$subroles[1]}";
772 if ($addPrincipalRole) {
773 $parsedRoles[] =
"{$prefix}#Instructor";
776 $role =
"{$subroles[0]}#{$subroles[1]}";
777 if ($addPrincipalRole) {
778 $parsedRoles[] =
"{$prefix}#{$subroles[0]}";
781 } elseif ($subroles[0] ===
'TeachingAssistant') {
782 $role =
'Instructor#TeachingAssistant';
783 if ($addPrincipalRole) {
784 $parsedRoles[] =
"{$prefix}#Instructor";
787 $role = substr($role, 21);
789 } elseif (substr($role, 0, 46) ===
'http://purl.imsglobal.org/vocab/lis/v2/person#') {
790 if (in_array(substr($role, 46), $systemRoles)) {
791 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/system/person';
792 } elseif (in_array(substr($role, 46), $institutionRoles)) {
793 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/institution/person';
795 $role = substr($role, 46);
796 $pos = strrpos($role,
'/');
797 if ($pos !==
false) {
798 $role = substr($role, 0, $pos - 1) .
'#' . substr($role, $pos + 1);
800 } elseif (substr($role, 0, 50) ===
'http://purl.imsglobal.org/vocab/lis/v2/membership#') {
801 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/membership';
802 if (substr($role, 50, 18) ===
'TeachingAssistant') {
803 $role =
'Instructor#TeachingAssistant';
804 if ($addPrincipalRole) {
805 $parsedRoles[] =
"{$prefix}#Instructor";
808 $role = substr($role, 50);
810 } elseif (substr($role, 0, 50) ===
'http://purl.imsglobal.org/vocab/lis/v2/membership/') {
811 $prefix =
'http://purl.imsglobal.org/vocab/lis/v2/membership';
812 $subroles = explode(
'#', substr($role, 50));
813 if (count($subroles) === 2) {
814 if ($subroles[0] ===
'TeachingAssistant') {
815 $role =
"Instructor#{subroles[1]}";
816 if ($addPrincipalRole) {
817 $parsedRoles[] =
"{$prefix}#Instructor";
820 $role = substr($role, 50);
821 if ($addPrincipalRole) {
822 $parsedRoles[] =
"{$prefix}#{$subroles[0]}";
826 $role = substr($role, 50);
829 if (!empty($prefix) && !empty($role)) {
830 $pos = strrpos($role,
'/');
831 if ((strpos($role,
'#') !==
false) || ($pos !==
false)) {
833 if ($pos !==
false) {
834 $role = substr($role, 0, $pos) .
'#' . substr($role, $pos + 1);
839 $role =
"{$prefix}{$role}";
844 $parsedRoles[] = $role;
848 return array_unique($parsedRoles);
865 $params[
'lti_version'] = $version;
866 $params[
'lti_message_type'] = $type;
868 $params = $this->addSignature($url, $params,
'POST',
'application/x-www-form-urlencoded');
889 public function signMessage(&$url, $type, $version, $params, $loginHint =
null, $ltiMessageHint =
null)
892 if (!isset($loginHint) || (strlen($loginHint) <= 0)) {
893 if (isset($params[
'user_id']) && (strlen($params[
'user_id']) > 0)) {
894 $loginHint = $params[
'user_id'];
896 $loginHint =
'Anonymous';
900 $params[
'lti_version'] = $version;
901 $params[
'lti_message_type'] = $type;
902 $this->onInitiateLogin($url, $loginHint, $ltiMessageHint, $params);
905 'iss' => $this->platformId,
906 'target_link_uri' => $url,
907 'login_hint' => $loginHint
909 if (!is_null($ltiMessageHint)) {
910 $params[
'lti_message_hint'] = $ltiMessageHint;
912 if (!empty($this->clientId)) {
913 $params[
'client_id'] = $this->clientId;
915 if (!empty($this->deploymentId)) {
916 $params[
'lti_deployment_id'] = $this->deploymentId;
921 if (!empty(static::$browserStorageFrame)) {
922 if (strpos($url,
'?') ===
false) {
927 $url .=
"{$sep}lti_storage_target=" . static::$browserStorageFrame;
930 $params = $this->signParameters($url, $type, $version, $params);
948 public function sendMessage($url, $type, $messageParams, $target =
'', $userId =
null, $hint =
null)
950 $sendParams = $this->signMessage($url, $type, $this->ltiVersion, $messageParams, $userId, $hint);
970 $header = $this->addSignature($url, $data, $method, $type);
988 $header = $this->addSignature($service->endpoint, $data, $method, $format);
991 $http =
new HttpMessage($service->endpoint, $method, $data, $header);
993 if ($http->send() && !empty($http->response)) {
995 $http->ok = !is_null($http->responseJson);
1008 return empty($this->signatureMethod) || (substr($this->signatureMethod, 0, 2) !==
'RS');
1024 public function addSignature($endpoint, $data, $method =
'POST', $type =
null, $nonce =
'', $hash =
null, $timestamp =
null)
1026 if ($this->useOAuth1()) {
1027 return $this->addOAuth1Signature($endpoint, $data, $method, $type, $hash, $timestamp);
1029 return $this->addJWTSignature($endpoint, $data, $method, $type, $nonce, $timestamp);
1040 $ok = $_SERVER[
'REQUEST_METHOD'] ===
'POST';
1042 $this->reason =
'LTI messages must use HTTP POST';
1043 } elseif (!empty($this->jwt) && !empty($this->jwt->hasJwt())) {
1045 $context = $this->jwt->getClaim(
'https://purl.imsglobal.org/spec/lti/claim/context');
1046 $resourceLink = $this->jwt->getClaim(
'https://purl.imsglobal.org/spec/lti/claim/resource_link');
1047 if (is_null($this->messageParameters[
'oauth_consumer_key']) || (strlen($this->messageParameters[
'oauth_consumer_key']) <= 0)) {
1048 $this->reason =
'Missing iss claim';
1049 } elseif (empty($this->jwt->getClaim(
'iat',
''))) {
1050 $this->reason =
'Missing iat claim';
1051 } elseif (empty($this->jwt->getClaim(
'exp',
''))) {
1052 $this->reason =
'Missing exp claim';
1053 } elseif (intval($this->jwt->getClaim(
'iat')) > intval($this->jwt->getClaim(
'exp'))) {
1054 $this->reason =
'iat claim must not have a value greater than exp claim';
1055 } elseif (empty($this->jwt->getClaim(
'nonce',
''))) {
1056 $this->reason =
'Missing nonce claim';
1057 } elseif (!empty($context) && property_exists($context,
'id') && (empty($context->id) || !is_string($context->id))) {
1058 $this->reason =
'Invalid value for id property in https://purl.imsglobal.org/spec/lti/claim/context claim';
1059 } elseif (!empty($resourceLink) && property_exists($resourceLink,
'id') && (empty($resourceLink->id) || !is_string($resourceLink->id))) {
1060 $this->reason =
'Invalid value for id property in https://purl.imsglobal.org/spec/lti/claim/resource_link claim';
1066 if (isset($this->messageParameters[
'oauth_signature_method'])) {
1067 $this->signatureMethod = $this->messageParameters[
'oauth_signature_method'];
1068 if (($this instanceof
Tool) && !empty($this->platform)) {
1069 $this->platform->signatureMethod = $this->signatureMethod;
1074 $ok = isset($this->messageParameters[
'lti_message_type']);
1076 $this->reason =
'Missing lti_message_type parameter.';
1080 $ok = isset($this->messageParameters[
'lti_version']) && in_array($this->messageParameters[
'lti_version'],
1083 $this->reason =
'Invalid or missing lti_version parameter.';
1100 $secret = $this->secret;
1101 } elseif (($this instanceof
Tool) && !empty($this->platform)) {
1102 $key = $this->platform->getKey();
1103 $secret = $this->platform->secret;
1108 if ($this instanceof
Tool) {
1109 $platform = $this->platform;
1110 $publicKey = $this->platform->rsaKey;
1111 $jku = $this->platform->jku;
1118 $publicKey = $this->rsaKey;
1122 if (empty($this->jwt) || empty($this->jwt->hasJwt())) {
1127 $server->add_signature_method($method);
1129 $server->add_signature_method($method);
1131 $server->add_signature_method($method);
1133 $server->add_signature_method($method);
1135 $server->add_signature_method($method);
1136 $request = OAuth\OAuthRequest::from_request();
1137 if (isset($request->get_parameters()[
'_new_window']) && !isset($this->messageParameters[
'_new_window'])) {
1138 $request->unset_parameter(
'_new_window');
1140 $server->verify_request($request);
1142 }
catch (\Exception $e) {
1143 if (empty($this->reason)) {
1145 $signature = $request->build_signature($method, $oauthConsumer,
false);
1146 if ($this->debugMode) {
1147 $this->reason = $e->getMessage();
1149 if (empty($this->reason)) {
1150 $this->reason =
'OAuth signature check failed - perhaps an incorrect secret or timestamp.';
1152 $this->details[] =
"Shared secret: '{$secret}'";
1153 $this->details[] =
'Current timestamp: ' . time();
1154 $this->details[] =
"Expected signature: {$signature}";
1155 $this->details[] =
"Base string: {$request->base_string}";
1159 $nonce =
new PlatformNonce($platform, $this->jwt->getClaim(
'nonce'));
1160 $ok = !$nonce->load();
1162 $ok = $nonce->save();
1165 $this->reason =
'Invalid nonce.';
1166 } elseif (!empty($publicKey) || !empty($jku) || Jwt::$allowJkuHeader) {
1167 $ok = $this->jwt->verify($publicKey, $jku);
1169 $this->reason =
'JWT signature check failed - perhaps an invalid public key or timestamp';
1173 $this->reason =
'Unable to verify JWT signature as neither a public key nor a JSON Web Key URL is specified';
1191 private function parseMessage($strictMode, $disableCookieCheck, $generateWarnings)
1193 if (is_null($this->messageParameters)) {
1194 $this->getRawParameters();
1195 if (isset($this->rawParameters[
'id_token']) || isset($this->rawParameters[
'JWT'])) {
1197 $this->jwt = Jwt::getJwtClient();
1198 if (isset($this->rawParameters[
'id_token'])) {
1199 $this->ok = $this->jwt->load($this->rawParameters[
'id_token'], $this->rsaKey);
1201 $this->ok = $this->jwt->load($this->rawParameters[
'JWT'], $this->rsaKey);
1204 $this->reason =
'Message does not contain a valid JWT';
1206 $this->ok = $this->jwt->hasClaim(
'iss') && $this->jwt->hasClaim(
'aud') && $this->jwt->hasClaim(
'nonce') &&
1209 $iss = $this->jwt->getClaim(
'iss');
1210 $aud = $this->jwt->getClaim(
'aud');
1212 $this->ok = !empty($iss) && !empty($aud) && !empty($deploymentId);
1214 $this->reason =
'iss, aud and/or deployment_id claim is empty';
1215 } elseif (is_array($aud)) {
1216 if ($this->jwt->hasClaim(
'azp')) {
1217 $this->ok = !empty($this->jwt->getClaim(
'azp'));
1219 $this->reason =
'azp claim is empty';
1221 $this->ok = in_array($this->jwt->getClaim(
'azp'), $aud);
1223 $aud = $this->jwt->getClaim(
'azp');
1225 $this->reason =
'azp claim value is not included in aud claim';
1230 $this->ok = !empty($aud);
1232 $this->reason =
'First element of aud claim is empty';
1235 } elseif ($this->jwt->hasClaim(
'azp')) {
1236 $this->ok = $this->jwt->getClaim(
'azp') === $aud;
1238 $this->reason =
'aud claim does not match the azp claim';
1242 if ($this instanceof Tool) {
1244 $this->platform->platformId = $iss;
1245 if (isset($this->rawParameters[
'id_token'])) {
1246 $this->ok = !empty($this->rawParameters[
'state']);
1248 $state = $this->rawParameters[
'state'];
1249 $parts = explode(
'.', $state);
1250 if (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1]) &&
1251 ($parts[1] !==
'platformStorage')) {
1253 session_id($parts[1]);
1255 $this->onResetSessionId();
1257 $usePlatformStorage = (substr($state, -16) ===
'.platformStorage');
1258 if ($usePlatformStorage) {
1259 $state = substr($state, 0, -16);
1261 $this->onAuthenticate($state, $this->jwt->getClaim(
'nonce'), $usePlatformStorage);
1262 if (!$disableCookieCheck) {
1263 if (empty($_COOKIE) && !isset($_POST[
'_new_window'])) {
1265 $_POST[
'_new_window'] =
'';
1272 $this->reason =
'state parameter is missing';
1275 $nonce =
new PlatformNonce($this->platform, $state);
1276 $this->ok = $nonce->load();
1279 $nonce =
new PlatformNonce($platform, $state);
1280 $this->ok = $nonce->load();
1284 $nonce =
new PlatformNonce($platform, $state);
1285 $this->ok = $nonce->load();
1288 $this->ok = $nonce->delete();
1291 $this->reason =
'state parameter is invalid or has expired';
1296 $this->messageParameters = array();
1298 $this->messageParameters[
'oauth_consumer_key'] = $aud;
1299 $this->messageParameters[
'oauth_signature_method'] = $this->jwt->getHeader(
'alg');
1300 $this->parseClaims($strictMode, $generateWarnings);
1304 $this->reason =
'iss, aud, deployment_id and/or nonce claim not found';
1307 }
catch (\Exception $e) {
1309 $this->reason =
'Message does not contain a valid JWT';
1311 } elseif (isset($this->rawParameters[
'error'])) {
1313 $this->reason = $this->rawParameters[
'error'];
1314 if (!empty($this->rawParameters[
'error_description'])) {
1315 $this->reason .=
": {$this->rawParameters['error_description']}";
1318 if ($this instanceof Tool) {
1319 if (isset($this->rawParameters[
'oauth_consumer_key'])) {
1322 if (isset($this->rawParameters[
'tool_state'])) {
1323 $state = $this->rawParameters[
'tool_state'];
1324 if (!$disableCookieCheck) {
1325 $parts = explode(
'.', $state);
1326 if (empty($_COOKIE) && !isset($_POST[
'_new_window'])) {
1328 $_POST[
'_new_window'] =
'';
1331 } elseif (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1])) {
1333 session_id($parts[1]);
1335 $this->onResetSessionId();
1337 unset($this->rawParameters[
'_new_window']);
1340 $nonce =
new PlatformNonce($this->platform, $state);
1341 $this->ok = $nonce->load();
1343 $this->reason =
"Invalid tool_state parameter: '{$state}'";
1347 $this->messageParameters = $this->rawParameters;
1358 private function parseClaims($strictMode, $generateWarnings)
1364 if (!empty($mapping[
'suffix'])) {
1365 $claim .=
"-{$mapping['suffix']}";
1367 $claim .=
'/claim/';
1368 if (is_null($mapping[
'group'])) {
1369 $claim = $mapping[
'claim'];
1370 } elseif (empty($mapping[
'group'])) {
1371 $claim .= $mapping[
'claim'];
1373 $claim .= $mapping[
'group'];
1375 if ($this->jwt->hasClaim($claim)) {
1377 if (empty($mapping[
'group'])) {
1378 unset($payload->{$claim});
1379 $value = $this->jwt->getClaim($claim);
1381 $group = $this->jwt->getClaim($claim);
1382 if (is_array($group) && array_key_exists($mapping[
'claim'], $group)) {
1383 unset($payload->{$claim}[$mapping[
'claim']]);
1384 $value = $group[$mapping[
'claim']];
1385 } elseif (is_object($group) && isset($group->{$mapping[
'claim']})) {
1386 unset($payload->{$claim}->{$mapping[
'claim']});
1387 $value = $group->{$mapping[
'claim']};
1390 if (!is_null($value)) {
1391 if (isset($mapping[
'isArray']) && $mapping[
'isArray']) {
1392 if (!is_array($value)) {
1393 $errors[] =
"'{$claim}' claim must be an array";
1395 $value = implode(
',', $value);
1397 } elseif (isset($mapping[
'isObject']) && $mapping[
'isObject']) {
1398 $value = json_encode($value);
1399 } elseif (isset($mapping[
'isBoolean']) && $mapping[
'isBoolean']) {
1400 $value = $value ?
'true' :
'false';
1401 } elseif (isset($mapping[
'isInteger']) && $mapping[
'isInteger']) {
1402 $value = strval($value);
1403 } elseif (!is_string($value)) {
1404 if ($generateWarnings) {
1405 $this->warnings[] =
"Value of claim '{$claim}' is not a string: '{$value}'";
1408 $value = strval($value);
1412 if (!is_null($value) && is_string($value)) {
1413 $this->messageParameters[$key] = $value;
1417 if (!empty($this->messageParameters[
'lti_message_type']) &&
1419 $this->messageParameters[
'lti_message_type'] = array_search($this->messageParameters[
'lti_message_type'],
1422 if (!empty($this->messageParameters[
'accept_types'])) {
1423 $types = array_map(
'trim', explode(
',', $this->messageParameters[
'accept_types']));
1424 $types = array_filter($types);
1425 $mediaTypes = array();
1426 if (!empty($this->messageParameters[
'accept_media_types'])) {
1427 $mediaTypes = array_map(
'trim', explode(
',', $this->messageParameters[
'accept_media_types']));
1428 $mediaTypes = array_filter($mediaTypes);
1430 if (in_array(Item::TYPE_LTI_LINK, $types)) {
1431 $mediaTypes[] = Item::LTI_LINK_MEDIA_TYPE;
1433 if (in_array(Item::TYPE_LTI_ASSIGNMENT, $types)) {
1434 $mediaTypes[] = Item::LTI_ASSIGNMENT_MEDIA_TYPE;
1436 if (in_array(
'html', $types) && !in_array(
'*/*', $mediaTypes)) {
1437 $mediaTypes[] =
'text/html';
1439 if (in_array(
'image', $types) && !in_array(
'*/*', $mediaTypes)) {
1440 $mediaTypes[] =
'image/*';
1442 $mediaTypes = array_unique($mediaTypes);
1443 $this->messageParameters[
'accept_media_types'] = implode(
',', $mediaTypes);
1446 if ($this->jwt->hasClaim($claim)) {
1447 unset($payload->{$claim});
1448 $custom = $this->jwt->getClaim($claim);
1449 if (!is_array($custom) && !is_object($custom)) {
1450 $errors[] =
"'{$claim}' claim must be an object";
1452 foreach ($custom as $key => $value) {
1453 $this->messageParameters[
"custom_{$key}"] = $value;
1458 if ($this->jwt->hasClaim($claim)) {
1459 unset($payload->{$claim});
1460 $ext = $this->jwt->getClaim($claim);
1461 if (!is_array($ext) && !is_object($ext)) {
1462 $errors[] =
"'{$claim}' claim must be an object";
1464 foreach ($ext as $key => $value) {
1465 $this->messageParameters[
"ext_{$key}"] = $value;
1470 if ($this->jwt->hasClaim($claim)) {
1471 unset($payload->{$claim});
1472 $lti1p1 = $this->jwt->getClaim($claim);
1473 if (!is_array($lti1p1) && !is_object($lti1p1)) {
1474 $errors[] =
"'{$claim}' claim must be an object";
1476 foreach ($lti1p1 as $key => $value) {
1477 if (is_null($value)) {
1479 } elseif (is_object($value)) {
1480 $value = json_encode($value);
1482 $this->messageParameters[
"lti1p1_{$key}"] = $value;
1486 $claim =
'http://www.brightspace.com';
1487 if ($this->jwt->hasClaim($claim)) {
1488 $d2l = $this->jwt->getClaim($claim);
1489 if (is_array($d2l)) {
1490 if (!empty($d2l[
'username'])) {
1491 $this->messageParameters[
'ext_d2l_username'] = $d2l[
'username'];
1492 unset($payload->{$claim}[
'username']);
1494 }
else if (is_object($ext)) {
1495 if (!empty($d2l->username)) {
1496 $this->messageParameters[
'ext_d2l_username'] = $d2l->username;
1497 unset($payload->{$claim}->username);
1501 if (!empty($payload)) {
1502 $objVars = get_object_vars($payload);
1503 foreach ($objVars as $attrName => $attrValue) {
1504 if (empty((array) $attrValue)) {
1505 unset($payload->{$attrName});
1508 $this->messageParameters[
'unmapped_claims'] = json_encode($payload);
1510 if (!empty($errors)) {
1512 $this->reason =
'Invalid JWT: ' . implode(
', ', $errors);
1521 private function doCallback()
1523 if (array_key_exists($this->messageParameters[
'lti_message_type'],
Util::$METHOD_NAMES)) {
1526 $callback =
"on{$this->messageParameters['lti_message_type']}";
1528 if (method_exists($this, $callback)) {
1530 } elseif ($this->ok) {
1532 $this->reason =
"Message type not supported: {$this->messageParameters['lti_message_type']}";
1548 private function addOAuth1Signature($endpoint, $data, $method, $type, $hash, $timestamp)
1551 if (is_array($data)) {
1553 $params[
'oauth_callback'] =
'about:blank';
1556 $queryString = parse_url($endpoint, PHP_URL_QUERY);
1557 $queryParams = OAuth\OAuthUtil::parse_parameters($queryString);
1558 $params = array_merge_recursive($queryParams, $params);
1560 if (!is_array($data)) {
1562 if (is_null($data)) {
1565 switch ($this->signatureMethod) {
1567 $hash = base64_encode(hash(
'sha224', $data,
true));
1570 $hash = base64_encode(hash(
'sha256', $data,
true));
1573 $hash = base64_encode(hash(
'sha384', $data,
true));
1576 $hash = base64_encode(hash(
'sha512', $data,
true));
1579 $hash = base64_encode(sha1($data,
true));
1583 $params[
'oauth_body_hash'] = $hash;
1585 if (!empty($timestamp)) {
1586 $params[
'oauth_timestamp'] = $timestamp;
1590 switch ($this->signatureMethod) {
1592 $hmacMethod =
new OAuth\OAuthSignatureMethod_HMAC_SHA224();
1595 $hmacMethod =
new OAuth\OAuthSignatureMethod_HMAC_SHA256();
1598 $hmacMethod =
new OAuth\OAuthSignatureMethod_HMAC_SHA384();
1601 $hmacMethod =
new OAuth\OAuthSignatureMethod_HMAC_SHA512();
1604 $hmacMethod =
new OAuth\OAuthSignatureMethod_HMAC_SHA1();
1608 $secret = $this->secret;
1610 if (($this instanceof Tool) && !empty($this->platform)) {
1611 $key = $this->platform->getKey();
1612 $secret = $this->platform->secret;
1618 $oauthConsumer =
new OAuth\OAuthConsumer($key, $secret,
null);
1619 $oauthReq = OAuth\OAuthRequest::from_consumer_and_token($oauthConsumer,
null, $method, $endpoint, $params);
1620 $oauthReq->sign_request($hmacMethod, $oauthConsumer,
null);
1621 if (!is_array($data)) {
1622 $header = $oauthReq->to_header();
1624 if (!empty($type)) {
1625 $header .=
"\nAccept: {$type}";
1627 } elseif (isset($type)) {
1628 $header .=
"\nContent-Type: {$type}; charset=UTF-8";
1629 $header .=
"\nContent-Length: " . strlen($data);
1634 $params = $oauthReq->get_parameters();
1635 foreach ($queryParams as $key => $value) {
1636 if (!is_array($value)) {
1637 if (!is_array($params[$key])) {
1638 if ($params[$key] === $value) {
1639 unset($params[$key]);
1642 $params[$key] = array_diff($params[$key], array($value));
1645 foreach ($value as $element) {
1646 $params[$key] = array_diff($params[$key], array($value));
1651 foreach ($params as $key => $value) {
1652 if (is_array($value)) {
1653 if (count($value) <= 0) {
1654 unset($params[$key]);
1655 } elseif (count($value) === 1) {
1656 $params[$key] = reset($value);
1676 private function addJWTSignature($endpoint, $data, $method, $type, $nonce, $timestamp)
1679 if (is_array($data)) {
1681 if (empty($nonce)) {
1685 if (!array_key_exists(
'grant_type', $data)) {
1686 $this->messageParameters = $data;
1687 $payload = $this->getMessageClaims();
1688 $privateKey = $this->rsaKey;
1691 if ($this instanceof Platform) {
1695 $payload[
'iss'] = $this->platformId;
1696 $payload[
'aud'] = array($this->clientId);
1697 $payload[
'azp'] = $this->clientId;
1700 $paramName =
'id_token';
1702 if (!empty($this->platform)) {
1703 $publicKey = $this->platform->rsaKey;
1704 $payload[
'iss'] = $this->platform->clientId;
1705 $payload[
'aud'] = array($this->platform->platformId);
1706 $payload[
'azp'] = $this->platform->platformId;
1711 $payload[
'nonce'] = $nonce;
1713 $authorizationId =
'';
1714 if ($this instanceof Tool) {
1716 if (!empty($this->platform)) {
1717 $sub = $this->platform->clientId;
1718 $authorizationId = $this->platform->authorizationServerId;
1719 $publicKey = $this->platform->rsaKey;
1721 $privateKey = $this->rsaKey;
1725 $sub = $this->clientId;
1728 $privateKey = $this->rsaKey;
1733 $payload[
'iss'] = $sub;
1734 $payload[
'sub'] = $sub;
1735 if (empty($authorizationId)) {
1736 $authorizationId = $endpoint;
1738 $payload[
'aud'] = array($authorizationId);
1739 $payload[
'jti'] = $nonce;
1741 $paramName =
'client_assertion';
1745 if (empty($timestamp)) {
1746 $timestamp = time();
1748 $payload[
'iat'] = $timestamp;
1749 $payload[
'exp'] = $timestamp + Jwt::$life;
1751 $jwt = Jwt::getJwtClient();
1752 $params[$paramName] = $jwt::sign($payload, $this->signatureMethod, $privateKey, $kid, $jku, $this->encryptionMethod,
1754 }
catch (\Exception $e) {
1761 if ($this instanceof Tool) {
1762 $platform = $this->platform;
1766 $accessToken = $platform->getAccessToken();
1767 if (empty($accessToken)) {
1768 $accessToken =
new AccessToken($platform);
1769 $platform->setAccessToken($accessToken);
1771 if (!$accessToken->hasScope()) {
1772 $accessToken->get();
1774 if (!empty($accessToken->token)) {
1775 $header =
"Authorization: Bearer {$accessToken->token}";
1777 if (empty($data) && ($method !==
'DELETE')) {
1778 if (!empty($type)) {
1779 $header .=
"\nAccept: {$type}";
1781 } elseif (isset($type)) {
1782 $header .=
"\nContent-Type: {$type}; charset=UTF-8";
1783 if (!empty($data) && is_string($data)) {
1784 $header .=
"\nContent-Length: " . strlen($data);
1800 private static function fullyQualifyClaim($claim, $value)
1804 if (is_object($value)) {
1805 foreach ($value as $c => $v) {
1807 $claims = array_merge($claims, static::fullyQualifyClaim(
"{$claim}/{$c}", $v));
1811 $claims[$claim] = $value;
Class to represent a content-item object.
Class to provide a connection to a persistent store for LTI objects.
Class to represent an HTTP message request.
Class to represent an HTTP message request.
Class to represent an OAuth Consumer.
Class to represent an OAuth Data Store.
static $POST_INPUT
Access to POST data.
Class to represent an OAuth server.
Class to represent an OAuth HMAC_SHA1 signature method.
Class to represent an OAuth HMAC_SHA224 signature method.
Class to represent an OAuth HMAC_SHA256 signature method.
Class to represent an OAuth HMAC_SHA384 signature method.
Class to represent an OAuth HMAC_SHA512 signature method.
Class to represent an LTI system.
sendMessage($url, $type, $messageParams, $target='', $userId=null, $hint=null)
Generate a web page containing an auto-submitted form of LTI message parameters.
$warnings
Warnings relating to last request processed.
addSignature($endpoint, $data, $method='POST', $type=null, $nonce='', $hash=null, $timestamp=null)
Add the signature to an array of message parameters or to a header string.
$reason
Error message for last request processed.
getRawParameters()
Get the raw POST parameters.
$debugMode
Whether debug level messages are to be reported.
$name
Local name of platform/tool.
$enableUntil
Timestamp until which the system instance is enabled to accept connection requests.
getMessageClaims($fullyQualified=false)
Get the message claims.
$enabled
Whether the system instance is enabled to accept connection requests.
hasJwt()
Check whether a JWT exists.
signMessage(&$url, $type, $version, $params, $loginHint=null, $ltiMessageHint=null)
Add the signature to an LTI message.
$dataConnector
Data connector object.
verifySignature()
Verify the signature of a message.
$messageParameters
LTI message parameters.
$updated
Timestamp for when the object was last updated.
setSetting($name, $value=null)
Set a setting value.
signParameters($url, $type, $version, $params)
Add the signature to an LTI message.
$rsaKey
RSA key in PEM or JSON format.
$encryptionMethod
Algorithm used for encrypting messages.
$details
Details for error message relating to last request processed.
$jku
Endpoint for public key.
saveSettings()
Save setting values.
$created
Timestamp for when the object was created.
$ok
True if the last request was successful.
$rawParameters
Raw message parameters.
setKey($key)
Set the consumer key.
$enableFrom
Timestamp from which the the system instance is enabled to accept connection requests.
setSettings($settings)
Set an array of all setting values.
signServiceRequest($url, $method, $type, $data=null)
Generates the headers for an LTI service request.
$lastAccess
Timestamp for date of last connection to this system.
useOAuth1()
Determine whether this consumer is using the OAuth 1 security model.
checkMessage()
Verify the required properties of an LTI message.
getRecordId()
Get the system record ID.
getKey()
Get the consumer key.
getSetting($name, $default='')
Get a setting value.
setRecordId($id)
Sets the system record ID.
$requiredScopes
Scopes to request when obtaining an access token.
doServiceRequest($service, $method, $format, $data)
Perform a service request.
getSettings()
Get an array of all setting values.
static parseRoles($roles, $ltiVersion=Util::LTI_VERSION1, $addPrincipalRole=false)
Parse a set of roles to comply with a specified version of LTI.
$jwt
JWT ClientInterface object.
$signatureMethod
Method used for signing messages.
Class to implement utility methods.
static sendForm($url, $params, $target='', $javascript='')
Generate a web page containing an auto-submitted form of parameters.
const LTI_VERSION1P3
LTI version 1.3 for messages.
const JWT_CLAIM_MAPPING
Mapping for standard message parameters to JWT claim.
const MESSAGE_TYPE_MAPPING
Mapping for standard message types.
static getRandomString($length=8)
Generate a random string.
static cloneObject($obj)
Clone an object and any objects it contains.
static $METHOD_NAMES
List of supported message types and associated class methods.
static $LTI_VERSIONS
Permitted LTI versions for messages.
const LTI_VERSION1
LTI version 1 for messages.
static jsonDecode($str, $associative=false)
Decode a JSON string.
static setTestCookie($delete=false)
Set or delete a test cookie.
const JWT_CLAIM_PREFIX
Prefix for standard JWT message claims.
const LTI_VERSION2
LTI version 2 for messages.
Interface to represent an HWT client.