Authentication
Status: 🔴 DA REVISIONARE
What it does
Configures how callers prove who they are and controls which routes they may
reach. genro-asgi accepts basic, bearer and JWT credentials plus API keys, and
filters routes by the tags carried on the caller’s Avatar.
When to use it
When some routes must be restricted to authenticated callers, or to callers with
a particular role. Pass an auth config to AsgiServer and mark the protected
routes with auth_rule.
Setup
Auth is a mixin capability of AsgiServer. You arm it by passing the auth
keyword argument; doing so wires the auth middleware automatically.
from genro_asgi import AsgiServer, RoutedApplication
from genro_routes import route
AUTH = {
"basic": {"alice": {"password": "wonderland", "tags": "admin,ops"}},
"bearer": {"svc": {"token": "sk_live_xyz", "tags": "api"}},
"jwt": [{"secret": "topsecret", "algorithm": "HS256"}],
}
class App(RoutedApplication):
@route()
def public(self) -> dict:
return {"open": True}
@route(auth_rule="admin")
def secret(self) -> dict:
return {"classified": True}
server = AsgiServer(primary=App(), auth=AUTH)
server.serve(host="127.0.0.1", port=8000)
Notes on the config shape:
basicandbearerare dictionaries keyed by identity. A basic entry carries apassword; a bearer entry carries atoken. Both carrytags(a comma-separated string or a list) that become the avatar’s roles.jwtis a list of verifier configurations, each with asecretand analgorithm— you can accept tokens from more than one issuer.API keys with the
gak_...prefix are handled by anApiKeyStore. Passtokens=<ApiKeyStore | dict>alongsideauth. Users can likewise come from aUserStoreviausers=<UserStore | dict>, and anadmin_password="..."bootstraps a SUPERADMIN identity.
Minimal snippet
Protecting a single route:
@route(auth_rule="admin")
def secret(self) -> dict:
return {"classified": True}
auth_rule="admin" means the caller’s avatar must carry the admin tag.
Protection is default-deny: this route answers 403 even if no auth is
configured at all, so a protected endpoint is never accidentally open.
The Avatar
An authenticated caller is represented by an Avatar:
from genro_asgi import Avatar
avatar = Avatar("alice", tags="admin,ops") # tags: comma string or list
The avatar’s tags are what auth_rule matches against. The same Avatar type is
used whether the identity came from a header credential or from a session (see
the sessions guide).
Header vs session precedence
The
Authorizationheader wins — genro-asgi is API-first. If a request carries a valid credential in the header, that identity is used.An invalid header credential produces
401with no fallback to the session — a broken token is an error, not an invitation to try the cookie.With
auth=None, no header backend is configured, but the server still resolves the identity carried by the session.
Server-side login flow
The always-mounted _server app exposes a login flow for session-based clients:
POST /_server/loginwith body{"identity", "password"}→200with{identity, tags, session_id}on success.GET /_server/login_page— an HTML login page.GET /_server/login_methods— a public JSON descriptor of available methods.POST /_server/logout.
Login lockout with backoff is configurable via server_app={"login": {...}}.
OIDC
An OIDC provider is configured under server_app, and the server must know its
own public base address — external_url:
PROVIDER = {
"issuer": "https://accounts.example.com",
"client_id": "client-123",
"scopes": "openid email profile",
"identity_claim": "email",
"tags": [],
}
server = AsgiServer(
primary=App(),
external_url="https://shop.example.com",
server_app={"oidc": {"google": PROVIDER}},
)
external_url is what the server calls itself when it hands its own URL to
the provider: the redirect_uri the browser is sent back to must be absolute,
and must match the one you registered for the client in the provider’s console —
here https://shop.example.com/_server/auth/oidc:google/callback. It is
distinct from the host/port the server binds to, which differ behind a proxy
and mean nothing to an outside caller. Configuring a provider without
external_url is a boot error: the server refuses to start rather than fail at
the first login attempt with a provider-side error.
In a config recipe it lives on the server section:
root.server(host="127.0.0.1", port=8000, external_url="https://shop.example.com")
GET /_server/auth/oidc:google/start?next=...→302(PKCE S256).GET /_server/auth/oidc:google/callback?...→ token exchange, avatar attach, redirect.The public descriptor never exposes the
client_secretor theissuer.
How to verify it
$ curl -i http://127.0.0.1:8000/secret
HTTP/1.1 403 Forbidden
$ curl -u alice:wonderland http://127.0.0.1:8000/secret
{"classified": true}
$ curl -H "Authorization: Bearer sk_live_xyz" http://127.0.0.1:8000/public
{"open": true}
Gotchas
auth_ruleis default-deny: a protected route is403even with no auth configured. If a route unexpectedly returns403, check whether it carries anauth_rulethe caller’s avatar does not satisfy.jwtis a list, not a dict — a single verifier still goes inside a one-element list.An invalid
Authorizationheader is401and does not fall back to the session cookie.An OIDC provider needs
external_url, and the resulting callback URL must be registered verbatim with the provider — a mismatch is refused by the provider, not by us. A missingexternal_urlstops the server at boot.The auth symbols (
AuthMixin,PasswordMethod,OidcMethod,ApiKeyStore,FileApiKeyStore,UserStore,FileUserStore,AuthCore,AuthMethod) are importable fromgenro_asgi.