Skip to content

[jwt] revoke tokens on password change#211

Open
capcom6 wants to merge 1 commit intomasterfrom
codex/plan-jwt-token-invalidation-on-password-change
Open

[jwt] revoke tokens on password change#211
capcom6 wants to merge 1 commit intomasterfrom
codex/plan-jwt-token-invalidation-on-password-change

Conversation

@capcom6
Copy link
Member

@capcom6 capcom6 commented Mar 15, 2026

Motivation

  • Ensure all previously issued JWTs for a user are invalidated immediately after a password change without enumerating every token JTI.

Description

  • Add RevokeAllByUser(ctx, userID string) to the jwt.Service interface and a repository implementation that updates revoked_at for active tokens matching user_id.
  • Add service.RevokeAllByUser to forward to the repository with metrics and a no-op implementation in the disabled service.
  • Wire bulk token revocation into the password-change flow by introducing a TokenRevoker interface, accepting it in users.NewService, and calling tokenRevoker.RevokeAllByUser after updating the password.
  • Add a design doc docs/jwt-password-change-invalidation-plan.md describing the approach, SQL snippet, trade-offs, observability, and testing plan.

Testing

  • No automated tests were added or executed in this change; unit and integration tests described in the new design doc are planned as follow-up work.

Codex Task

Summary by CodeRabbit

  • New Features
    • Users can now revoke all their active authentication tokens at once.
    • Changing your password now automatically revokes all previously issued authentication tokens to prevent continued access with old credentials.

@coderabbitai
Copy link

coderabbitai bot commented Mar 15, 2026

Walkthrough

Adds user-based token revocation across the JWT stack: new Service.RevokeByUser, repository implementation to mark non-expired tokens revoked, metrics adjustments, a no-op disabled implementation, and wiring into the users service (revokes tokens, then updates password).

Changes

Cohort / File(s) Summary
JWT Service Interface
internal/sms-gateway/jwt/jwt.go
Added RevokeByUser(ctx context.Context, userID string) error to the Service interface.
JWT Service Implementation
internal/sms-gateway/jwt/service.go, internal/sms-gateway/jwt/disabled.go
Implemented RevokeByUser on the concrete service (calls repository and metrics) and added a no-op RevokeByUser on the disabled implementation that returns nil.
Repository
internal/sms-gateway/jwt/repository.go
Added Repository.RevokeByUser(ctx, userID) (int64, error) which updates revoked_at = NOW() for non-expired, non-revoked tokens and returns affected row count.
Metrics
internal/sms-gateway/jwt/metrics.go
Changed IncrementTokensRevoked signature to IncrementTokensRevoked(status string, value ...int) to allow optional custom increment amounts.
User Service Integration
internal/sms-gateway/users/service.go
Added jwtSvc jwt.Service field and constructor parameter; ChangePassword now calls jwtSvc.RevokeByUser before updating the password.

Sequence Diagram

sequenceDiagram
    participant Client
    participant UserService as User Service
    participant JWTService as JWT Service
    participant Repository
    participant Database
    participant Metrics

    Client->>UserService: ChangePassword(userID, oldPwd, newPwd)
    UserService->>JWTService: RevokeByUser(ctx, userID)
    JWTService->>Repository: RevokeByUser(ctx, userID)
    Repository->>Database: UPDATE tokens SET revoked_at = NOW() WHERE user_id=... AND revoked_at IS NULL AND expires_at > NOW()
    Database-->>Repository: rows_affected
    Repository-->>JWTService: count, error
    JWTService->>Metrics: IncrementTokensRevoked("revoked", count)
    Metrics-->>JWTService: ok
    JWTService-->>UserService: error (if any)
    alt no error
        UserService->>Database: Update user password hash
        Database-->>UserService: ok
        UserService->>Client: success
    else error
        UserService->>Client: error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title '[jwt] revoke tokens on password change' accurately describes the main change: adding JWT token revocation functionality when a user changes their password.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

@github-actions
Copy link

github-actions bot commented Mar 15, 2026

🤖 Pull request artifacts

Platform File
🐳 Docker GitHub Container Registry
🍎 Darwin arm64 server_Darwin_arm64.tar.gz
🍎 Darwin x86_64 server_Darwin_x86_64.tar.gz
🐧 Linux arm64 server_Linux_arm64.tar.gz
🐧 Linux i386 server_Linux_i386.tar.gz
🐧 Linux x86_64 server_Linux_x86_64.tar.gz
🪟 Windows arm64 server_Windows_arm64.zip
🪟 Windows i386 server_Windows_i386.zip
🪟 Windows x86_64 server_Windows_x86_64.zip

@capcom6 capcom6 force-pushed the codex/plan-jwt-token-invalidation-on-password-change branch 2 times, most recently from e11005d to bb1638a Compare March 16, 2026 01:14
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/sms-gateway/jwt/repository.go (1)

112-114: Consider index coverage for this bulk-update predicate.

This path can become hot on password changes. Ensure an index supports user_id, revoked_at, and expires_at to avoid table scans under load.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/sms-gateway/jwt/repository.go` around lines 112 - 114, The
bulk-update in repository.go using
r.db.WithContext(...).Model((*tokenModel)(nil)).Where("user_id = ? and
revoked_at is null and expires_at > NOW()", userID).Update(...) can cause table
scans; add a supporting DB index for tokenModel to cover the predicate. Add a
composite index on (user_id, revoked_at, expires_at) via your migrations/schema
changes (or a partial index on user_id WHERE revoked_at IS NULL if your DB
supports partial indexes) so the Update query can use an index and avoid full
table scans under high load.
internal/sms-gateway/jwt/metrics.go (1)

115-118: Consider stricter API design for counter increments to prevent misuse.

The IncrementTokensRevoked function accepts variadic int input and forwards directly to Counter.Add(float64(...)). Since Prometheus Counter.Add panics on negative values, the variadic signature creates a weak API surface.

Current call sites are safe—they pass RowsAffected (a database row count, always non-negative)—but defensive improvements would prevent future misuse. Consider either:

  1. Removing the variadic parameter entirely and requiring explicit calls to Inc() or Add(value) at the call site, or
  2. Validating or rejecting negative values explicitly rather than silently dropping them.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/sms-gateway/jwt/metrics.go` around lines 115 - 118, Change the weak
variadic API of Metrics.IncrementTokensRevoked to require an explicit
non-variadic integer and validate it before calling the Prometheus counter:
replace func (m *Metrics) IncrementTokensRevoked(status string, value ...int)
with func (m *Metrics) IncrementTokensRevoked(status string, value int), then
check value >= 0 and return or log/error if negative (do not call Counter.Add
with negative), and finally call
m.tokensRevokedCounter.WithLabelValues(status).Add(float64(value)); update call
sites that previously passed RowsAffected to use the new signature.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/sms-gateway/jwt/service.go`:
- Around line 268-272: The error-path metric currently increments StatusError by
int(revoked) which is often zero and undercounts failures; change the logic to
compute the actual failed count (e.g., failed := attemptedTotal - revoked where
attemptedTotal is the total number of tokens you tried to revoke such as
len(ids) or the total variable used in the surrounding function) and call
s.metrics.IncrementTokensRevoked(StatusError, int(failed)) on err != nil, while
keeping s.metrics.IncrementTokensRevoked(StatusSuccess, int(revoked)) for the
success path; update the code around s.metrics.IncrementTokensRevoked,
StatusError, StatusSuccess, and the revoked variable to use the computed failed
value.

In `@internal/sms-gateway/users/service.go`:
- Around line 113-119: The password update and JWT revocation are non-atomic: if
s.users.UpdatePassword succeeds but s.jwtSvc.RevokeByUser fails the old tokens
remain valid; change the flow so the operation fails closed or becomes atomic —
either call s.jwtSvc.RevokeByUser(ctx, username) before committing the new
password and only persist the new hash if revocation succeeds, or perform both
steps inside a single transactional operation (e.g., add a transactional method
that invokes s.users.UpdatePassword and token revocation together and rolls back
the password update on revocation failure); ensure any temporary/transactional
API surfaces clearly reference s.users.UpdatePassword and s.jwtSvc.RevokeByUser
so failures return an error and do not leave old tokens valid.

---

Nitpick comments:
In `@internal/sms-gateway/jwt/metrics.go`:
- Around line 115-118: Change the weak variadic API of
Metrics.IncrementTokensRevoked to require an explicit non-variadic integer and
validate it before calling the Prometheus counter: replace func (m *Metrics)
IncrementTokensRevoked(status string, value ...int) with func (m *Metrics)
IncrementTokensRevoked(status string, value int), then check value >= 0 and
return or log/error if negative (do not call Counter.Add with negative), and
finally call m.tokensRevokedCounter.WithLabelValues(status).Add(float64(value));
update call sites that previously passed RowsAffected to use the new signature.

In `@internal/sms-gateway/jwt/repository.go`:
- Around line 112-114: The bulk-update in repository.go using
r.db.WithContext(...).Model((*tokenModel)(nil)).Where("user_id = ? and
revoked_at is null and expires_at > NOW()", userID).Update(...) can cause table
scans; add a supporting DB index for tokenModel to cover the predicate. Add a
composite index on (user_id, revoked_at, expires_at) via your migrations/schema
changes (or a partial index on user_id WHERE revoked_at IS NULL if your DB
supports partial indexes) so the Update query can use an index and avoid full
table scans under high load.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52449b7d-080b-49a0-8258-2341f3ce7dc3

📥 Commits

Reviewing files that changed from the base of the PR and between bb48cff and bb1638a.

📒 Files selected for processing (6)
  • internal/sms-gateway/jwt/disabled.go
  • internal/sms-gateway/jwt/jwt.go
  • internal/sms-gateway/jwt/metrics.go
  • internal/sms-gateway/jwt/repository.go
  • internal/sms-gateway/jwt/service.go
  • internal/sms-gateway/users/service.go

@capcom6 capcom6 force-pushed the codex/plan-jwt-token-invalidation-on-password-change branch from 3bdbe3f to ac00753 Compare March 17, 2026 01:15
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
internal/sms-gateway/jwt/disabled.go (1)

39-43: Inconsistent error behavior with RevokeToken.

RevokeByUser returns nil while RevokeToken (Line 37) returns ErrDisabled. If this is intentional to allow password changes when JWT is disabled, consider adding a comment explaining the design rationale. Otherwise, align the behavior with other methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/sms-gateway/jwt/disabled.go` around lines 39 - 43, The disabled JWT
service has inconsistent behavior: RevokeByUser (method on type disabled)
returns nil while RevokeToken returns ErrDisabled; either make RevokeByUser
return ErrDisabled to match RevokeToken or, if the nil return is intentional to
permit password changes when JWT is disabled, add an explanatory comment above
the RevokeByUser method documenting that design choice and why it differs from
RevokeToken. Ensure you reference the disabled type, RevokeByUser, RevokeToken,
and ErrDisabled when making the change so behavior is consistent or clearly
documented.
internal/sms-gateway/jwt/repository.go (1)

109-119: Implementation looks correct.

The query logic properly targets active tokens (non-revoked, non-expired) for the specified user. The pattern is consistent with the existing Revoke method.

Consider adding a composite index on (user_id, revoked_at, expires_at) to optimize this query if users accumulate many tokens. The current schema has separate indexes which may require index merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/sms-gateway/jwt/repository.go` around lines 109 - 119, Add a
composite index covering the tokenModel fields used in RevokeByUser (user_id,
revoked_at, expires_at) to avoid index-merge overhead: update the tokenModel
schema (or add a migration) to create an index on (user_id, revoked_at,
expires_at) — e.g., add a GORM index tag on tokenModel or write a migration that
creates idx_user_revoked_expires on those three columns and ensure it runs
during schema migrations so the WHERE clause in RevokeByUser benefits from the
composite index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/sms-gateway/jwt/disabled.go`:
- Around line 39-43: The disabled JWT service has inconsistent behavior:
RevokeByUser (method on type disabled) returns nil while RevokeToken returns
ErrDisabled; either make RevokeByUser return ErrDisabled to match RevokeToken
or, if the nil return is intentional to permit password changes when JWT is
disabled, add an explanatory comment above the RevokeByUser method documenting
that design choice and why it differs from RevokeToken. Ensure you reference the
disabled type, RevokeByUser, RevokeToken, and ErrDisabled when making the change
so behavior is consistent or clearly documented.

In `@internal/sms-gateway/jwt/repository.go`:
- Around line 109-119: Add a composite index covering the tokenModel fields used
in RevokeByUser (user_id, revoked_at, expires_at) to avoid index-merge overhead:
update the tokenModel schema (or add a migration) to create an index on
(user_id, revoked_at, expires_at) — e.g., add a GORM index tag on tokenModel or
write a migration that creates idx_user_revoked_expires on those three columns
and ensure it runs during schema migrations so the WHERE clause in RevokeByUser
benefits from the composite index.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: afb15f67-4417-4dcb-81ce-6dd80b878f38

📥 Commits

Reviewing files that changed from the base of the PR and between bb1638a and ac00753.

📒 Files selected for processing (6)
  • internal/sms-gateway/jwt/disabled.go
  • internal/sms-gateway/jwt/jwt.go
  • internal/sms-gateway/jwt/metrics.go
  • internal/sms-gateway/jwt/repository.go
  • internal/sms-gateway/jwt/service.go
  • internal/sms-gateway/users/service.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/sms-gateway/jwt/service.go
  • internal/sms-gateway/jwt/jwt.go

@capcom6 capcom6 marked this pull request as ready for review March 18, 2026 01:58
@capcom6 capcom6 changed the title Invalidate all user JWTs on password change (bulk revoke by user) [jwt] revoke tokens on password change Mar 18, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac00753ce2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@capcom6 capcom6 added the deployed The PR is deployed on staging label Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex deployed The PR is deployed on staging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant