Skip to content

[ANCHOR-1224]: Missing JWT audience/type binding in JwtService → SEP-10 user token usable as Platform-API admin credential under JWT secret reuse#1965

Merged
amandagonsalves merged 2 commits into
developfrom
fix/anchor-1224
Jul 6, 2026
Merged

[ANCHOR-1224]: Missing JWT audience/type binding in JwtService → SEP-10 user token usable as Platform-API admin credential under JWT secret reuse#1965
amandagonsalves merged 2 commits into
developfrom
fix/anchor-1224

Conversation

@amandagonsalves

Copy link
Copy Markdown
Collaborator

Description

The platform issues 7 functionally distinct JWT types (Sep10Jwt, Sep45Jwt, Sep24InteractiveUrlJwt, Sep24MoreInfoUrlJwt, Sep6MoreInfoUrlJwt, CallbackAuthJwt, PlatformAuthJwt), each signed with a separate HMAC secret. Tokens carried no aud claim - the only thing distinguishing a platform admin token from a user SEP-10 session token was which secret signed it. JwtService.decode selected the secret by the requested Java class and verified signature and expiry only; if any two secrets shared the same value, a token from one context was byte-for-byte valid in the other. PropertySecretConfig.validate() checked only the SEP-45 secret for cryptographic weakness and never checked whether any two secrets were equal, so the collapse went undetected at startup. The canonical dev.env example reused identical placeholder strings across distinct secret slots, normalising the dangerous configuration.

If an operator set any two of the 7 secrets to the same value, a token the attacker already held - any completed SEP-10 login, or a SEP-24 interactive URL handed to the user's browser — became a valid Platform API admin credential: read all customer transactions, forge money-movement events (request_offchain_funds, notify_offchain_funds_received), and drive transactions to terminal states.

The fix adds two independent, layered defences: audience binding closes the vulnerability regardless of configuration; the startup equality check makes the dangerous configuration impossible to deploy.

Changes

  • JwtService: added 7 AUD_* string constants, one per token type ("sep10", "sep45", "sep24_interactive", "sep24_more_info", "sep6_more_info", "callback_api", "platform_api").
  • JwtService.encode(WebAuthJwt): stamps aud=sep10 or aud=sep45 via .audience().add(aud).and() before signing.
  • JwtService.encode(MoreInfoUrlJwt): stamps aud=sep6_more_info or aud=sep24_more_info based on token type.
  • JwtService.encode(Sep24InteractiveUrlJwt): stamps aud=sep24_interactive.
  • JwtService.encode(ApiAuthJwt, String, String): private method gains an aud parameter; encode(CallbackAuthJwt) and encode(PlatformAuthJwt) pass AUD_CALLBACK_API and AUD_PLATFORM_API respectively.
  • JwtService.decode: tracks expectedAud per class alongside secret in the existing dispatch chain; after signature verification, extracts Claims.getAudience() and throws JwtException if the set is null or does not contain expectedAud.
  • PropertySecretConfig.validate(): collects all 7 configured JWT secrets into a map (skipping null/empty), checks every pair for equality, and rejects with error code secrets.jwt.must_be_unique on any collision, naming both config keys in the message.

Acceptance Criteria

  • A Sep10Jwt encoded and decoded as Sep10Jwt round-trips correctly (all existing roundtrip tests pass unchanged).
  • A Sep10Jwt cipher is rejected when decoded as PlatformAuthJwt, even when sep10JwtSecret == platformAuthSecret.
  • A PlatformAuthJwt cipher is rejected when decoded as Sep10Jwt, even when secrets collide.
  • PropertySecretConfig.validate() emits secrets.jwt.must_be_unique for every colliding pair when any two secrets share a value.
  • PropertySecretConfig.validate() passes without error when all 7 configured secrets are distinct.

Context

#3810399

Testing

  • Unit: ./gradlew :core:test --tests "org.stellar.anchor.auth.JwtServiceTest"
  • Unit: ./gradlew :platform:test --tests "org.stellar.anchor.platform.config.SecretConfigTest"
  • Full suites: ./gradlew :core:test and ./gradlew :platform:test

Documentation

N/A

Known limitations

Tokens issued before this change (no aud claim) will be rejected by the updated decode. All platform JWT types are short-lived so no active tokens survive a server restart.

* add explicit aud (audience) claims to all generated jwt tokens

* add jwt audience validation during decoding to prevent token type confusion

* add a configuration check to reject non-unique jwt secrets
@amandagonsalves amandagonsalves self-assigned this Jul 3, 2026
Copilot AI review requested due to automatic review settings July 3, 2026 18:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens JWT type separation across the Anchor Platform by binding each token class to an aud claim during issuance and enforcing that expected audience during decoding, eliminating token-type confusion even if HMAC secrets are accidentally reused. It also adds startup validation to ensure all configured JWT secrets are unique.

Changes:

  • Added per-token-type aud stamping across all JWT encoders in JwtService.
  • Enforced expected aud membership in JwtService.decode(...) for each supported JWT class.
  • Added PropertySecretConfig.validate() checks to reject any equality collisions across the 7 JWT secrets, plus tests covering single and multiple collisions.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
platform/src/test/kotlin/org/stellar/anchor/platform/config/SecretConfigTest.kt Adds validation tests for JWT secret collisions and distinct-secret success cases.
platform/src/main/java/org/stellar/anchor/platform/config/PropertySecretConfig.java Enforces uniqueness across all configured JWT secrets at startup/validation time.
core/src/test/kotlin/org/stellar/anchor/auth/JwtServiceTest.kt Adds regression tests proving aud prevents cross-type decoding under secret collisions.
core/src/main/java/org/stellar/anchor/auth/JwtService.java Adds audience constants, stamps aud during encoding, and validates aud during decoding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread platform/src/test/kotlin/org/stellar/anchor/platform/config/SecretConfigTest.kt Outdated
Comment thread core/src/main/java/org/stellar/anchor/auth/JwtService.java Outdated
* refactor jwt service by removing duplicate subject call

* update test assertion in secret config test
@amandagonsalves
amandagonsalves requested a review from JiahuiWho July 3, 2026 20:43
@amandagonsalves
amandagonsalves merged commit 6f81c20 into develop Jul 6, 2026
20 of 26 checks passed
@amandagonsalves
amandagonsalves deleted the fix/anchor-1224 branch July 6, 2026 13:50
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.

3 participants