enlace_auth.auth.middleware#

Platform auth middleware (pure ASGI).

Runs before any mounted sub-app. Responsibilities:

  1. Normalize the request path and reject known traversal bypasses.

  2. Strip client-provided identity headers so apps can’t be fooled by spoofed X-User-ID / X-Forwarded-User / similar.

  3. Resolve the access level for the request by longest-prefix match on the route prefix of each mounted app. Deny-by-default: an unmatched path is treated as protected:user.

  4. For public / local, pass through with user_id=None.

  5. For protected:shared, look for a per-app signed cookie; on failure return 401 (JSON) or redirect browsers to login_redirect_path.

  6. For protected:user, look for the platform session cookie, load the session from SessionStore, set user_id / user_email; on failure return 401 (JSON) or redirect browsers to login_redirect_path.

Browser vs. API deny behavior: GET/HEAD navigations that look like a browser (Accept header contains text/html) get a 303 redirect to the login page with ?login_required=1&next=<original-path>, so the user lands somewhere meaningful instead of seeing a raw JSON error. Non-HTML requests (XHR, fetch with Accept: application/json, curl, etc.) keep getting the machine-readable 401.

Design notes:

  • Pure ASGI three-callable pattern. Never BaseHTTPMiddleware (see CLAUDE.md).

  • Exempts /auth/* from auth checks so login/register pages stay reachable.

Classes

AccessRule(prefix, level, app_id[, ...])

Auth policy for a single mount prefix.

CSRFMiddleware(app, *, signing_key[, ...])

Signed double-submit CSRF for state-changing requests.

PlatformAuthMiddleware(app, *, access_rules, ...)

Pure-ASGI auth middleware.

class enlace_auth.auth.middleware.AccessRule(prefix, level, app_id, shared_password_hash=None, allowed_users=(), deny_signed_in=True)[source]#

Bases: object

Auth policy for a single mount prefix.

class enlace_auth.auth.middleware.CSRFMiddleware(app, *, signing_key, cookie_name='enlace_csrf', header_name='X-CSRF-Token', exempt_prefixes=('/auth/callback', '/auth/login/', '/api/'), enforce_prefixes=(), mint_prefixes=None)[source]#

Bases: object

Signed double-submit CSRF for state-changing requests.

On safe-method requests, sets an enlace_csrf cookie if one isn’t present. On state-changing requests, requires the cookie and an X-CSRF-Token header to match after signature verification.

mint_prefixes narrows where the cookie is set (not where the check runs): a request outside every prefix never receives one. The default, None, mints on every safe request. The platform plugin passes the places a state-changing request can follow, so a public app’s pages set no cookie (i2mint/enlace_auth#31).

Exempt paths skip the check entirely. Defaults exempt sub-app APIs under /api/ because the enlace_session cookie is SameSite=Lax: a cross-site POST from an attacker site arrives without credentials and is rejected by PlatformAuthMiddleware regardless of CSRF. This keeps pre-enlace apps working out of the box without each having to implement the /auth/csrf double-submit flow. The auth endpoints themselves (/auth/login, /auth/register, /auth/logout) stay protected.

class enlace_auth.auth.middleware.PlatformAuthMiddleware(app, *, access_rules, session_store, signing_key, cookie_name='enlace_session', max_age=86400, auth_path_prefix='/auth', login_redirect_path='/', dynamic_allowed_users=None)[source]#

Bases: object

Pure-ASGI auth middleware. See module docstring for behavior.