Skip to content

fix(deps): upgrade jjwt from 0.11.5 to 0.13.0 - #3010

Open
Naenyn wants to merge 4 commits into
uPortal-Project:masterfrom
Naenyn:upgrade/jjwt-0.13.0
Open

Naenyn wants to merge 4 commits into
uPortal-Project:masterfrom
Naenyn:upgrade/jjwt-0.13.0

Conversation

@Naenyn

@Naenyn Naenyn commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Migrate all JJWT usage to the 0.12+ API:

  • Replace Jwts.parser().setSigningKey(String) with Jwts.parser().verifyWith(SecretKey).build().parseSignedClaims()
  • Replace Jwts.builder().signWith(algorithm, String) with Jwts.builder().signWith(SecretKey)
  • Replace getBody() with getPayload()
  • Replace Jwts.claims().setX() builder with Map-based claims
  • Derive SecretKey via Keys.hmacShaKeyFor(Decoders.BASE64.decode())
  • Remove JwtSignatureAlgorithmFactory from AbstractJwtService and IdTokenFactory (algorithm inferred from key size)

Affected modules: uPortal-soffit-core, uPortal-soffit-renderer, uPortal-security-core, uPortal-spring

Migrate all JJWT usage to the 0.12+ API:
- Replace Jwts.parser().setSigningKey(String) with
  Jwts.parser().verifyWith(SecretKey).build().parseSignedClaims()
- Replace Jwts.builder().signWith(algorithm, String) with
  Jwts.builder().signWith(SecretKey)
- Replace getBody() with getPayload()
- Replace Jwts.claims().setX() builder with Map-based claims
- Derive SecretKey via Keys.hmacShaKeyFor(Decoders.BASE64.decode())
- Remove JwtSignatureAlgorithmFactory from AbstractJwtService and
  IdTokenFactory (algorithm inferred from key size)

Affected modules: uPortal-soffit-core, uPortal-soffit-renderer,
uPortal-security-core, uPortal-spring
@Naenyn
Naenyn requested a review from bjagg August 7, 2026 22:17

@bjagg bjagg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this on — the 0.11 → 0.12+ API migration is unavoidable work and the mechanical parts look right. Before merging I rebuilt the new code paths against real jjwt 0.13.0 jars on Java 11 to check the parts I was least sure about. Three worries turned out to be non-issues, and three things I'd like to resolve.

What I verified as safe

  • The Map<String,Object> claims rewrite round-trips correctly. I was concerned that putting java.util.Date values into a raw map under Claims.EXPIRATION / Claims.ISSUED_AT would skip the RFC date conversion that .expiration(...) / .issuedAt(...) apply. It doesn't — Jwts.builder().claims().add(map) runs the same converters, and the payload serializes as numeric epoch seconds:

    {"sub":"student","iss":"Soffit","groups":["Students"],"exp":1789681303,"iat":1789677703,"class":"...Bearer","jti":"..."}
    

    So the expiry check in parseEncryptedToken still behaves.

  • Default deployments see no algorithm change. DEFAULT_SIGNATURE_KEY decodes to 1072 bits, so Keys.hmacShaKeyFor yields HmacSHA512 — identical to the old SIGNATURE_ALGORITHM_DEFAULT = HS512.

  • Cross-version interop holds. This was my main worry, since uPortal-soffit-core is a published artifact and a remote soffit app may still be on 0.11.5 after the portal upgrades. I signed a token with the new 0.13 path and parsed it with 0.11.5's Jwts.parser().setSigningKey(base64Key).parseClaimsJws(...) — verifies fine, subject and custom claims intact. No forced lockstep upgrade for deployers' soffit apps.

What I'd like resolved

1. JwtSignatureAlgorithmFactory becomes dead config rather than being removed.

After this PR nothing consumes it, but the @Component and its documented property org.apereo.portal.soffit.jwt.signatureAlgorithm (@since 5.6.1, default HS512) are still there. The algorithm is now inferred from key length instead. A deployer who deliberately set HS256 and holds a >=512-bit key will silently start emitting HS512 with no warning in the logs. Interop-wise that's survivable given the 0.11.5 result above, but it will bite anyone whose downstream consumer pins an expected alg.

Either direction is fine by me, but I'd like it to be deliberate: delete the class and the property with a note in the upgrade docs, or keep honoring it via signWith(key, Jwts.SIG.HS256/HS384/HS512).

2. Key-derivation failures move from request time to boot time.

Keys.hmacShaKeyFor(Decoders.BASE64.decode(signatureKey)) now runs in @PostConstruct (AbstractJwtService, IdTokenFactory) and in the SoffitApiPreAuthenticatedProcessingFilter constructor. I confirmed both sides of this:

  • jjwt 0.13.0: a sub-256-bit key throws WeakKeyException at derivation, i.e. now at context startup.
  • jjwt 0.11.5 on master: signWith(SignatureAlgorithm.HS512, shortKey) throws WeakKeyException too — but only when a token is first created.

So a deployer with an undersized org.apereo.portal.soffit.jwt.signatureKey who uses neither soffits nor OIDC id tokens goes from a dormant misconfiguration to a portal that won't start, and the stack trace won't obviously name the property. Failing fast is defensible, but could we catch it in init() and rethrow with a message naming SIGNATURE_KEY_PROPERTY?

3. No test coverage for the rewritten paths.

This changes token creation and parsing across four modules, and there's no create -> parse round-trip test in the tree today — uPortal-soffit-core has only JwtEncryptorTest, and the green CI here isn't evidence that the new code is correct, just that nothing exercises it. The harness I wrote to check the above is basically the missing test: build claims via createClaims, sign, parse, assert subject / exp / jti / a custom collection claim survive. Could we land that against BearerService or AbstractJwtService as part of this PR? It would also lock in the interop behavior so a future jjwt bump can't quietly break it.

Happy to pair on any of these if it's easier — none of it is a rewrite, and the migration itself looks sound.

bjagg and others added 3 commits September 22, 2026 18:21
Resolves the gradle.properties conflict: keep jjwtVersion=0.13.0 from this
branch, take jodaTimeVersion=2.14.3 from master (uPortal-Project#3018).
Problem: the jjwt 0.13 migration dropped both uses of
JwtSignatureAlgorithmFactory but left the @component and its documented
property org.apereo.portal.soffit.jwt.signatureAlgorithm in the tree, so
a deployer who set HS256 would silently start emitting a different alg
with nothing in the logs. Separately, jjwt 0.12+ derives the HMAC key
eagerly, so an undersized signatureKey now fails at context startup
instead of on first token use, and jjwt's WeakKeyException does not name
the property responsible.

Goal: leave the documented configuration working as documented, and make
the new boot-time failure diagnosable.

Changes:
- JwtSignatureAlgorithmFactory returns a MacAlgorithm resolved from the
  Jwts.SIG registry, falling back to HS512 with a warning naming the
  property (previously a bare exception message)
- AbstractJwtService and IdTokenFactory sign with
  signWith(secretKey, algorithmFactory.getAlgorithm()) again
- add AbstractJwtService.deriveKey(String), which wraps WeakKeyException
  in an IllegalStateException naming SIGNATURE_KEY_PROPERTY and the
  256-bit minimum; route all three derivation sites through it
- drop the now-unused Keys/Decoders imports

Notes: verified against jjwt 0.13.0 that signWith only enforces a MINIMUM
key length per algorithm, so the 1072-bit DEFAULT_SIGNATURE_KEY is valid
for HS256, HS384 and HS512 alike -- honoring the property does not
require re-deriving the key per algorithm.

Refs: uPortal-Project#3010

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Problem: the jjwt 0.11 -> 0.13 migration rewrote token creation and
parsing across four modules, and nothing in the tree exercised either
path -- uPortal-soffit-core had only JwtEncryptorTest. A green build said
nothing about whether tokens still round-tripped.

Goal: pin the token contract so a future jjwt upgrade cannot quietly
change it, and cover the two behaviours fixed in the previous commit.

Changes:
- add BearerServiceTest with six cases: create/parse round-trip asserting
  username, groups and both single- and multi-value attribute shapes
  survive; expired token rejected; token signed with a different key
  rejected; signatureAlgorithm property honored (HS256 appears in the
  JWT header); unsupported algorithm falls back to HS512; undersized
  signatureKey fails at init naming the property

Notes: JwtEncryptor is mocked as a pass-through rather than constructed
-- the real one instantiates jasypt's BasicTextEncryptor, which needs
commons-logging that this module's test runtime does not have. Field
injection uses plain reflection for the same reason, since Spring's
ReflectionTestUtils pulls in commons-logging too.

Refs: uPortal-Project#3010

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bjagg

bjagg commented Sep 23, 2026

Copy link
Copy Markdown
Member

I've pushed three commits to this branch rather than leave it parked — it's the keystone for the
5.17.10 release and NotificationPortlet #703, and the remaining work was mine to unblock. All
appends, no force-push, so the threads above are intact. Revert or rework any of it freely.

a1cbd349 — merge master. The only conflict was gradle.properties: kept your
jjwtVersion=0.13.0, took jodaTimeVersion=2.14.3 from master (#3018). No Java file had moved
on master since August, so nothing in the migration itself needed redoing.

1501d3f0 — the two open items.

  • JwtSignatureAlgorithmFactory now returns a MacAlgorithm from the Jwts.SIG registry, and
    both signing sites use signWith(secretKey, algorithmFactory.getAlgorithm()) again, so
    org.apereo.portal.soffit.jwt.signatureAlgorithm keeps working as documented. I went with
    honoring it rather than deleting it after checking the thing I wasn't sure about: jjwt 0.13.0
    enforces only a minimum key length per algorithm, so the 1072-bit DEFAULT_SIGNATURE_KEY
    signs fine as HS256, HS384 or HS512. Honoring the property needs no per-algorithm key
    derivation. An unrecognized value now warns and names the property instead of logging a bare
    exception message.
  • Added AbstractJwtService.deriveKey(String), which wraps WeakKeyException in an
    IllegalStateException naming SIGNATURE_KEY_PROPERTY and the 256-bit minimum. All three
    derivation sites (AbstractJwtService, IdTokenFactory, SoffitApiPreAuthenticatedProcessingFilter)
    route through it, so a deployer with an undersized key gets a message that names the property
    rather than a bare jjwt stack trace at startup.

c33d67d8 — the missing round-trip test. BearerServiceTest, six cases: create/parse
round-trip asserting username, groups and both single- and multi-value attribute shapes survive;
expired token rejected; token signed with a different key rejected; the algorithm property
honored (HS256 in the JWT header); unsupported algorithm falls back to HS512; undersized key
fails at init naming the property.

Worth saying that the first four passed against your migration unchanged — the round-trip,
expiry and signature-verification behavior was already correct. Only the two cases covering the
items above were red before 1501d3f0.

Two notes on the test, both to avoid adding a dependency: JwtEncryptor is mocked as a
pass-through because constructing the real one pulls jasypt's BasicTextEncryptor, which needs
commons-logging that this module's test runtime doesn't have; field injection uses plain
reflection for the same reason, since Spring's ReflectionTestUtils drags in commons-logging too.

verGJF and the soffit-core, soffit-renderer and security-core suites are green locally on
Java 11. Once CI confirms, I'll merge and cut 5.17.10, which unblocks NotificationPortlet #703.

This branch has not been deployed

No deployments
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.

2 participants