Skip to content

Add replay protection to private_key_jwt client assertions - #4673

Merged
ThaminduDilshan merged 1 commit into
thunder-id:mainfrom
thiva-k:fix-privatekey-jti
Aug 7, 2026
Merged

Add replay protection to private_key_jwt client assertions#4673
ThaminduDilshan merged 1 commit into
thunder-id:mainfrom
thiva-k:fix-privatekey-jti

Conversation

@thiva-k

@thiva-k thiva-k commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Purpose

private_key_jwt client assertions had no replay protection: jti was never tracked,
unlike DPoP proofs and ID-JAG assertions which both use the shared oauth2/jti deny-list.
A captured, still-valid client_assertion could be replayed to mint tokens as that client
without its private key. Affects all endpoints behind ClientAuthMiddleware. (CWE-294.)

Approach

Wire the existing jti deny-list into the private_key_jwt path, mirroring DPoP/ID-JAG:
after verification, record the assertion's jti (now required) and reject replays. The
entry is bounded by the assertion's exp plus clock-skew leeway. Threaded the shared
jtiStore and leeway through ClientAuthMiddleware into all five consuming endpoints.

Summary by CodeRabbit

  • Security Enhancements
    • Added replay protection for OAuth client assertions.
    • Assertions now require valid, one-time-use identifiers and expiration claims.
    • Reused, incomplete, or invalid assertions are rejected.
    • Consistent replay protection and configurable JWT clock tolerance are applied across OAuth authentication flows.

@thiva-k thiva-k added Type/Improvement trigger-pr-builder Add when the PR is ready for CI; starts the PR Builder for this and all later pushes labels Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

OAuth client assertion validation now uses shared JTI storage and JWT leeway. OAuth initialization passes these values through PAR, CIBA, token, introspection, and revocation route setup.

Changes

OAuth JTI replay protection

Layer / File(s) Summary
Client assertion replay validation
backend/internal/oauth/oauth2/clientauth/clientauth.go, backend/internal/oauth/oauth2/clientauth/clientauth_test.go
Assertions require jti and exp claims. The validation records each JTI until expiration plus leeway and rejects replayed assertions.
Client authentication middleware wiring
backend/internal/oauth/oauth2/clientauth/middleware.go, backend/internal/oauth/oauth2/clientauth/middleware_test.go
The middleware accepts and forwards the JTI store and leeway. Existing middleware tests use the expanded signature.
OAuth route initialization
backend/internal/oauth/init.go, backend/internal/oauth/oauth2/{ciba,introspect,par,revocation,token}/init*.go
OAuth initialization creates a shared JTI store and passes it with configured JWT leeway to route registration and client authentication middleware.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: thamindudilshan, senthalan, anushasunkada

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: replay protection for private_key_jwt client assertions.
Description check ✅ Passed The description clearly covers the purpose and approach, but it omits the template’s related items, checklist, and security checks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/internal/oauth/oauth2/clientauth/clientauth_test.go (1)

1516-1521: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the JTI retention deadline.

These expectations accept any expiry value. Assert time.Unix(9999999999+testLeeway, 0) in one RecordJTI expectation. Otherwise, removing leeway from recordAssertionJTI keeps the replay tests passing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/oauth/oauth2/clientauth/clientauth_test.go` around lines
1516 - 1521, Update the two RecordJTI expectations in the replay test to assert
the retention deadline explicitly: use time.Unix(9999999999+testLeeway, 0) for
the expiry argument in one expectation instead of mock.Anything, while
preserving the existing return values and call ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/internal/oauth/oauth2/clientauth/clientauth.go`:
- Around line 312-335: Update the relevant OAuth client-authentication guide
under docs/content/guides/ and the request semantics in docs/content/apis.mdx to
document private_key_jwt requirements: jti is mandatory, each assertion is
single-use across client-authenticated OAuth endpoints, replay attempts fail,
and replay retention lasts until exp plus the configured JWT leeway. Ensure the
documentation matches recordAssertionJTI behavior.

---

Nitpick comments:
In `@backend/internal/oauth/oauth2/clientauth/clientauth_test.go`:
- Around line 1516-1521: Update the two RecordJTI expectations in the replay
test to assert the retention deadline explicitly: use
time.Unix(9999999999+testLeeway, 0) for the expiry argument in one expectation
instead of mock.Anything, while preserving the existing return values and call
ordering.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 64180c34-8fa4-4c98-850e-88d468d7508e

📥 Commits

Reviewing files that changed from the base of the PR and between 98caa2d and d242ac3.

📒 Files selected for processing (12)
  • backend/internal/oauth/init.go
  • backend/internal/oauth/oauth2/ciba/init.go
  • backend/internal/oauth/oauth2/clientauth/clientauth.go
  • backend/internal/oauth/oauth2/clientauth/clientauth_test.go
  • backend/internal/oauth/oauth2/clientauth/middleware.go
  • backend/internal/oauth/oauth2/clientauth/middleware_test.go
  • backend/internal/oauth/oauth2/introspect/init.go
  • backend/internal/oauth/oauth2/introspect/init_test.go
  • backend/internal/oauth/oauth2/par/init.go
  • backend/internal/oauth/oauth2/revocation/init.go
  • backend/internal/oauth/oauth2/revocation/init_test.go
  • backend/internal/oauth/oauth2/token/init.go

Comment thread backend/internal/oauth/oauth2/clientauth/clientauth.go
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...end/internal/oauth/oauth2/clientauth/clientauth.go 80.95% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Comment thread backend/internal/oauth/oauth2/clientauth/clientauth.go

@ThaminduDilshan ThaminduDilshan 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.

LGTM

@ThaminduDilshan
ThaminduDilshan added this pull request to the merge queue Aug 7, 2026
Merged via the queue into thunder-id:main with commit c36a5b7 Aug 7, 2026
86 of 87 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trigger-pr-builder Add when the PR is ready for CI; starts the PR Builder for this and all later pushes Type/Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants