feat(api): scoped, optionally expiring API tokens (#6201)

* security(api): add scoped expiring API tokens

* security(api): make scoped token lifecycle enforceable

---------

Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com>
This commit is contained in:
n0ctal
2026-08-15 18:31:49 +05:00
committed by GitHub
parent aecbad3ab1
commit 1230559e69
19 changed files with 785 additions and 94 deletions
@@ -1,66 +1,47 @@
---
title: API Tokens
description: >-
Manage Bearer tokens used for programmatic auth (bots, central panels acting
on this node, CI). Each token has a unique name and an enabled flag — disable
to revoke without deleting, delete to revoke permanently. Tokens are stored as
SHA-256 hashes and the plaintext is returned only once, in the create response
— it cannot be retrieved afterwards, so copy it then. Send one as
<code>Authorization: Bearer &lt;token&gt;</code> on any /panel/api/* request —
the token is a full-admin credential.
description: 'Manage Bearer tokens used for programmatic auth (bots, central
panels acting on this node, CI). Each token has a unique name and an enabled
flag — disable to revoke without deleting, delete to revoke permanently.
Tokens are stored as SHA-256 hashes and the plaintext is returned only once,
in the create response — it cannot be retrieved afterwards, so copy it then.
Send one as <code>Authorization: Bearer &lt;token&gt;</code> on any
/panel/api/* request — the token is a full-admin credential.'
full: true
_openapi:
preload:
- ./public/openapi.json
toc:
- depth: 2
title: >-
List every API token, enabled or not. The token value is never returned
only metadata.
url: >-
#list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
title: List every API token, enabled or not. The token value is never returned —
only metadata.
url: '#list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata'
- depth: 2
title: >-
Mint a new API token. Name must be unique and 1-64 characters; the token
string is server-generated and returned only in this response — it is
stored hashed and cannot be retrieved later.
url: >-
#mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
title: Mint a scoped API token. The server-generated plaintext is returned only
once and stored as a hash.
url: '#mint-a-scoped-api-token-the-server-generated-plaintext-is-returned-only-once-and-stored-as-a-hash'
- depth: 2
title: >-
Permanently delete a token. Any caller using it stops authenticating
title: Permanently delete a token. Any caller using it stops authenticating
immediately.
url: >-
#permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
url: '#permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately'
- depth: 2
title: >-
Toggle a token enabled/disabled without deleting it. Disabled tokens are
title: Toggle a token enabled/disabled without deleting it. Disabled tokens are
rejected by checkAPIAuth on the next request.
url: >-
#toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
url: '#toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request'
structuredData:
headings:
- content: >-
List every API token, enabled or not. The token value is never
returnedonly metadata.
id: >-
list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
- content: >-
Mint a new API token. Name must be unique and 1-64 characters; the
token string is server-generated and returned only in this response —
it is stored hashed and cannot be retrieved later.
id: >-
mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
- content: >-
Permanently delete a token. Any caller using it stops authenticating
- content: List every API token, enabled or not. The token value is never returned
— only metadata.
id: list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
- content: Mint a scoped API token. The server-generated plaintext is returned
only once and stored as a hash.
id: mint-a-scoped-api-token-the-server-generated-plaintext-is-returned-only-once-and-stored-as-a-hash
- content: Permanently delete a token. Any caller using it stops authenticating
immediately.
id: >-
permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
- content: >-
Toggle a token enabled/disabled without deleting it. Disabled tokens
id: permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
- content: Toggle a token enabled/disabled without deleting it. Disabled tokens
are rejected by checkAPIAuth on the next request.
id: >-
toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
id: toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
contents: []
---
+70 -6
View File
@@ -1033,17 +1033,25 @@
"ApiToken": {
"properties": {
"createdAt": {
"format": "int64",
"type": "integer"
},
"enabled": {
"type": "boolean"
},
"expiresAt": {
"format": "int64",
"type": "integer"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"scope": {
"type": "string"
},
"token": {
"description": "SHA-256 hash; the plaintext is shown only once at creation",
"type": "string"
@@ -1052,8 +1060,10 @@
"required": [
"createdAt",
"enabled",
"expiresAt",
"id",
"name",
"scope",
"token"
],
"type": "object"
@@ -1062,12 +1072,18 @@
"properties": {
"createdAt": {
"example": 1736000000,
"format": "int64",
"type": "integer"
},
"enabled": {
"example": true,
"type": "boolean"
},
"expiresAt": {
"example": 0,
"format": "int64",
"type": "integer"
},
"id": {
"example": 2,
"type": "integer"
@@ -1076,6 +1092,10 @@
"example": "central-panel-a",
"type": "string"
},
"scope": {
"example": "admin",
"type": "string"
},
"token": {
"example": "new-token-string",
"type": "string"
@@ -1084,8 +1104,10 @@
"required": [
"createdAt",
"enabled",
"expiresAt",
"id",
"name"
"name",
"scope"
],
"type": "object"
},
@@ -8817,7 +8839,7 @@
"tags": [
"API Tokens"
],
"summary": "Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.",
"summary": "Mint a scoped API token. The server-generated plaintext is returned only once and stored as a hash.",
"operationId": "post_panel_api_setting_apiTokens_create",
"requestBody": {
"required": true,
@@ -8829,14 +8851,26 @@
"name": {
"type": "string",
"description": "Human-readable label, e.g. \"central-panel-a\"."
},
"scope": {
"type": "string",
"description": "admin (default), monitor, or node-sync."
},
"expiresAt": {
"type": "integer",
"description": "Future Unix milliseconds, or 0 for no expiry."
}
},
"required": [
"name"
"name",
"scope",
"expiresAt"
]
},
"example": {
"name": "central-panel-a"
"name": "central-panel-a",
"scope": "node-sync",
"expiresAt": 1798761600000
}
}
}
@@ -8865,8 +8899,10 @@
"obj": {
"createdAt": 1736000000,
"enabled": true,
"expiresAt": 0,
"id": 2,
"name": "central-panel-a",
"scope": "admin",
"token": "new-token-string"
}
}
@@ -8916,6 +8952,28 @@
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"expectedScope": {
"type": "string",
"description": "Stored scope expected by the operator."
}
},
"required": [
"expectedScope"
]
},
"example": {
"expectedScope": "node-sync"
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
@@ -8970,14 +9028,20 @@
"enabled": {
"type": "boolean",
"description": "New enabled state."
},
"expectedScope": {
"type": "string",
"description": "Stored scope expected by the operator."
}
},
"required": [
"enabled"
"enabled",
"expectedScope"
]
},
"example": {
"enabled": false
"enabled": false,
"expectedScope": "node-sync"
}
}
}