Refresh Access Token
Exchange a refresh token (from cookie or body) for a new access + refresh JWT pair. Token rotation enforced.
POST /api/v1/auth/refresh — 🌐 Public · Rate limit: 60 req / hour
Rotates the refresh token. Reads from the biore_refresh httpOnly cookie by default, or from body.refreshToken as fallback. Issues a new access token + new refresh token (cookie). Detects refresh token reuse → revokes ALL sessions for the user (security alert).
Token reuse detection: if a refresh token is presented after it's been rotated, the entire user session family is revoked and a security alert email is sent. Clients must always discard the old refresh token immediately upon receiving a new one.
Request
Cookies
| Cookie | Notes |
|---|---|
biore_refresh | httpOnly, Secure, SameSite=Strict, scoped to .bio.re. Auto-sent by browsers; preferred path. |
Body — RefreshDto (cookie fallback only)
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
refreshToken | string | — | @IsOptional() @IsString() | Used only when cookie is absent (e.g., mobile app non-web client) |
Response
200 OK — ApiResponseOf<LoginResponseDto>
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 900
}
}A new biore_refresh cookie is set. The old refresh token is revoked (Session record updated).
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
401 | auth.refresh.invalid_token | Refresh token not found, expired, or already rotated |
401 | auth.refresh.token_reuse_detected | Security alert — token reused after rotation; all user sessions revoked |
401 | auth.refresh.account_suspended | User.status = SUSPENDED between issuance and refresh |
429 | (throttle) | Rate limit exceeded (60 req/hour) |
Side effects
- Verify refresh token signature + lookup
Sessionrecord. - Reuse check: if
Session.rotatedAt != null, all sessions for thisUserare revoked + admin alert + email to user (security incident). - On valid refresh: mark old
Session.rotatedAt = now(), create new Session with new refresh token, issue new access token. - Update httpOnly cookie with new refresh token.
- Audit log:
auth.refresh.successorauth.refresh.token_reuse_detected.
Code samples
# Cookie-based (preferred):
curl -X POST https://api.bio.re/api/v1/auth/refresh \
-b cookies.txt -c cookies.txt
# Body-based (mobile / non-cookie clients):
curl -X POST https://api.bio.re/api/v1/auth/refresh \
-H 'Content-Type: application/json' \
-d '{"refreshToken": "..."}'async function refresh(): Promise<{ accessToken: string; expiresIn: number }> {
const res = await fetch('https://api.bio.re/api/v1/auth/refresh', {
method: 'POST',
credentials: 'include', // sends + receives biore_refresh cookie
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Refresh failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useRefresh() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => {
const res = await fetch('/api/v1/auth/refresh', {
method: 'POST',
credentials: 'include',
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Refresh failed'), {
code: json?.error?.code,
});
}
return json.data;
},
onError: (err: { code?: string }) => {
if (err.code === 'auth.refresh.token_reuse_detected') {
// Security incident — force re-login
qc.clear();
router.push('/login?reason=security_alert');
}
},
});
}Try it
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Refresh token (optional — prefer httpOnly cookie)
Response Body
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/auth/refresh" \ -H "Content-Type: application/json" \ -d '{}'{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 900,
"requiresTwoFactor": true,
"tempToken": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/auth/auth.controller.ts | 166–186 |
| DTO (request) | apps/api-core/src/modules/auth/dto/index.ts | 98–101 (RefreshDto) |
| DTO (response) | apps/api-core/src/modules/auth/dto/response.dto.ts | 17–29 (LoginResponseDto) |
| Service | apps/api-core/src/modules/auth/auth.service.ts | refresh(), reuse detection logic |
| Prisma model | packages/prisma/prisma/schema.prisma | Session.rotatedAt (token rotation tracking) |