ali-ahnaf / ali-ahnaf/pocket_pixel
Implement refresh tokens (short-lived access + rotating refresh token)
- Ngôn ngữ chính
- TypeScript
- Star
- 14
- Fork
- 91
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Mô tả
## 🔄 Context / background
Today our authentication uses a **single long-lived JWT** — a 30-day access token issued at login (see [packages/api/src/services/auth.service.ts](packages/api/src/services/auth.service.ts) and the auth routes in [packages/api/src/routes/auth/](packages/api/src/routes/auth/)). That token is what every API request is authenticated with.
A single 30-day token is a security trade-off: if it's stolen, an attacker has access for up to 30 days, and we have no easy way to revoke it.
> 💡 **Terms:**
> - **Access token** — short-lived (e.g. 15 min), sent with every API request to prove who you are.
> - **Refresh token** — longer-lived, stored safely, used *only* to get a new access token when the old one expires. It can be revoked, which logs the user out everywhere.
## 🎯 Problem / goal
Replace the single long-lived token with a **two-token system**:
1. A **short-lived access token** (e.g. 15 minutes) used for normal API calls.
2. A **longer-lived refresh token** (e.g. 30 days) used to silently obtain new access tokens without forcing the user to log in again.
This shrinks the window an attacker has if an access token leaks, and gives us a way to revoke sessions.
## 🛠️ Suggested approach
**Storage / entity**
1. Create a `RefreshToken` entity in [packages/api/src/entities/](packages/api/src/entities/) (extend `BaseEntity`, use soft delete per project conventions). Store a **hashed** token value, the owning `userId`, an `expiresAt`, and a `revokedAt`. Never store the raw token.
2. Generate a migration: `npm run migration:generate`, then `npm run migration:run`.
**Auth service & routes (`packages/api`)**
3. On sign-in / sign-up ([sign-in.route.ts](packages/api/src/routes/auth/sign-in.route.ts), [sign-up.route.ts](packages/api/src/routes/auth/sign-up.route.ts)): issue **both** a short-lived access token and a refresh token; persist the refresh token (hashed).
4. Add a new `POST /api/auth/refresh` route + service method that validates the refresh token, and **rotates** it (issue a new refresh token, revoke the old one) along with a fresh access token.
5. Update sign-out ([sign-out.route.ts](packages/api/src/routes/auth/sign-out.route.ts)) to revoke the user's refresh token(s).
6. Add the access-token expiry as a configurable value (env / `env.ts`).
**Shared DTOs (`packages/shared`)**
7. Add/update request & response contracts for the refresh flow in `packages/shared/src/contracts/` and rebuild with `npm run build:shared`.
**UI (`packages/ui`)**
8. When an API call returns `401` because the access token expired, automatically call `/api/auth/refresh` once and retry the original request (an `axios` response interceptor in [packages/ui/src/lib/api/ApiClient.ts](packages/ui/src/lib/api/ApiClient.ts) is a good place).
> 📝 **Note:** This pairs naturally with moving sessions into `httpOnly` cookies (see the related cookie issue) — ideally the refresh token lives in an `httpOnly` cookie.
## ✅ Acceptance criteria
- [ ] Login/registration returns a short-lived access token **and** a refresh token.
- [ ] Refresh tokens are stored hashed in the database, with expiry and revocation support.
- [ ] A `POST /api/auth/refresh` endpoint issues a new access token and **rotates** the refresh token.
- [ ] Logout revokes the refresh token so it can no longer be used.
- [ ] The UI transparently refreshes an expired access token and retries the failed request.
- [ ] Shared DTOs for the refresh flow live in `packages/shared` and the package is rebuilt.
- [ ] Unit tests cover issuing, refreshing, rotating, and revoking tokens.
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Đánh giá
Issue này chưa được đánh giá.