API Tokens
API tokens answer “which user is calling this API?” without standing up a
full OAuth2 server. Almasix ships a Signet-class package
(almasix.signet, optional extras almasix[tokens] / almasix[signet]) for:
- Personal access tokens (PATs) — hashed tokens owned by a user, sent as
Authorization: Bearer …(mobile apps, CLIs, first-party API clients). - SPA cookie authentication — first-party single-page apps keep using the session cookie + CSRF; Signet only accepts that cookie from configured stateful domains.
They are always tied to a user account. Application / client-credentials API keys that authenticate a client with no user are a different product and are not part of this package (see Out of scope).
Installation
Section titled “Installation”The package is in the framework today. Publish the config and migration when you want them in your app tree:
pip install 'almasix[tokens]' # or almasix[signet] — empty extras that mark the featuresmith vendor:publish --tag=signet-configsmith vendor:publish --tag=signet-migrationssmith migrateAdd the mixin to your authenticatable model:
from almasix.auth import AuthenticatableMixinfrom almasix.orm import Modelfrom almasix.signet import HasApiTokens
class User(HasApiTokens, AuthenticatableMixin, Model): ...Register a Signet guard in config/auth.py:
"guards": { "web": {"driver": "session", "provider": "users"}, "signet": {"driver": "signet", "provider": "users"},}Protect routes with auth:signet — the guard tries the session user for
first-party SPA requests, then falls back to a Bearer PAT.
Issuing tokens
Section titled “Issuing tokens”issued = await user.create_token("Nuno's iPhone", ["server:update"])print(issued.plain_text_token) # "12|a3f2…" — show once, store hashedThe plain-text value is {id}|{secret}. Only the SHA-256 hash is stored.
Abilities default to ["*"] (full access for that user).
Mobile apps typically POST credentials to an endpoint that returns a token:
Route.post("/signet/token", [TokenController, "issue"])Store the returned token in the platform keychain and send it on every request.
Abilities
Section titled “Abilities”issued = await user.create_token("deploy", ["server:update", "server:read"])
# On an authenticated request:if user.token_can("server:update"): ...if user.token_cant("server:delete"): ...Middleware aliases (register in bootstrap/app.py, or call
middleware.stateful_api() which also registers them):
middleware.alias({ "abilities": CheckAbilities, # all listed abilities required "ability": CheckForAnyAbility, # at least one})
Route.get("/orders", ...).middleware(["auth:signet", "abilities:check-status,place-orders"])For first-party SPA session requests (no PAT), token_can returns True so
policies can stay uniform — authorization still belongs in Gates / Policies.
Protecting routes
Section titled “Protecting routes”Route.get("/user", [UserController, "show"], middleware=["auth:signet"])auth:signet authenticates either:
- a session cookie from a stateful domain, or
- a valid Bearer personal access token.
Revoking and expiration
Section titled “Revoking and expiration”await user.tokens_delete() # all tokensawait user.current_access_token().delete() # this request's tokenawait user.tokens().where("id", "=", token_id).first() # then .delete()Configure minutes until expiry in config/signet.py (expiration), or pass
expires_at= to create_token. Prune with:
smith signet:prune-expired --hours=24SPA authentication
Section titled “SPA authentication”- List first-party domains in
config/signet.py→stateful. - Call
middleware.stateful_api()inbootstrap/app.pyso theapigroup gets session + CSRF for those domains. - Before login,
GET /signet/csrf-cookie(setsXSRF-TOKEN). POST /loginwith the session guard as usual.- Subsequent API calls send cookies +
X-XSRF-TOKEN.
CORS must allow credentials; the session cookie domain should cover your SPA subdomain.
Testing
Section titled “Testing”from almasix.signet import Signet
Signet.acting_as(user, ["profile:read"])# next HTTP request through the test client is authenticated as userOr issue a real token and with_token(plain_text_token).
Configuration
Section titled “Configuration”| Key | Meaning |
|---|---|
signet.stateful |
Domains allowed to use cookie session auth against the API |
signet.guard |
Session guards consulted before Bearer lookup |
signet.expiration |
Default PAT lifetime in minutes (None = never) |
Signet.use_personal_access_token_model(...) |
Swap the token model |
Helpers: Signet.current_application_url_with_port(),
Signet.current_request_host() for the stateful list.
Out of scope
Section titled “Out of scope”| Need | Use instead |
|---|---|
| Client / application API keys (no user) | Separate client-credentials surface (not Signet) |
| Third-party OAuth2 server | Passport-class package (deferred) |
| Social login (Google, GitHub, …) | Socialite-class providers (deferred) |
Classic driver: "token" looking up users.api_token remains available for
simple demos; prefer Signet PATs for production APIs.