aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMurtaza Sultani <sultani@data-quest.de>2026-02-11 14:54:43 +0100
committerMurtaza Sultani <sultani@data-quest.de>2026-03-19 17:36:05 +0100
commit7a854a4f8ff86688072fa04118dc273b60f2ad97 (patch)
tree0261dad09891bb7c2758442e5c96db3c5f6d69dc
parentdc14f85649ae3d9ee187db2ac31de7b023c95d66 (diff)
Refactor and fix deeplinking issues
-rw-r--r--app/controllers/lti/1p3/auth.php89
-rw-r--r--app/controllers/lti/1p3/index.php18
-rw-r--r--app/controllers/lti/auth.php87
-rw-r--r--lib/classes/Lti/LTI1p3/Identity.php1
-rw-r--r--lib/classes/Lti/LTI1p3/PlatformManager.php10
-rw-r--r--lib/classes/Lti/LTI1p3/RegistrationManager.php13
-rw-r--r--lib/classes/Lti/LTI1p3/RegistrationRepository.php10
-rw-r--r--lib/classes/Lti/LTI1p3/ToolManager.php4
-rw-r--r--lib/models/Keyring.php13
-rw-r--r--lib/models/Lti/Registration.php23
-rw-r--r--resources/vue/components/lti/registrations/LtiRegistrationForm.vue2
-rw-r--r--resources/vue/components/lti/resources/ResourceForm.vue7
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)
);
}