gns3-server/docs/features/refresh-token-mechanism.md

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 token
  • 422 — Missing refresh_token field 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. logout increments token_version in the database, immediately invalidating all outstanding access and refresh tokens.
  • Stateless (no replay detection): Since there is no refresh_tokens database table, a stolen refresh token remains valid until its exp or until the user logs out. This is an accepted trade-off for avoiding a new table and migration.
  • Sliding window: Each /refresh call 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_data
  • gns3server/api/routes/controller/users.pyrefresh_access_token endpoint handler
  • gns3server/api/routes/controller/dependencies/authentication.py_reject_refresh_token guard in HTTP and WebSocket paths
  • gns3server/schemas/controller/tokens.pyToken, TokenData, RefreshTokenRequest models
  • gns3server/schemas/config.pyjwt_refresh_token_expire_minutes configuration

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 a refresh_tokens table.
  • Rate limiting: /refresh is a public endpoint with a valid credential (the refresh token). Rate limiting is recommended if brute-force attacks are a concern.