mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-28 13:00:15 +03:00
6.4 KiB
6.4 KiB
Stateless JWT Refresh Token Mechanism
Overview
GNS3 server now supports a stateless JWT refresh token mechanism for interactive sessions (e.g., Web UI). This allows clients to stay authenticated across page reloads without repeated username/password prompts, while keeping access tokens short-lived.
No new database table or migration is required — refresh tokens are signed JWTs using the same secret and algorithm as access tokens.
Architecture
graph TD
Client -->|login / authenticate| API[Controller API]
API -->|access_token + refresh_token| Client
Client -->|POST /refresh| Refresh[Refresh Endpoint]
Refresh -->|new access_token + new refresh_token| Client
Client -->|Bearer access_token| Protected[Protected Endpoints]
Protected -->|401| Client
Client -->|refresh_token in body| Refresh
Refresh -->|401 if invalid/expired/revoked| Client
Refresh -->|verify type, exp, ver| AuthService[AuthService]
AuthService -->|check token_version| DB[(users table)]
Business Process
Login / Authenticate Flow
sequenceDiagram
participant C as Client
participant API as Controller API
participant AS as AuthService
participant DB as Database
C->>API: POST /login or /authenticate (username + password)
API->>DB: authenticate_user()
DB-->>API: user (with token_version)
API->>AS: create_access_token(user, ver)
API->>AS: create_refresh_token(user, ver)
AS-->>API: access_token (type: access, exp: 15min)
AS-->>API: refresh_token (type: refresh, exp: 30d)
API-->>C: { access_token, token_type, refresh_token }
Refresh Flow (Silent Renewal)
sequenceDiagram
participant C as Client
participant API as Controller API
participant AS as AuthService
participant DB as Database
Note over C: access_token expired
C->>API: POST /refresh { refresh_token }
API->>AS: get_token_data(refresh_token)
AS-->>API: { username, ver, token_use: "refresh" }
API->>DB: get_user_by_username()
DB-->>API: user (with current token_version)
Note over API,DB: rejects if user not found, inactive, or token_version mismatch
API->>AS: create_access_token(user, ver)
API->>AS: create_refresh_token(user, ver)
AS-->>API: new access_token (sliding window)
AS-->>API: new refresh_token (sliding window)
API-->>C: { access_token, token_type, refresh_token }
C->>API: Retry original request with new access_token
Logout — Token Revocation
sequenceDiagram
participant C as Client
participant API as Controller API
participant DB as Database
C->>API: POST /logout (Bearer access_token)
API->>DB: logout_user(user_id) → token_version += 1
DB-->>API: done
API-->>C: 204 No Content
Note over C, DB: All existing access and refresh tokens with old ver are now invalid
API Endpoints
| Method | Path | Description | Authentication |
|---|---|---|---|
| POST | /v3/access/users/login |
Login with form data, returns access + refresh tokens | Public |
| POST | /v3/access/users/authenticate |
Login with JSON, returns access + refresh tokens | Public |
| POST | /v3/access/users/refresh |
Exchange a refresh token for a new access token + refresh token | Public (token itself proves identity) |
| POST | /v3/access/users/logout |
Revoke all tokens for the current user | Bearer token required |
POST /v3/access/users/refresh
Request:
{
"refresh_token": "<refresh_token>"
}
Response 200:
{
"access_token": "<new_access_token>",
"token_type": "bearer",
"refresh_token": "<new_refresh_token>"
}
Error Responses:
401— Invalid, expired, or revoked refresh token422— Missingrefresh_tokenfield in request body
Security Design
Token Claims
| Claim | Access Token | Refresh Token |
|---|---|---|
sub |
username | username |
exp |
24h (configurable) | 30d (configurable) |
ver |
user's token_version |
user's token_version |
type |
"access" |
"refresh" |
Key Security Properties
- Type-based isolation: Access tokens (
type: access) are rejected by/refresh. Refresh tokens (type: refresh) are rejected by HTTP and WebSocket authentication paths. This prevents a stolen long-lived refresh token from being used directly for API access. - Token version integration: Both token types carry the user's
token_version.logoutincrementstoken_versionin the database, immediately invalidating all outstanding access and refresh tokens. - Stateless (no replay detection): Since there is no
refresh_tokensdatabase table, a stolen refresh token remains valid until itsexpor until the user logs out. This is an accepted trade-off for avoiding a new table and migration. - Sliding window: Each
/refreshcall issues a new refresh token with a fresh expiry, keeping active sessions alive indefinitely until logout or inactivity.
Implementation Files
gns3server/services/authentication.py—_create_token,create_access_token,create_refresh_token,get_token_datagns3server/api/routes/controller/users.py—refresh_access_tokenendpoint handlergns3server/api/routes/controller/dependencies/authentication.py—_reject_refresh_tokenguard in HTTP and WebSocket pathsgns3server/schemas/controller/tokens.py—Token,TokenData,RefreshTokenRequestmodelsgns3server/schemas/config.py—jwt_refresh_token_expire_minutesconfiguration
Configuration
| Setting | Default | Description |
|---|---|---|
Controller.jwt_access_token_expire_minutes |
1440 (24h) | Access token TTL. Web UI recommends 15 min. |
Controller.jwt_refresh_token_expire_minutes |
43200 (30d) | Refresh token TTL. |
Controller.jwt_secret_key |
(random) | HMAC signing key for all JWT tokens. |
Notes
- Web UI integration: The client should implement a response interceptor that catches 401, silently calls
/refresh, and retries the original request. Multiple concurrent 401s should be queued with a single refresh request. - No per-session revocation: All tokens for a user share the same
token_version. Logout revokes everything. Per-session granularity would require adding arefresh_tokenstable. - Rate limiting:
/refreshis a public endpoint with a valid credential (the refresh token). Rate limiting is recommended if brute-force attacks are a concern.