Skip to content

fix(deps): pin jjwt-impl + jjwt-jackson at runtime (fixes /api/v2 403)#695

Merged
bjagg merged 3 commits into
uPortal-Project:masterfrom
bjagg:fix/jjwt-runtime-classpath
May 5, 2026
Merged

fix(deps): pin jjwt-impl + jjwt-jackson at runtime (fixes /api/v2 403)#695
bjagg merged 3 commits into
uPortal-Project:masterfrom
bjagg:fix/jjwt-runtime-classpath

Conversation

@bjagg

@bjagg bjagg commented May 5, 2026

Copy link
Copy Markdown
Member

Problem

Every request to /NotificationPortlet/api/v2/notifications returns 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:

INFO  o.a.p.s.s.SoffitApiPreAuthenticatedProcessingFilter
  - The following Bearer token is unusable: 'eyJhbGciOiJIUzUxMiJ9...'

The actual exception only surfaces if you turn org.apereo.portal.soffit.security to DEBUG:

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?

Root cause

WEB-INF/lib of the deployed 4.8.x WAR contains only jjwt-api-0.11.5.jar. JJWT 0.10+ split into three artifacts:

  • jjwt-api — interfaces (compile-time)
  • jjwt-impl — runtime implementation (DefaultJwtParser lives here)
  • jjwt-jackson — JSON parsing

uPortal-soffit-core (the chain that brings JJWT into this project) declares the impl and jackson artifacts as runtimeOnly. Gradle does not propagate runtimeOnly transitive dependencies through the deprecated compile configuration 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:

runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}"

Pin jjwtVersion=0.11.5 in gradle.properties to match the version uPortal-soffit-core ships transitively (jjwt-api-0.11.5.jar is what's already on the classpath today).

Verification

Redeployed against uPortal-start with notificationPortletVersion=4.8.4-SNAPSHOT:

  • Before: GET /NotificationPortlet/api/v2/notifications → 403, body empty, components render nothing.
  • After: 200 OK, body is the expected JSON array of notifications. The <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 at tests/ux/portlets/notifications.spec.ts that 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

  • Considered fixing this upstream by changing uPortal-soffit-core to declare the jars as api instead of runtimeOnly. 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 exercises Jwts.parser().
  • The release plugin commits at HEAD already moved the version to 4.8.4-SNAPSHOT, so this PR ships against that.

bjagg added 3 commits May 4, 2026 15:29
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 bjagg merged commit 7c7d16d into uPortal-Project:master May 5, 2026
3 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant