Repository navigation
fix(portal): read portaliq's signing secret through IAppConfig - #1897
Merged
rubenvdlinde merged 1 commit intoOct 14, 2026
Merged
Conversation
Portaliq stores jwt_signing_secret as a sensitive app value, so the
legacy IConfig::getAppValue() returns the encrypted value. The verifier
HMAC-checked every X-Portal-Subject assertion against that ciphertext,
and every portal action answered 401.
The verifier now depends on IAppConfig instead of IConfig and reads
getValueString('portaliq', 'jwt_signing_secret', '', lazy: false), which
decrypts it. Adds a regression test that fails on the old read path.
Contributor
Quality Report — ConductionNL/learniq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 110/110 | |||
| npm | ✅ | ✅ 747/747 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| lockfile sync | ✅ | ||||
| PHPUnit | ✅ 3376/3376 · 5 skipped | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development. |
||||
| Hydra gates | ❌ |
Quality workflow — 2026-10-14 10:17 UTC
Download the full PDF report from the workflow artifacts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke live
learniq is not part of the Zuiddrecht demo, but it carries the same bug, so it gets the same fix.
In the Zuiddrecht demo test of 13 and 14 October 2026, every portal action that portaliq forwards to learniq answered 401.
Portaliq stores its
jwt_signing_secretas a sensitive app value (setValueString(..., lazy: false, sensitive: true)inlib/Repair/InitializeSettings.php), so Nextcloud keeps it encrypted at rest.PortalAssertionVerifier::secret()(lib/Portal/PortalAssertionVerifier.php:220) read it with the legacyIConfig::getAppValue('portaliq', 'jwt_signing_secret'). In Nextcloud that path (AppConfig::getValue()) returns the stored value without decrypting it: a$AppConfigEncryption$...string of about 281 characters instead of the 32-character secret. Every HMAC check onX-Portal-Subjectthen failed.The fix
Read the secret the way the minter does (
PortalSessionService::__construct()in portaliq):IAppConfig::getValueString('portaliq', 'jwt_signing_secret', '', lazy: false).getValueStringdecrypts sensitive values.lazy: falsematches how portaliq stores the key. opencatalogi already reads it this way and works.In this app the verifier needed
IConfigonly for this read, so its constructor now takes?IAppConfig $configinstead (same parameter name, so existing named call sites keep working; DI autowires the new type). There was no instance-secret fallback here to keep, and none is added.Why not a verifier service in portaliq: ADR-046 says no leaf app depends on portaliq (duck typing only), so a shared class would add the coupling the contract forbids. Reading the shared app value through the right API keeps the apps independent.
Verified
PortalAssertionVerifierTest::testTheSensitiveSecretIsReadDecrypted(a mock implementing bothIConfigandIAppConfig, so the old code runs its legacy read and fails on the signature, not on a type error): the mockedIConfig::getAppValue()returns a ciphertext-looking value and the mockedIAppConfig::getValueString()returns the secret. It fails on the old code (Failed asserting that null is of type array) and passes with the fix.COMPOSER_PROCESS_TIMEOUT=0 composer check:strict: exit 0, ALL CHECKS PASSED (lint, phpcs, phpmd, psalm, phpstan, 3,381 tests). No NEW findings, no INHERITED findings.Follow-up, not in this PR
None for this app.
🤖 Generated with Claude Code