Skip to content

[jwt] add refresh tokens support - #19

Merged
capcom6 merged 1 commit into
masterfrom
jwt/refresh-token
May 29, 2026
Merged

[jwt] add refresh tokens support#19
capcom6 merged 1 commit into
masterfrom
jwt/refresh-token

Conversation

@capcom6

@capcom6 capcom6 commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

Release Notes

  • New Features
    • Login now returns access and refresh tokens for improved session management
    • Added /auth/refresh endpoint to obtain new access tokens
    • Added /auth/logout endpoint to revoke refresh tokens
    • Implemented automatic client-side token refresh on access token expiry
    • Sessions can now be securely rotated without requiring re-authentication

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@capcom6, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 56 minutes and 1 second. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a67999b2-f007-4909-bfed-dd433f7f371d

📥 Commits

Reviewing files that changed from the base of the PR and between 525b859 and cb60adc.

📒 Files selected for processing (17)
  • internal/config/config.go
  • internal/config/module.go
  • internal/db/migrations/20260527032544_refresh_tokens.sql
  • internal/jwt/config.go
  • internal/jwt/domain.go
  • internal/jwt/errors.go
  • internal/jwt/models.go
  • internal/jwt/module.go
  • internal/jwt/repository.go
  • internal/jwt/service.go
  • internal/server/auth/dto.go
  • internal/server/auth/handler.go
  • internal/server/docs/docs.go
  • internal/server/middlewares/jwtauth/jwtauth.go
  • requests.http
  • web/static/js/api.js
  • web/static/js/app.js
📝 Walkthrough

Walkthrough

This PR implements a complete JWT refresh token system spanning server and client. The backend introduces refresh token persistence via a new database table, a Service API for token generation/validation/revocation/rotation, and new /auth/refresh and /auth/logout endpoints. The frontend adds automatic access token refresh on 401 responses and updates error handling to defer logout decisions to the token refresh logic.

Changes

JWT Refresh Token Lifecycle

Layer / File(s) Summary
Configuration, Database Schema, and Domain Types
internal/config/config.go, internal/config/module.go, internal/db/migrations/20260527032544_refresh_tokens.sql, internal/jwt/config.go, internal/jwt/domain.go, internal/jwt/errors.go
JWT Config extends to include RefreshTTL duration field with a 7-day default; database migration creates refresh_tokens table with user foreign key, indexes, and revocation tracking; Token domain type and ErrRefreshTokenRevoked error introduced.
Refresh Token Persistence Layer
internal/jwt/models.go, internal/jwt/repository.go
Bun ORM refreshTokenModel with table metadata; Repository implements Create, FindByHash, RevokeByHash, and RevokeAllForUser for token storage and lifecycle, including nil-safe domain conversion.
JWT Service Token Generation and Lifecycle
internal/jwt/service.go, internal/jwt/module.go
Service gains refreshRepo dependency; GenerateTokenGenerateTokenPair (returns both access and refresh tokens); new methods GenerateRefreshToken, ValidateRefreshToken, RevokeRefreshToken, RotateRefreshToken, RotateTokenPair with SHA-256 token hashing for storage and lookup.
Authentication Endpoints and Handlers
internal/server/auth/dto.go, internal/server/auth/handler.go
LoginResponse now includes RefreshToken alongside AccessToken and User; new DTOs RefreshRequest, RefreshResponse, LogoutRequest with Swagger annotations; /auth/refresh handler validates refresh token and rotates pair; /auth/logout handler revokes token with 204 success regardless of prior state.
JWT Middleware Route Exemptions
internal/server/middlewares/jwtauth/jwtauth.go
JWT auth middleware Next check expanded to bypass validation for /api/v1/auth/refresh and /api/v1/auth/logout in addition to login/register.
Client-side Token Refresh and Retry
web/static/js/api.js
Module-level _refreshPromise deduplicates concurrent refresh attempts; getRefreshToken() reads from localStorage; refreshAccessToken() POSTs to /auth/refresh and updates stored tokens; apiFetch() retries on 401 (excluding refresh/login paths) by refreshing and updating the Authorization header.
Frontend Auth State Management and 401 Handling
web/static/js/app.js
Auth store login() persists refresh_token to localStorage and logout() clears it; 401 error handling across all page flows (dashboards, projects, tasks, admin, profile) changed from calling Alpine.store("auth").logout() to early return, allowing API module's auto-refresh to take precedence.
Testing, Documentation, and API Specification
requests.http, internal/server/docs/docs.go
Example requests demonstrate admin and user token lifecycle: login, refresh, and logout; Swagger docs updated to document refresh_token field in LoginResponse schema.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant API
    participant Service
    participant Repository
    User->>Frontend: Login
    Frontend->>API: POST /auth/login
    API->>Service: GenerateTokenPair(user)
    Service->>Repository: Create(userID, tokenHash, expiresAt)
    Service-->>API: accessToken, refreshToken
    API-->>Frontend: LoginResponse
    Frontend->>Frontend: Store tokens in localStorage
    User->>Frontend: Make authenticated request
    Frontend->>API: Fetch with accessToken
    API-->>Frontend: 401 Unauthorized (token expired)
    Frontend->>API: POST /auth/refresh with refreshToken
    API->>Service: ValidateRefreshToken(rawToken)
    Service->>Repository: FindByHash(tokenHash)
    Service->>Service: RotateTokenPair(oldToken, user)
    Service->>Repository: RevokeByHash(oldHash)
    Service->>Repository: Create(userID, newTokenHash, newExpiry)
    Service-->>API: newAccessToken, newRefreshToken
    API-->>Frontend: RefreshResponse
    Frontend->>Frontend: Update tokens in localStorage
    Frontend->>API: Retry original request with newAccessToken
    API-->>Frontend: 200 OK
    User->>Frontend: Logout
    Frontend->>API: POST /auth/logout with refreshToken
    API->>Service: RevokeRefreshToken(rawToken)
    Service->>Repository: RevokeByHash(tokenHash)
    API-->>Frontend: 204 No Content
    Frontend->>Frontend: Clear localStorage tokens
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • bit-issues/backend#2: Introduces the initial JWT Service/Module and auth handler wiring; this PR extends that foundation with refresh token persistence and rotation.
  • bit-issues/backend#15: Implements frontend auth flow; this PR builds on that work by adding token refresh retry logic and localStorage persistence in the same web/static/js/*.js modules.

