- Python 90.3%
- HTML 9.2%
- Mako 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
[tool.splent.config] lists the variables and their defaults, matching the os.getenv fallbacks in config.py, so a product can see what it may override without reading the source. Credentials stay out: their place is the product's .env, which is not committed. |
||
| src | ||
| .gitignore | ||
| MANIFEST.in | ||
| pyproject.toml | ||
| README.md | ||
splent_feature_apikeys
Developer API tokens for a SPLENT product. A port of the UVLHub apikeys
feature to SPLENT conventions, and the only place in a product where a
password is exchanged for a credential a machine can carry.
It owns tokens and nothing else. It does not know what the tokens are used
for, and it never touches the auth feature: credentials are checked through
the AuthenticationService that auth already registers.
The model
ApiToken belongs to one user and keeps:
| Column | Meaning |
|---|---|
user_id |
Owner, foreign key to auth's user.id |
name |
Human label chosen by the owner |
token_hash |
sha256 of the plaintext, hex. The plaintext is never stored |
prefix |
First 15 characters of the plaintext, indexed, safe to show |
scopes |
Comma separated, as in UVLHub |
created_at, expires_at, revoked_at, last_used_at |
lifecycle |
A plaintext token looks like splent_<64 hex characters>. The splent_
label makes a leaked token greppable; the prefix makes verification a single
indexed lookup instead of a table scan.
Scopes
read_spl and write_spl. Unknown names are dropped, and a request that
asks for nothing but unknown names is refused rather than silently producing
a token that can do nothing.
The service
ApiTokenService, registered under that name in the service locator.
api_token_service = service_proxy("ApiTokenService")
row, plaintext = api_token_service.mint(user, "laptop", ["write_spl"], ttl_days=90)
api_token_service.verify(plaintext) # row, or None if unknown/revoked/expired
api_token_service.find(plaintext) # row whatever its state, to explain a refusal
api_token_service.revoke(row.id, user) # only the owner can revoke
api_token_service.touch(row) # last_used_at, at most once every 5 minutes
api_token_service.authenticate(email, password) # (user, None) or (None, reason)
mint returns the plaintext once. Nothing else in the product can ever
produce it again.
Guarding a route
from splent_io.splent_feature_apikeys.decorators import require_token
@some_bp.route("/api/v1/spls", methods=["POST"])
@require_token("write_spl")
def publish():
g.api_user # the owner
g.api_token # the presenting token
require_token() with no scope accepts any usable token. For routes where a
token is one of several ways in, call resolve_token(scope) directly: it
returns (api_token, None) or (None, (response, status)).
Endpoints
| Method | Path | Auth |
|---|---|---|
| POST | /api/v1/auth/tokens |
email + password |
| GET | /api/v1/auth/whoami |
Bearer token |
| DELETE | /api/v1/auth/tokens/current |
Bearer token |
curl -X POST https://<product>/api/v1/auth/tokens \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com", "password": "...", "name": "laptop"}'
curl https://<product>/api/v1/auth/whoami \
-H "Authorization: Bearer splent_..."
Omitting ttl_days gives the product default; sending it as null gives a
token that never expires.
Failed credential attempts are throttled per caller and account. Users are
created inactive and an administrator activates them, so an account still
waiting is answered with 403 and a message that says exactly that, never
with "wrong password".
Pages
/developer/tokens— the owner's tokens, by prefix, with a revoke button./developer/tokens/new— create one; the plaintext is shown once.
A link appears in the authenticated sidebar
(layout.authenticated_sidebar hook).
Configuration
| Variable | Default | Meaning |
|---|---|---|
APIKEYS_DEFAULT_TTL_DAYS |
90 | Life of a token when none is asked |
APIKEYS_MAX_LOGIN_ATTEMPTS |
5 | Failed attempts per window |
APIKEYS_LOGIN_WINDOW_SECONDS |
300 | Length of that window |
All three have working defaults, so the feature needs no .env entry.
Install
splent feature:install splent-io/splent_feature_apikeys
splent db:migrate splent_feature_apikeys
splent db:upgrade
Tests
splent feature:test splent_feature_apikeys