fix(deps): pin jjwt-impl + jjwt-jackson at runtime (fixes /api/v2 403)#695
Merged
Merged
Conversation
Problem: every request to /NotificationPortlet/api/v2/notifications
returned 403 Forbidden, even with a valid uPortal-issued OIDC bearer
token in the Authorization header. The notification-icon and
notification-list portlets (Vue web components powered by this API)
silently rendered empty as a result. Enabling DEBUG on
org.apereo.portal.soffit.security surfaced the actual exception:
io.jsonwebtoken.lang.UnknownClassException: Unable to load class
named [io.jsonwebtoken.impl.DefaultJwtParser] from the thread
context, current, or system/application ClassLoaders. All
heuristics have been exhausted. Class could not be found. Have
you remembered to include the jjwt-impl.jar in your runtime
classpath?
The deployed WAR ships only `jjwt-api-0.11.5.jar` in WEB-INF/lib.
JJWT 0.10+ split into `jjwt-api` (interfaces) + `jjwt-impl` (runtime
implementation) + `jjwt-jackson` (JSON parsing). Without the impl
jar, `Jwts.parser()` throws on first call.
Why the impl jars were missing: uPortal-soffit-core declares
jjwt-impl and jjwt-jackson as `runtimeOnly`. Gradle does not
propagate `runtimeOnly` transitive dependencies through the
deprecated `compile` configuration this project still uses, so they
never make it onto this WAR's runtime classpath.
Goal: include jjwt-impl and jjwt-jackson at runtime so SoffitApi-
PreAuthenticatedProcessingFilter can validate the bearer JWT and the
/api/v2 endpoint stops 403'ing.
Changes:
- gradle.properties: pin `jjwtVersion=0.11.5` to match the version
uPortal-soffit-core ships transitively
- notification-portlet-webapp/build.gradle: declare
`runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"` and
`runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"`
alongside the existing uPortal-soffit-renderer compile dep, with
a comment explaining why these explicit declarations are needed
Notes: verified by redeploying NotificationPortlet 4.8.4-SNAPSHOT
into uPortal-start's Tomcat. Before: GET /api/v2/notifications →
403 with "Bearer token is unusable" in NotificationPortlet.log.
After: 200 OK with the demo notifications JSON, the
notification-list portlet renders the demo entries (Room Available,
Past Due Book, Math 101, etc.), and the notification-icon badge
shows a non-zero unread count. Captured as a regression guard in
uPortal-start's Playwright suite at
tests/ux/portlets/notifications.spec.ts.
bjagg
added a commit
to bjagg/uPortal-start
that referenced
this pull request
May 5, 2026
Problem: NotificationPortlet coverage was just two structural smoke
tests in web-components.spec.ts (icon attached, dropdown attached).
Neither asserted that the API actually returned data or that the
portlet rendered any specific entries — so the live JJWT
runtime-classpath bug, where /api/v2 was returning 403 and the
components rendered empty, slipped through coverage entirely.
Goal: lock in the real end-to-end happy path (demo notifications
flow API → web component → DOM) plus the negative auth path, so the
JJWT regression cannot recur silently.
Changes:
- new tests/ux/portlets/notifications.spec.ts with three tests —
* notification-list portlet renders demo entries: asserts the
expected demo titles (Room Available, Bike License Ticket Was
Issued, Past Due Book, Math 101) reach the rendered DOM. If
/api/v2 starts 403'ing again, none of these will appear and
this test fails immediately.
* notification-icon web component shows a badge with a count:
confirms the icon mounts as a custom element and renders a
numeric unread badge for an authenticated student.
* API rejects missing or malformed bearer tokens: negative
guard. With no Authorization header → 403; with a junk JWT →
403. This is the security half of the regression guard — the
happy path landing on 200 only matters if the security filter
is still enforcing.
Notes: the JJWT runtime-classpath bug that broke the API is fixed
in uPortal-Project/NotificationPortlet#695. The above tests
verified the fix end-to-end. The expected-titles list is the
intersection of the two demo response files configured via
DemoNotificationService.locations in etc/portal/notification.properties.
Removed an earlier draft `API returns demo notifications` test
that listened on page.on("response") — it was order-dependent in
the full suite (the listener attached after login may or may not
catch the first /api/v2 fetch), and the DOM-level
"renders demo entries" test already covers the same regression
without that race.
bjagg
added a commit
to bjagg/uPortal-start
that referenced
this pull request
May 5, 2026
Problem: this branch has been carrying tests and overlay seeds that depend on three companion fixes published over the last few days (uPortal Bootstrap dedupe, CalendarPortlet template-sigil leak, NotificationPortlet jjwt runtime classpath). Until those landed on Maven Central, CI couldn't go green here even though every spec passed locally against the locally-built SNAPSHOTs. Goal: consume the published GA versions so PR uPortal-Project#692 lights up green and is ready to merge. Changes: - uPortalVersion 5.17.4 → 5.17.5 (picks up uPortal-Project/uPortal#2980 — Bootstrap 5 include de-dup in the respondr skin, fixes silently-broken Options menus / favorites / rate-portlet across the dashboard) - calendarPortletVersion 2.7.2 → 2.7.3 (picks up uPortal-Project/CalendarPortlet#404 — Underscore template settings override per call, fixes the events column rendering raw template source instead of event entries) - notificationPortletVersion 4.8.3 → 4.8.4 (picks up uPortal-Project/NotificationPortlet#695 — pin jjwt-impl + jjwt-jackson at runtime, fixes /api/v2 silently 403'ing and the notification web components rendering empty) Notes: locally redeployed the three webapps from Maven Central artifacts and ran the Playwright suite three times in a row — 117/117 each pass — to confirm the published versions match what we were testing against the SNAPSHOTs.
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.
Problem
Every request to
/NotificationPortlet/api/v2/notificationsreturns 403 Forbidden in 4.8.x deployments even when the caller sends a valid uPortal-issued OIDC bearer token. Both the<notification-icon>and<notification-list>web components power their UI off this endpoint, so they silently render empty in the portal.The 403 is logged on the server as:
The actual exception only surfaces if you turn
org.apereo.portal.soffit.securityto DEBUG:Root cause
WEB-INF/libof the deployed 4.8.x WAR contains onlyjjwt-api-0.11.5.jar. JJWT 0.10+ split into three artifacts:jjwt-api— interfaces (compile-time)jjwt-impl— runtime implementation (DefaultJwtParserlives here)jjwt-jackson— JSON parsinguPortal-soffit-core(the chain that brings JJWT into this project) declares the impl and jackson artifacts asruntimeOnly. Gradle does not propagateruntimeOnlytransitive dependencies through the deprecatedcompileconfiguration this project still uses, so neither impl nor jackson lands in this WAR's runtime classpath.Jwts.parser()throws on first call → the filter swallows that as "Bearer token is unusable" → Spring Security returns 403.Fix
Declare the runtime jars directly in
notification-portlet-webapp/build.gradle:Pin
jjwtVersion=0.11.5ingradle.propertiesto match the versionuPortal-soffit-coreships transitively (jjwt-api-0.11.5.jaris what's already on the classpath today).Verification
Redeployed against uPortal-start with
notificationPortletVersion=4.8.4-SNAPSHOT:GET /NotificationPortlet/api/v2/notifications→ 403, body empty, components render nothing.<notification-list>portlet renders the demo entries (Room Available, Bike License Ticket, Past Due Book, Advanced Computer Engineering 524, Math 101, …). The<notification-icon>badge shows a non-zero unread count.I added a Playwright regression guard in
uPortal-start's suite attests/ux/portlets/notifications.spec.tsthat asserts (a) the demo titles render in the DOM and (b) the API rejects missing/garbage bearer tokens with 403 — so a future regression in the runtime-classpath plumbing or in Spring Security itself fails loudly.Notes
uPortal-soffit-coreto declare the jars asapiinstead ofruntimeOnly. Decided against — that pollutes every consumer's compile classpath with implementation jars they shouldn't need at compile time. The right place is the consumer module that actually exercisesJwts.parser().4.8.4-SNAPSHOT, so this PR ships against that.