Personal Access Tokens
Overview
Personal Access Tokens (PATs) provide named, scoped, time-limited tokens for programmatic API access. They are distinct from the existing Token model, which continues to serve UI/session authentication. PATs use the w_ prefix to distinguish them from OIDC JWT tokens.
Token Format
Format: w_<unix_timestamp>_<random> (e.g., w_1735689599_Abc123def...)
w_prefix identifies the token as a Waldur PAT<unix_timestamp>is the expiration time, visible by inspecting the token<random>is 256 bits of entropy viasecrets.token_urlsafe(32)- SHA-256 hash stored in database; plaintext shown only once at creation
- First 8 characters stored as
token_prefixfor UI identification
The embedded timestamp allows humans and scripts to check expiry without an API call. The server still validates against the database on every request.
Authentication
PATs use the Authorization: Bearer header:
1 | |
The PATAuthentication class in waldur_core.core.authentication handles PAT requests. It is registered in DEFAULT_AUTHENTICATION_CLASSES between SessionAuthentication and OIDCAuthentication:
1 2 3 4 5 6 | |
The w_ prefix lets PATAuthentication claim the request and fall through to OIDCAuthentication for non-PAT Bearer tokens.
API Endpoints
Base URL: /api/personal-access-tokens/
| Method | Path | Description |
|---|---|---|
| GET | /api/personal-access-tokens/ |
List user's tokens (no plaintext) |
| POST | /api/personal-access-tokens/ |
Create a new token (plaintext returned once) |
| GET | /api/personal-access-tokens/{uuid}/ |
Retrieve token details (no plaintext) |
| DELETE | /api/personal-access-tokens/{uuid}/ |
Soft-revoke (sets is_active=False) |
| POST | /api/personal-access-tokens/{uuid}/rotate/ |
Atomic rotation: creates new token, revokes old |
| GET | /api/personal-access-tokens/available_scopes/ |
Lists all delegatable permission enum values |
PUT and PATCH are disabled -- tokens are immutable after creation.
Create Token
POST /api/personal-access-tokens/
Request:
1 2 3 4 5 | |
Response (201, with Cache-Control: no-store header):
1 2 3 4 5 6 7 8 | |
Rotate Token
POST /api/personal-access-tokens/{uuid}/rotate/
Atomically revokes the old token and creates a new one with the same name, scopes, and expiration. The operation uses select_for_update() to prevent races. Returns the same response format as create.
Revoke Token
DELETE /api/personal-access-tokens/{uuid}/
Sets is_active=False on the token (soft delete). Returns 204.
Scope Enforcement
PAT scopes follow an intersection model: the effective permission at request time is the intersection of the PAT's scopes and the user's current roles.
1 | |
Key implementation details in waldur_core.permissions.utils:
_pat_scope_check()runs in bothhas_permission()andhas_any_permission()before theis_staffbypass, so staff PATs are properly scoped- If a user loses a role after creating a PAT, the PAT's matching permissions stop working immediately
- Scope validation at creation ensures users can only request permissions they currently have
Security
PAT-via-PAT Blocked
The create, destroy, and rotate actions reject requests authenticated with a PAT. This prevents token escalation -- PATs can only be managed through session or token authentication.
Uniform Error Responses
All authentication failures return the same "Invalid token." message regardless of the failure reason (inactive, expired, nonexistent). This prevents information leakage about token state.
User Deactivation Cascade
The revoke_user_pats_on_deactivation signal handler in waldur_core.core.handlers revokes all active PATs when a user's is_active changes from True to False.
Global Kill Switch
The PAT_ENABLED Constance setting (default: True) controls whether PAT authentication is active. When False, PATAuthentication.authenticate() returns None for all w_-prefixed tokens, effectively disabling all PATs system-wide.
Configuration
Constance Settings
| Setting | Default | Description |
|---|---|---|
PAT_ENABLED |
True |
Global enable/disable for PAT authentication |
PAT_MAX_LIFETIME_DAYS |
365 |
Maximum token lifetime in days |
PAT_MAX_TOKENS_PER_USER |
20 |
Maximum active tokens per user |
These settings are in the "Personal Access Tokens" fieldset in the Constance admin panel.
Model
PersonalAccessToken in waldur_core.core.models:
| Field | Type | Description |
|---|---|---|
user |
ForeignKey(User) | Token owner |
name |
CharField | User-assigned name |
token_prefix |
CharField | First 8 chars of token (for UI display) |
token_hash |
CharField(64) | SHA-256 hash (unique, indexed) |
scopes |
JSONField | List of PermissionEnum values |
expires_at |
DateTimeField | Expiration timestamp |
is_active |
BooleanField | Whether the token is active |
last_used_at |
DateTimeField | Last usage timestamp |
last_used_ip |
GenericIPAddressField | Last client IP |
use_count |
PositiveIntegerField | Total usage count |
Usage Tracking
The last_used_at, last_used_ip, and use_count fields are updated via a batched write with 10-minute cache-based debouncing (cache key: pat_usage:{pk}). This avoids a database write on every authenticated request.
Audit Events
| Event Type | Trigger |
|---|---|
pat_created |
Token created |
pat_revoked |
Token revoked (manual or user deactivation cascade) |
pat_rotated |
Token rotated |
pat_expired |
Token expired (deactivated by cleanup task) |
pat_used_from_new_ip |
Token used from a different IP than last_used_ip |
All events are scoped to the token's user and registered under USER_MANAGEMENT_EVENTS in waldur_core.logging.enums.
Celery Tasks
The cleanup_expired_personal_access_tokens task runs every 6 hours. It deactivates all PATs where expires_at <= now() and is_active=True, emitting a pat_expired event for each.
Examples
Create a PAT
1 2 3 4 5 6 7 8 | |
Use the PAT
1 2 | |
Rotate a PAT
1 2 | |
Revoke a PAT
1 2 | |
See Also
- Service Accounts -- programmatic access at organizational/project scope
- Identity Bridge -- federated identity management