Skip to content

feat(oauth): add refresh token rotation replay detection (RFC 6819)#104

Merged
appleboy merged 3 commits intomainfrom
worktree-new
Mar 14, 2026
Merged

feat(oauth): add refresh token rotation replay detection (RFC 6819)#104
appleboy merged 3 commits intomainfrom
worktree-new

Conversation

@appleboy
Copy link
Copy Markdown
Member

Summary

  • Add RevokeTokenFamily store method to bulk-revoke all tokens sharing a ParentTokenID (token family)
  • When token rotation is enabled, detect reuse of revoked/disabled refresh tokens and automatically revoke the entire token family per RFC 6819 §4.14.2
  • Log family revocation as a CRITICAL audit event (SUSPICIOUS_ACTIVITY) with synchronous write for security monitoring

Test plan

  • Store test: RevokeTokenFamily correctly revokes active family tokens, skips already-revoked, leaves unrelated tokens untouched
  • Service test: End-to-end replay detection in rotation mode — refresh, replay old token, verify entire family revoked
  • Service test: Fixed mode does NOT trigger family revocation on disabled token reuse
  • Service test: Rotation mode revokes family when disabled token is reused
  • make test passes (all existing + new tests)
  • make lint passes

🤖 Generated with Claude Code

- Add RevokeTokenFamily store method to revoke all tokens in a token family
- Detect reuse of revoked/disabled refresh tokens in rotation mode and revoke entire family
- Log token family revocation as CRITICAL audit event for security monitoring
- Add store and service tests for replay detection and fixed mode behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 14, 2026 10:06
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 14, 2026

Codecov Report

❌ Patch coverage is 48.00000% with 26 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/services/token.go 42.22% 22 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds refresh token rotation replay detection and token-family revocation, aligning refresh-token security behavior with RFC 6819 guidance and expanding tests around rotation vs fixed refresh modes.

Changes:

  • Introduces a Store.RevokeTokenFamily(parentTokenID) method to bulk-revoke tokens in a “family”.
  • Adds rotation-mode replay detection in TokenService.RefreshAccessToken, revoking a token family and writing a CRITICAL audit event when a revoked/disabled refresh token is reused.
  • Adds store + service tests covering family revocation and rotation/fixed-mode behavior.

Reviewed changes

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

File Description
internal/store/sqlite.go Adds RevokeTokenFamily DB update for revoking active tokens in a family.
internal/services/token.go Adds family-revocation helper with audit logging and hooks it into refresh-token validation.
internal/store/store_test.go Adds integration test validating RevokeTokenFamily revokes the expected tokens only.
internal/services/token_test.go Adds tests for rotation-mode replay detection and fixed/rotation behavior on disabled-token reuse.

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

Comment on lines +670 to +677
// RevokeTokenFamily revokes all tokens that share the same ParentTokenID (token family).
// This is used for refresh token rotation replay detection: when a revoked refresh token
// is reused, all tokens in the family must be invalidated to prevent stolen token abuse.
func (s *Store) RevokeTokenFamily(parentTokenID string) (int64, error) {
result := s.db.Model(&models.AccessToken{}).
Where("(parent_token_id = ? OR id = ?) AND status = ?", parentTokenID, parentTokenID, models.TokenStatusActive).
Update("status", models.TokenStatusRevoked)
return result.RowsAffected, result.Error
Comment on lines +470 to +476
// Determine the family root: use ParentTokenID if set, otherwise the token itself is the root
familyID := reusedToken.ParentTokenID
if familyID == "" {
familyID = reusedToken.ID
}

revokedCount, err := s.store.RevokeTokenFamily(familyID)
}

// Record metrics for each revoked token
if revokedCount > 0 {
require.NoError(t, err)
assert.Equal(t, models.TokenStatusRevoked, newToken.Status)
}

appleboy and others added 2 commits March 14, 2026 19:06
- Add TokenFamilyID field to AccessToken model for stable family tracking
- Set TokenFamilyID at initial token creation (ExchangeDeviceCode, ExchangeAuthorizationCode)
- Propagate TokenFamilyID through rotation chain in RefreshAccessToken
- Update RevokeTokenFamily to query by token_family_id instead of parent_token_id
- Fix metrics: call RecordTokenRevoked per revoked token instead of once with wrong type
- Add multi-rotation replay detection test (3 rotations then replay oldest token)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@appleboy appleboy merged commit 45ce9be into main Mar 14, 2026
17 checks passed
@appleboy appleboy deleted the worktree-new branch March 14, 2026 11:17
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