Suggested labels

codex

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: adding refresh token support to the JWT system. It aligns with the comprehensive changes across multiple files including new refresh token domain types, repository, service methods, database migrations, API endpoints, and client-side implementations.
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.

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


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.

@coderabbitai coderabbitai Bot added the codex label May 27, 2026
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown

🤖 Pull request artifacts

Platform File
🐳 Docker GitHub Container Registry
🍎 Darwin arm64 backend_Darwin_arm64.tar.gz
🍎 Darwin x86_64 backend_Darwin_x86_64.tar.gz
🐧 Linux arm64 backend_Linux_arm64.tar.gz
🐧 Linux i386 backend_Linux_i386.tar.gz
🐧 Linux x86_64 backend_Linux_x86_64.tar.gz
🪟 Windows arm64 backend_Windows_arm64.zip
🪟 Windows i386 backend_Windows_i386.zip
🪟 Windows x86_64 backend_Windows_x86_64.zip

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
internal/jwt/service.go (1)

153-160: ⚖️ Poor tradeoff

Consider making token rotation atomic.

If RevokeRefreshToken succeeds but GenerateRefreshToken fails (e.g., database write error), the user loses their refresh token with no recovery path except re-authentication. Wrapping both operations in a database transaction would ensure the old token is only revoked if the new one is successfully created.

This is acceptable for now since users can re-login, but could cause poor UX under database instability.

🤖 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 `@internal/jwt/service.go` around lines 153 - 160, RotateRefreshToken currently
calls RevokeRefreshToken then GenerateRefreshToken separately, which can leave
the user without a refresh token if generation fails; change RotateRefreshToken
to perform both operations inside a single database transaction (use the
Service's DB/transaction helper) so that you begin a tx, call revoke and
generate using the transaction-aware variants (or pass the tx/context into
RevokeRefreshToken and GenerateRefreshToken or inline their DB operations),
commit the tx only after GenerateRefreshToken succeeds, and rollback on any
error; ensure errors from Begin/Commit/Rollback are handled and propagated.
🤖 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 `@internal/db/migrations/20260527032544_refresh_tokens.sql`:
- Line 16: Remove the stray standalone '---' line from the migration file
20260527032544_refresh_tokens.sql: open the migration SQL and delete the lone
line containing only '---' so it is not emitted to the DB as SQL; ensure the
rest of the migration content (up/down sections or SQL statements) remains
unchanged and the file still conforms to Goose migration formatting.

In `@internal/jwt/repository.go`:
- Around line 44-57: RevokeByHash should only match tokens that are not already
revoked to avoid race replay; change the update Where clause to include revoked
= false (e.g. .Where("token_hash = ? AND revoked = ?", tokenHash, false)) so the
UPDATE only affects unrevoked rows, keep the existing Set("revoked = ?", true)
and the RowsAffected check, and return ErrRefreshTokenRevoked when RowsAffected
== 0 as before.

In `@internal/server/auth/handler.go`:
- Around line 99-135: Add Swagger annotations to expose the new endpoints by
adding comment blocks above the handler functions: place `@Summary`, `@Description`
(optional), `@Tags` (e.g., Auth), `@Accept` json, `@Produce` json, `@Param` for the
request bodies, `@Success` responses (e.g., 200 for handleRefresh returning
RefreshResponse and 204 for handleLogout), and `@Router` entries for the paths
/auth/refresh [post] and /auth/logout [post]; update the comment blocks located
immediately above the handleRefresh and handleLogout function declarations so
the generator includes these endpoints in the API docs.
- Around line 114-117: The handler currently treats all errors from
h.jwtSvc.RotateTokenPair(...) as internal errors; change it to detect token-auth
errors (e.g., invalid or revoked refresh token) using errors.Is / errors.As
against the JWT service's sentinel errors (such as ErrInvalidRefreshToken or
ErrTokenRevoked / jwt package sentinel errors) and return an HTTP 401
unauthorized result instead of a 500; keep using the same RotateTokenPair call
and variables (accessToken, refreshToken, err) but branch on the error type and
return a mapped 401 "unauthorized" error/response when the error indicates a bad
refresh token, otherwise preserve the existing error handling for other
failures.

In `@requests.http`:
- Around line 29-44: The logout request is still using the original
adminRefreshToken; update it to use the rotated refresh token returned by the
refresh call (the same pattern used for adminAccessToken). Replace the body
value for "refresh_token" in the POST /auth/logout request to reference the
refresh token from the refresh response (e.g., the response variable used for
adminAccessToken, such as adminRefresh.response.body.$.refresh_token) so the
active rotated token is revoked. Ensure any environment variable or shorthand
like adminRefreshToken is reassigned to that response value if other requests
rely on it.

In `@web/static/js/api.js`:
- Around line 19-45: The refreshAccessToken function leaves the module-level
_refreshPromise set after a successful refresh, causing future callers to reuse
a stale resolved promise; update refreshAccessToken so that _refreshPromise is
cleared once the refresh settles successfully (complementing the existing .catch
handler) — for example, inside the inner async IIFE (the Promise assigned to
_refreshPromise) capture the parsed data/access_token into a local, persist
tokens, then set _refreshPromise = null before returning the access token (or
use a try/finally around the await/logic to ensure _refreshPromise is nulled on
both success and failure).

In `@web/static/js/app.js`:
- Around line 116-131: The logout() method calls getRefreshToken() which is not
defined in this scope; fix by either (A) reading the refresh token directly from
localStorage inside logout() (e.g. get the "refresh_token" value and use that as
rt before calling window.apiFetch("/auth/logout", ...)), or (B) export or attach
getRefreshToken from api.js so it is available where logout() runs (make
getRefreshToken accessible and then call that exported/attached function).
Update the reference in logout() (and keep the try/catch around the apiFetch) so
there is no direct call to an undefined identifier.

---

Nitpick comments:
In `@internal/jwt/service.go`:
- Around line 153-160: RotateRefreshToken currently calls RevokeRefreshToken
then GenerateRefreshToken separately, which can leave the user without a refresh
token if generation fails; change RotateRefreshToken to perform both operations
inside a single database transaction (use the Service's DB/transaction helper)
so that you begin a tx, call revoke and generate using the transaction-aware
variants (or pass the tx/context into RevokeRefreshToken and
GenerateRefreshToken or inline their DB operations), commit the tx only after
GenerateRefreshToken succeeds, and rollback on any error; ensure errors from
Begin/Commit/Rollback are handled and propagated.
🪄 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: 0e3038b4-832d-415a-a0a2-6ec13dcaee61

📥 Commits

Reviewing files that changed from the base of the PR and between 713a3ac and 525b859.

📒 Files selected for processing (17)
  • internal/config/config.go
  • internal/config/module.go
  • internal/db/migrations/20260527032544_refresh_tokens.sql
  • internal/jwt/config.go
  • internal/jwt/domain.go
  • internal/jwt/errors.go
  • internal/jwt/models.go
  • internal/jwt/module.go
  • internal/jwt/repository.go
  • internal/jwt/service.go
  • internal/server/auth/dto.go
  • internal/server/auth/handler.go
  • internal/server/docs/docs.go
  • internal/server/middlewares/jwtauth/jwtauth.go
  • requests.http
  • web/static/js/api.js
  • web/static/js/app.js

Comment thread internal/db/migrations/20260527032544_refresh_tokens.sql
Comment thread internal/jwt/repository.go
Comment thread internal/server/auth/handler.go
Comment thread internal/server/auth/handler.go
Comment thread requests.http
Comment thread web/static/js/api.js
Comment thread web/static/js/app.js
@capcom6
capcom6 requested a review from dudina-ma May 28, 2026 03:06
@capcom6
capcom6 force-pushed the jwt/refresh-token branch from 8eca748 to cb60adc Compare May 28, 2026 03:07
@capcom6 capcom6 added ready PR is ready to merge and removed codex labels May 28, 2026
@capcom6
capcom6 merged commit 909b639 into master May 29, 2026
8 checks passed
@capcom6
capcom6 deleted the jwt/refresh-token branch May 29, 2026 01:27
@coderabbitai coderabbitai Bot mentioned this pull request Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready PR is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant