diff options
| author | Murtaza Sultani <sultani@data-quest.de> | 2026-02-11 14:54:43 +0100 |
|---|---|---|
| committer | Murtaza Sultani <sultani@data-quest.de> | 2026-03-19 17:36:05 +0100 |
| commit | 7a854a4f8ff86688072fa04118dc273b60f2ad97 (patch) | |
| tree | 0261dad09891bb7c2758442e5c96db3c5f6d69dc | |
| parent | dc14f85649ae3d9ee187db2ac31de7b023c95d66 (diff) | |
Refactor and fix deeplinking issues
| -rw-r--r-- | app/controllers/lti/1p3/auth.php | 89 | ||||
| -rw-r--r-- | app/controllers/lti/1p3/index.php | 18 | ||||
| -rw-r--r-- | app/controllers/lti/auth.php | 87 | ||||
| -rw-r--r-- | lib/classes/Lti/LTI1p3/Identity.php | 1 | ||||
| -rw-r--r-- | lib/classes/Lti/LTI1p3/PlatformManager.php | 10 | ||||
| -rw-r--r-- | lib/classes/Lti/LTI1p3/RegistrationManager.php | 13 | ||||
| -rw-r--r-- | lib/classes/Lti/LTI1p3/RegistrationRepository.php | 10 | ||||
| -rw-r--r-- | lib/classes/Lti/LTI1p3/ToolManager.php | 4 | ||||
| -rw-r--r-- | lib/models/Keyring.php | 13 | ||||
| -rw-r--r-- | lib/models/Lti/Registration.php | 23 | ||||
| -rw-r--r-- | resources/vue/components/lti/registrations/LtiRegistrationForm.vue | 2 | ||||
| -rw-r--r-- | resources/vue/components/lti/resources/ResourceForm.vue | 7 |
12 files changed, 156 insertions, 121 deletions
diff --git a/app/controllers/lti/1p3/auth.php b/app/controllers/lti/1p3/auth.php new file mode 100644 index 0000000..c731995 --- /dev/null +++ b/app/controllers/lti/1p3/auth.php @@ -0,0 +1,89 @@ +<?php + +use Trails\Dispatcher; +use Studip\Cache\Factory; +use Studip\Lti\LTI1p3\KeyManager; +use Studip\OAuth2\Bridge\ScopeEntity; +use Studip\OAuth2\NegotiatesWithPsr7; +use Studip\Lti\LTI1p3\PlatformManager; +use Studip\Lti\LTI1p3\UserAuthenticator; +use Studip\Lti\LTI1p3\RegistrationManager; +use OAT\Library\Lti1p3Core\Security\Key\KeyChainRepository; +use OAT\Library\Lti1p3Core\Security\Oidc\OidcAuthenticator; +use OAT\Library\Lti1p3Core\Security\Jwks\Exporter\JwksExporter; +use OAT\Library\Lti1p3Core\Security\Jwks\Server\JwksRequestHandler; +use OAT\Library\Lti1p3Core\Security\OAuth2\Repository\ScopeRepository; +use OAT\Library\Lti1p3Core\Security\OAuth2\Repository\ClientRepository; +use OAT\Library\Lti1p3Core\Security\OAuth2\Repository\AccessTokenRepository; +use OAT\Library\Lti1p3Core\Security\OAuth2\Factory\AuthorizationServerFactory; +use OAT\Library\Lti1p3Core\Security\OAuth2\Generator\AccessTokenResponseGenerator; +use OAT\Library\Lti1p3Core\Security\Oidc\Server\OidcAuthenticationRequestHandler; + +class Lti_1p3_AuthController extends AuthenticatedController +{ + protected $allow_nobody = true; + protected $with_session = false; + use NegotiatesWithPsr7; + + public function __construct(Dispatcher $dispatcher) + { + $action = basename(get_route()); + if ($action === 'token') { + $this->allow_nobody = false; + $this->with_session = true; + } + + parent::__construct($dispatcher); + } + + public function login_action(): void + { + $oidcLoginHandler = new OidcAuthenticationRequestHandler( + new OidcAuthenticator( + new RegistrationManager(), + new UserAuthenticator() + ) + ); + + $response = $oidcLoginHandler->handle($this->getPsrRequest()); + $this->renderPsrResponse($response); + } + + public function jwks_action(): void + { + $keyChainRepo = new KeyChainRepository(); + $platformKeyring = PlatformManager::getKeyring(); + + $keyChainRepo->addKeyChain($platformKeyring->toKeyChain()); + $handler = new JwksRequestHandler(new JwksExporter($keyChainRepo)); + $this->renderPsrResponse($handler->handle($platformKeyring->range_id)); + } + + public function token_action(): void + { + $platformEncryptionKey = PlatformManager::getPrivateKey()->getContent(); + $responseGenerator = new AccessTokenResponseGenerator( + new KeyManager(), + new AuthorizationServerFactory( + new ClientRepository(new RegistrationManager()), + new AccessTokenRepository(Factory::getCache()), + new ScopeRepository( + [ + new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'), + new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'), + new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/score') + ] + ), + $platformEncryptionKey + ) + ); + + $response = $responseGenerator->generate( + $this->getPsrRequest(), + $this->getPsrResponse(), + '1' + ); + + $this->renderPsrResponse($response); + } +} diff --git a/app/controllers/lti/1p3/index.php b/app/controllers/lti/1p3/index.php index ae6bde1..958c97d 100644 --- a/app/controllers/lti/1p3/index.php +++ b/app/controllers/lti/1p3/index.php @@ -4,6 +4,7 @@ use Lti\Registration; use Lti\ResourceLink; use Studip\Cache\Factory; use Studip\Lti\LTI1p3\RoleMapper; +use Studip\LTIException; use Studip\OAuth2\NegotiatesWithPsr7; use Studip\Lti\LTI1p3\PlatformManager; use Studip\Lti\LTI1p3\RegistrationManager; @@ -16,12 +17,23 @@ use OAT\Library\Lti1p3Core\Message\Payload\Claim\LaunchPresentationClaim; use OAT\Library\Lti1p3Core\Message\Launch\Validator\Platform\PlatformLaunchValidator; use OAT\Library\Lti1p3Core\Message\Launch\Builder\LtiResourceLinkLaunchRequestBuilder; use OAT\Library\Lti1p3DeepLinking\Message\Launch\Builder\DeepLinkingLaunchRequestBuilder; +use Trails\Dispatcher; final class Lti_1p3_IndexController extends AuthenticatedController { use NegotiatesWithPsr7; use RegistrationValidationTrait; - protected COURSE | Institute $context; + protected COURSE | Institute | null $context; + + public function __construct(Dispatcher $dispatcher) + { + if (basename(get_route()) === 'store_contents') { + $this->allow_nobody = true; + $this->with_session = false; + } + + parent::__construct($dispatcher); + } public function before_filter(&$action, &$args) { @@ -123,9 +135,7 @@ final class Lti_1p3_IndexController extends AuthenticatedController $request = $validator->validateToolOriginatingLaunch($this->getPsrRequest()); if ($request->hasError()) { - PageLayout::postError($request->getError()); - $this->redirect('course/lti'); - return; + throw new LtiException($request->getError()); } $this->ltiResources = (new ResourceCollectionFactory())->createFromClaim( diff --git a/app/controllers/lti/auth.php b/app/controllers/lti/auth.php index b4b47da..348da0c 100644 --- a/app/controllers/lti/auth.php +++ b/app/controllers/lti/auth.php @@ -1,39 +1,11 @@ <?php -use OAT\Library\Lti1p3Core\Security\Jwks\Exporter\JwksExporter; -use OAT\Library\Lti1p3Core\Security\Jwks\Server\JwksRequestHandler; -use OAT\Library\Lti1p3Core\Security\Key\KeyChainRepository; -use OAT\Library\Lti1p3Core\Security\OAuth2\Factory\AuthorizationServerFactory; -use OAT\Library\Lti1p3Core\Security\OAuth2\Generator\AccessTokenResponseGenerator; -use OAT\Library\Lti1p3Core\Security\OAuth2\Repository\AccessTokenRepository; -use OAT\Library\Lti1p3Core\Security\OAuth2\Repository\ClientRepository; -use OAT\Library\Lti1p3Core\Security\OAuth2\Repository\ScopeRepository; -use OAT\Library\Lti1p3Core\Security\Oidc\OidcAuthenticator; -use OAT\Library\Lti1p3Core\Security\Oidc\Server\OidcAuthenticationRequestHandler; -use Studip\Cache\Factory; -use Studip\Lti\LTI1p3\KeyManager; -use Studip\Lti\LTI1p3\PlatformManager; -use Studip\Lti\LTI1p3\RegistrationManager; -use Studip\Lti\LTI1p3\UserAuthenticator; -use Studip\OAuth2\Bridge\ScopeEntity; use Studip\OAuth2\NegotiatesWithPsr7; -use Trails\Dispatcher; class Lti_AuthController extends StudipController { use NegotiatesWithPsr7; - public function __construct(Dispatcher $dispatcher) - { - $this->allow_nobody = false; - $action = basename(get_route()); - if (in_array($action, ['jwks', 'token'])) { - $this->allow_nobody = true; - $this->with_session = $action !== 'jwks'; - } - parent::__construct($dispatcher); - } - /** * Callback function being called before an action is executed. */ @@ -145,63 +117,4 @@ class Lti_AuthController extends StudipController $this->signature = $lti_link->getLaunchSignature($this->launch_data); $this->render_template('course/lti/iframe'); } - - public function login_action(): void - { - $oidcLoginHandler = new OidcAuthenticationRequestHandler( - new OidcAuthenticator( - new RegistrationManager(), - new UserAuthenticator() - ) - ); - - $response = $oidcLoginHandler->handle($this->getPsrRequest()); - $this->renderPsrResponse($response); - } - - /** - * This action handles JSON web key set (JWKS) requests for the platform key. - * - * @return void - */ - public function jwks_action(): void - { - $keyChainRepo = new KeyChainRepository(); - $platformKeyring = PlatformManager::getKeyring(); - - $keyChainRepo->addKeyChain($platformKeyring->toKeyChain()); - $handler = new JwksRequestHandler(new JwksExporter($keyChainRepo)); - $this->renderPsrResponse($handler->handle($platformKeyring->range_id)); - } - - /** - * Generates OAuth2 tokens. - */ - public function token_action(): void - { - $platformEncryptionKey = PlatformManager::getPrivateKey()->getContent(); - $responseGenerator = new AccessTokenResponseGenerator( - new KeyManager(), - new AuthorizationServerFactory( - new ClientRepository(new RegistrationManager()), - new AccessTokenRepository(Factory::getCache()), - new ScopeRepository( - [ - new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'), - new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'), - new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/score') - ] - ), - $platformEncryptionKey - ) - ); - - $response = $responseGenerator->generate( - $this->getPsrRequest(), - $this->getPsrResponse(), - '1' - ); - - $this->renderPsrResponse($response); - } } diff --git a/lib/classes/Lti/LTI1p3/Identity.php b/lib/classes/Lti/LTI1p3/Identity.php index ab38471..5b537ae 100644 --- a/lib/classes/Lti/LTI1p3/Identity.php +++ b/lib/classes/Lti/LTI1p3/Identity.php @@ -25,6 +25,7 @@ final class Identity implements UserIdentityInterface 'user_id' => $user->id ] ); + if ($privacySettings) { $this->optionalFields = explode(',', $privacySettings->allowed_optional_fields); } diff --git a/lib/classes/Lti/LTI1p3/PlatformManager.php b/lib/classes/Lti/LTI1p3/PlatformManager.php index f1d7d14..cb6fb4c 100644 --- a/lib/classes/Lti/LTI1p3/PlatformManager.php +++ b/lib/classes/Lti/LTI1p3/PlatformManager.php @@ -24,9 +24,9 @@ final class PlatformManager return new Platform( $c->STUDIP_INSTALLATION_ID, $c->UNI_NAME_CLEAN, - $GLOBALS['ABSOLUTE_URI_STUDIP'], - URLHelper::getURL('dispatch.php/lti/auth/login', null, true), - URLHelper::getURL('dispatch.php/lti/auth/token', null, true) + rtrim($GLOBALS['ABSOLUTE_URI_STUDIP'], '/'), + URLHelper::getURL('dispatch.php/lti/1p3/auth/login', null, true), + URLHelper::getURL('dispatch.php/lti/1p3/auth/token', null, true) ); } @@ -72,11 +72,11 @@ final class PlatformManager if ($courseId) { $params['cid'] = $courseId; } - return URLHelper::getURL('dispatch.php/lti/13/store_content/' . $linkId, $params, true); + return URLHelper::getURL('dispatch.php/lti/1p3/index/store_content/' . $linkId, $params, true); } public static function getJwksUrl(): string { - return URLHelper::getURL('dispatch.php/lti/auth/jwks'); + return URLHelper::getURL('dispatch.php/lti/1p3/auth/jwks', null, true); } } diff --git a/lib/classes/Lti/LTI1p3/RegistrationManager.php b/lib/classes/Lti/LTI1p3/RegistrationManager.php index 6c196cd..94fb90b 100644 --- a/lib/classes/Lti/LTI1p3/RegistrationManager.php +++ b/lib/classes/Lti/LTI1p3/RegistrationManager.php @@ -41,11 +41,11 @@ final class RegistrationManager implements RegistrationRepositoryInterface { $deployment = Deployment::findOneBySQL("`client_id` = ?", [$clientId]); $registration = $deployment->registration; + $registrationIssuer = $registration->getConfigValues()['issuer'] ?? null; if ( - !$registration - || $registration->status !== RegistrationStatus::Active->value - || $registration->getConfigValues()['issuer'] !== $issuer + $registration->status !== RegistrationStatus::Active->value + || !in_array($issuer, [$registrationIssuer, PlatformManager::getPlatformConfiguration()->getAudience()]) ) { return null; } @@ -57,10 +57,11 @@ final class RegistrationManager implements RegistrationRepositoryInterface { $deployment = Deployment::findOneBySQL("`client_id` = ?", [$clientId]); $registration = $deployment->registration; + $registrationAudience = $registration->getConfigValues()['audience'] ?? null; + if ( - !$registration - || $registration->status !== RegistrationStatus::Active->value - || $registration->getConfigValues()['audience'] !== $issuer + $registration->status !== RegistrationStatus::Active->value + || !in_array($issuer, [$registrationAudience, ToolManager::getToolConfiguration()->getAudience()]) ) { return null; } diff --git a/lib/classes/Lti/LTI1p3/RegistrationRepository.php b/lib/classes/Lti/LTI1p3/RegistrationRepository.php index 82fcffe..d1527b8 100644 --- a/lib/classes/Lti/LTI1p3/RegistrationRepository.php +++ b/lib/classes/Lti/LTI1p3/RegistrationRepository.php @@ -88,11 +88,11 @@ final class RegistrationRepository implements RegistrationInterface public function getPlatformKeyChain(): ?KeyChainInterface { if ($this->registration->role === 'platform') { - if ($this->registration->config_values['jwks_url']) { + if ($this->registration->getJwksURL()) { return null; } - return $this->registration->getKeyring()?->toKeyChain(); + return $this->registration->getKeyChain(); } return PlatformManager::getKeyring()->toKeyChain(); @@ -101,14 +101,14 @@ final class RegistrationRepository implements RegistrationInterface public function getToolKeyChain(): ?KeyChainInterface { if ($this->registration->role === 'tool') { - if ($this->registration->config_values['jwks_url']) { + if ($this->registration->getJwksURL()) { return null; } - return $this->registration->getKeyring()?->toKeyChain(); + return $this->registration->getKeyChain(); } - return ToolManager::getKeyring()?->toKeyChain(); + return ToolManager::getKeyring()->toKeyChain(); } public function getPlatformJwksUrl(): ?string diff --git a/lib/classes/Lti/LTI1p3/ToolManager.php b/lib/classes/Lti/LTI1p3/ToolManager.php index 342a174..acd7654 100644 --- a/lib/classes/Lti/LTI1p3/ToolManager.php +++ b/lib/classes/Lti/LTI1p3/ToolManager.php @@ -16,7 +16,7 @@ final class ToolManager return new Tool( $config->STUDIP_INSTALLATION_ID, $config->UNI_NAME_CLEAN, - $GLOBALS['ABSOLUTE_URI_STUDIP'], + rtrim($GLOBALS['ABSOLUTE_URI_STUDIP'], '/'), URLHelper::getURL('dispatch.php/enroll/lti/auth_init', null, true), URLHelper::getURL('dispatch.php/enroll/lti/launch', null, true), URLHelper::getURL('dispatch.php/enroll/lti/launch_deeplink', null, true) @@ -45,6 +45,6 @@ final class ToolManager public static function getJwksUrl(): string { - return URLHelper::getURL('dispatch.php/enroll/lti/jwks'); + return URLHelper::getURL('dispatch.php/enroll/lti/jwks', null, true); } } diff --git a/lib/models/Keyring.php b/lib/models/Keyring.php index 40f670f..2bef584 100644 --- a/lib/models/Keyring.php +++ b/lib/models/Keyring.php @@ -12,6 +12,9 @@ * @since 6.0 */ +use OAT\Library\Lti1p3Core\Security\Key\Key; +use OAT\Library\Lti1p3Core\Security\Key\KeyChain; + /** * The Keyring class stores cryptographic keyrings in the database. * @@ -141,25 +144,25 @@ class Keyring extends SimpleORMap /** * Converts the keyring to a KeyChain instance of the Lti1p3Core library. * - * @return \OAT\Library\Lti1p3Core\Security\Key\KeyChain A KeyChain representation + * @return KeyChain A KeyChain representation * of the keyring. */ - public function toKeyChain() : \OAT\Library\Lti1p3Core\Security\Key\KeyChain + public function toKeyChain() : KeyChain { - $public_key = new \OAT\Library\Lti1p3Core\Security\Key\Key( + $public_key = new Key( $this->public_key ); //A private key is optional. $private_key = null; if ($this->private_key) { - $private_key = new \OAT\Library\Lti1p3Core\Security\Key\Key( + $private_key = new Key( $this->private_key, $this->passphrase ?? null ); } - return new \OAT\Library\Lti1p3Core\Security\Key\KeyChain( + return new KeyChain( $this->id, $this->range_id, $public_key, diff --git a/lib/models/Lti/Registration.php b/lib/models/Lti/Registration.php index 7d5b33d..4f4b621 100644 --- a/lib/models/Lti/Registration.php +++ b/lib/models/Lti/Registration.php @@ -1,6 +1,9 @@ <?php namespace Lti; +use OAT\Library\Lti1p3Core\Security\Key\Key; +use OAT\Library\Lti1p3Core\Security\Key\KeyChain; +use OAT\Library\Lti1p3Core\Security\Key\KeyChainInterface; use Range; use Keyring; use SimpleORMap; @@ -85,11 +88,23 @@ class Registration extends SimpleORMap ); } - public function getKeyring(): ?Keyring + public function getJwksURL(): ?string { - return Keyring::findOneBySQL( - "`range_type` = 'lti_registration' AND `range_id` = ?", - [$this->id] + return $this->getConfigValues()['jwks_url'] ?? null; + } + + public function getKeyChain(): ?KeyChainInterface + { + $configs = $this->getConfigValues(); + if (!$configs['public_key']) { + return null; + } + + return new KeyChain( + $configs['jwks_key_id'], + $this->name, + new Key($configs['public_key']), + null ); } diff --git a/resources/vue/components/lti/registrations/LtiRegistrationForm.vue b/resources/vue/components/lti/registrations/LtiRegistrationForm.vue index 7f44984..80117bf 100644 --- a/resources/vue/components/lti/registrations/LtiRegistrationForm.vue +++ b/resources/vue/components/lti/registrations/LtiRegistrationForm.vue @@ -171,7 +171,7 @@ const formActionURL = computed(() => { <StudipTooltipIcon :text="$gettext('Die ID des Schlüssels, der über die JWKS-URL geladen werden soll.')" /> - <input type="url" name="jwks_key_id" v-model="form.jwks_key_id" /> + <input type="text" name="jwks_key_id" v-model="form.jwks_key_id" /> </label> </template> <label v-else class="studiprequired"> diff --git a/resources/vue/components/lti/resources/ResourceForm.vue b/resources/vue/components/lti/resources/ResourceForm.vue index 2713754..6c85043 100644 --- a/resources/vue/components/lti/resources/ResourceForm.vue +++ b/resources/vue/components/lti/resources/ResourceForm.vue @@ -99,8 +99,11 @@ const handleLtiMessage = event => { const resourceCount = event.data.ltiResources.length; formState.resources = event.data.ltiResources.map(r => ({ ...r, + launch_url: r.url, + description: r.text, + icon: r.icon?.url, + custom_parameters: r.custom ? objectToKeyValueString(r.custom) : null, launch_container: 'window', - custom_parameters: objectToKeyValueString(r.custom), colorPicked: false, isCollapsed: resourceCount > 1, isConfigurationCollapsed: true @@ -108,7 +111,7 @@ const handleLtiMessage = event => { STUDIP.Report.success( $gettext('%{count} LTI-Ressourcen wurden ausgewählt.', {count: formState.resources.length}), - formState.resources.map(r => r.title) + formState.resources.map(({ title }) => title) ); } |
