fix(api-tokens): harden with security fixes, design compliance, and test coverage (#567)

Security: add JWT-level expiry ceiling (400d), per-user token count limit (25),
and token name uniqueness enforcement. Fix async clipboard copy.

Design: migrate Select to Combobox, apply card bevel styling, fix icon
strokeWidth, add tabular-nums to timestamps, fix destructive button pattern.

Tests: expand from ~20 to 47 test cases covering creation validation, token
limits, name uniqueness, last_used_at tracking, ownership constraints, delete
edge cases, and registry blocked endpoints.

Docs: update API Tokens docs with token limits, name uniqueness, registry
restrictions, and JWT expiry ceiling. Update OpenAPI spec with 409 response.
This commit is contained in:
Anso
2026-04-13 18:32:07 -04:00
committed by GitHub
parent 02c2d24004
commit e0d1ca9dc0
8 changed files with 328 additions and 45 deletions
+7
View File
@@ -33,6 +33,7 @@ Regardless of scope, **all** API tokens are blocked from:
| **Node management** | Adding, updating, or deleting remote nodes |
| **License management** | Activating or deactivating license keys |
| **Token management** | Creating, listing, or revoking API tokens |
| **Registry management** | Viewing, creating, updating, deleting, or testing registry credentials |
| **Console access** | Generating console session tokens for interactive terminals |
These restrictions ensure that API tokens cannot escalate privileges or modify the identity and infrastructure configuration of your Sencho instance. These operations require a human user session (browser login).
@@ -47,6 +48,10 @@ These restrictions ensure that API tokens cannot escalate privileges or modify t
- **Expiration**: Choose 30 days, 60 days, 90 days, 1 year, or no expiration. Tokens without an expiration must be revoked manually.
4. Click **Create**. A green banner appears with the raw token value. Copy it immediately using the copy button.
<Note>
Each user can have up to **25 active API tokens**. Token names must be unique among your active tokens. If you need to reuse a name, revoke the existing token first.
</Note>
<Frame>
<img src="/images/api-tokens/api-tokens-overview.png" alt="API Tokens management view in Settings Hub showing the token list and create form" />
</Frame>
@@ -101,7 +106,9 @@ Click the trash icon next to any token in the API Tokens settings tab. A confirm
## Security model
- **Hashed storage**: Only a SHA-256 hash of the token is stored in the database. The raw token is never persisted.
- **JWT-level expiry ceiling**: Every token includes a built-in expiry at the JWT level as defense-in-depth, independent of the user-configured expiration. The database-level expiry is always the tighter constraint.
- **Audit trail**: All actions performed via API tokens are recorded in the [Audit Log](/features/audit-log) under the creating user's username.
- **Optional expiry**: Tokens can be created with an expiration period (30 days, 60 days, 90 days, or 1 year). Tokens without an expiry must be revoked manually when no longer needed.
- **Per-user limits**: Each user can create up to 25 active tokens. Token names must be unique among active tokens for the same user.
- **Usage tracking**: Each token tracks when it was last used, visible in the token list.
- **Scope enforcement**: Permission checks happen at the middleware level before any route handler executes, ensuring consistent enforcement across all endpoints.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 38 KiB

+9 -3
View File
@@ -1303,7 +1303,7 @@ paths:
**Note:** API tokens cannot create other API tokens.
responses:
"201":
description: Token created. The `token` field contains the full JWT — save it now, it won't be shown again.
description: Token created. The `token` field contains the full JWT. Save it now; it will not be shown again.
content:
application/json:
schema:
@@ -1316,13 +1316,19 @@ paths:
type: string
description: Full JWT token. Store securely — this is the only time it's returned.
"400":
description: Validation error.
description: Validation error (missing/invalid fields, or maximum of 25 active tokens reached).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
$ref: "#/components/responses/Forbidden"
"409":
description: An active token with this name already exists.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
requestBody:
@@ -1335,7 +1341,7 @@ paths:
properties:
name:
type: string
description: Human-readable token name.
description: Human-readable token name. Must be unique among the user's active tokens.
maxLength: 100
example: CI/CD Deploy Token
scope: