The Normal JWT Request Flow#
sequenceDiagram
participant User as User
participant Frontend as Frontend (browser/app)
participant Auth as Auth server
participant Backend as Backend API server
Note over User,Backend: 1. Login phase
User->>Frontend: enter username/password
Frontend->>Auth: POST /login (credentials)
Auth->>Auth: verify credentials
Auth-->>Frontend: return access_token + refresh_token
Frontend->>Frontend: store tokens (memory/localStorage)
Frontend-->>User: login successful
Note over User,Backend: 2. Normal request phase
User->>Frontend: request a protected resource
Frontend->>Backend: GET /api/resource
Authorization: Bearer access_token
Backend->>Backend: verify access_token signature and expiry
Backend-->>Frontend: return the requested resource
Frontend-->>User: display data
Note over User,Backend: 3. Access token expires
User->>Frontend: keep requesting
Frontend->>Backend: GET /api/resource
Authorization: Bearer access_token (expired)
Backend-->>Frontend: 401 Unauthorized (token expired)
Frontend->>Frontend: detects 401, triggers refresh logic
Note over Frontend,Auth: 4. Token refresh phase
Frontend->>Auth: POST /refresh
refresh_token
Auth->>Auth: verify refresh_token
Auth-->>Frontend: return a new access_token (optionally a new refresh_token)
Frontend->>Frontend: update the stored access_token
Frontend->>Backend: retry the original request (new access_token)
Backend-->>Frontend: return the requested resource
Frontend-->>User: display data
Why You Can’t Rely on JWT Alone#
- Once a refresh token is issued and the backend doesn’t store it, it becomes an uncontrollable long-term pass
- You can’t forcibly log a user out
- You can’t log out a single device
- You can’t tell whether a token has been stolen (an attacker who gets the refresh token can keep refreshing the access token forever)
- You can’t implement session management
The Flow After Using a refresh_tokens Table#
sequenceDiagram
participant User as User
participant Frontend as Frontend
participant Auth as Auth server
participant DB as Database (refresh_tokens table)
participant Backend as Backend API server
Note over User,DB: 1. Login phase (issue token + persist)
User->>Frontend: enter username/password
Frontend->>Auth: POST /login
Auth->>Auth: verify credentials
Auth->>DB: generate a unique refresh_token_id
INSERT INTO refresh_tokens
(user_id, token_hash, expires_at,
device_info, ip_address, revoked)
DB-->>Auth: insert successful
Auth->>Auth: generate access_token + refresh_token
(refresh_token contains the id reference)
Auth-->>Frontend: return access_token + refresh_token
Frontend->>Frontend: store tokens
Frontend-->>User: login successful
Note over User,Backend: 2. Normal request (same as before)
User->>Frontend: request a resource
Frontend->>Backend: GET /api/resource
Authorization: Bearer access_token
Backend->>Backend: verify access_token
Backend-->>Frontend: return the resource
Note over Frontend,DB: 3. Access token expires → refresh
Frontend->>Backend: GET /api/resource (access_token expired)
Backend-->>Frontend: 401 Unauthorized
Frontend->>Auth: POST /refresh
refresh_token
Note over Auth,DB: 4. Refresh verification (multi-step checks)
Auth->>Auth: parse refresh_token, extract token_id
Auth->>DB: SELECT * FROM refresh_tokens
WHERE id = token_id
DB-->>Auth: return the record
Auth->>Auth: verification checklist:
Note over Auth: ✅ does token_hash match
✅ is it expired (expires_at > now())
✅ is it revoked (revoked = false)
✅ is the user still valid
✅ does device info match (optional)
alt all checks pass
Auth->>DB: UPDATE refresh_tokens
SET last_used_at = now(),
last_used_ip = current_ip
WHERE id = token_id
Auth->>Auth: generate a new access_token
(optional: extend refresh_token validity)
Auth-->>Frontend: return the new access_token
Frontend->>Frontend: update access_token
Frontend->>Backend: retry the original request (new token)
Backend-->>Frontend: return the resource
else checks fail
Auth-->>Frontend: 401/403 (refresh failed)
Frontend->>Frontend: clear all tokens
Frontend->>User: redirect to the login page
end
Note over User,DB: 5. Explicit logout
User->>Frontend: click logout
Frontend->>Auth: POST /logout
refresh_token
Auth->>DB: UPDATE refresh_tokens
SET revoked = true
WHERE id = token_id
Auth-->>Frontend: logout successful
Frontend->>Frontend: clear local tokens
Frontend-->>User: logged out
Note over User,DB: 6. Security scenario: password change
User->>Frontend: change password
Frontend->>Auth: POST /change-password
Auth->>DB: UPDATE refresh_tokens
SET revoked = true
WHERE user_id = current_user_id
Note over DB: revoke all of the user's refresh_tokens
(force all devices to log in again)
Auth-->>Frontend: password change successful
Frontend->>Frontend: clear local tokens
Frontend-->>User: please log in again