Skip to content

Authentication & Authorization

FastPluggy ships a minimal auth layer built on Starlette's AuthenticationMiddleware. Auth backends are provided by plugins (e.g. auth_user) and auto-wired at startup.

How it works

  1. No auth plugin installed → all routes are open, request.state.current_user is FPAnonymousUser
  2. Auth plugin installed (e.g. auth_user) → the plugin calls fast_pluggy.set_auth_manager(backend) during on_load_complete, which sets the backend that the always-installed AuthenticationMiddleware delegates to (populating request.user) and flips the auth_enable template global. It does NOT gate anything.

Installing an auth plugin gates NOTHING by itself. AuthenticationMiddleware identifies the caller; it never rejects one. Every protected surface must opt in with Depends(require_authentication) / require_role(...) — per route or per include_router. An app that loads auth_user and adds no dependency is fully open to anonymous users.

Verifying a gate — test it, don't inspect it

Since FastAPI >= 0.138 include_router(..., dependencies=[...]) no longer bakes the dependencies into the route objects; it wraps them in an _IncludedRouter and resolves them at request time. Walking router.routes / route.dependencies therefore shows the original dependency-free routes and cannot tell you what is gated — an inspection-based test reports "0 routes gated" against a gate that demonstrably works, and would just as happily mis-report a broken one. Assert behaviourally instead:

r = TestClient(app).get("/plugin/thing", follow_redirects=False)
assert r.status_code in (401, 403, 307)   # anonymous is refused

You can also pass an auth manager explicitly:

fast_pluggy = FastPluggy(app, auth_manager=my_custom_backend)

If set explicitly, plugins will not override it.

set_auth_manager(backend)

Sets the auth backend at runtime. Called automatically by auth plugins, but can also be used programmatically:

fast_pluggy.set_auth_manager(my_backend)

This: - Sets self.auth_manager - Updates the auth_enable template global

It adds no middleware. AuthenticationMiddleware is installed once, unconditionally, in FastPluggy.__init__, with a DelegatingAuthBackend that re-reads fastpluggy.auth_manager on every request — so set_auth_manager is a plain assignment.

That is load-bearing, not incidental. Starlette freezes app.add_middleware() after the first ASGI call, so the older "add the middleware once the backend arrives" shape could only ever work for a plugin that loaded before the app started serving. Because the middleware is now a fixed point and only the attribute it reads changes, set_auth_manager is safe to call at any time, more than once, and from a degraded-mode self-heal long after the app has been answering requests (fast_pluggy#22).

Dependencies

from fastpluggy.core.auth import require_authentication, require_role

require_authentication

Rejects unauthenticated requests (401) or redirects to login if the auth backend defines on_authenticate_error. When no auth manager is set, this is a no-op — all requests pass through.

# Protect an entire router
app.include_router(my_router, dependencies=[Depends(require_authentication)])

# Or a single route
@router.get("/secret", dependencies=[Depends(require_authentication)])
async def secret(request: Request): ...

require_role(role: str)

Requires the user to have a specific role (checked against request.auth.scopes). FastPluggy uses "fp_admin" to protect its own admin routes. No-op when auth manager is not set.

app.include_router(admin_router, dependencies=[Depends(require_role("fp_admin"))])

Current user

Set by CurrentUserMiddleware on every request:

user = request.state.current_user   # FPAnonymousUser if not authenticated
if user.is_authenticated:
    ...
if user.is_admin:
    ...

Available in templates as {{ request.state.current_user }}.

FPAnonymousUser extends Starlette's UnauthenticatedUser and is used as the fallback when no user is authenticated. It guarantees request.state.current_user is never None.

The user object shape depends on the auth backend. FastPluggy core expects:

Attribute Type Description
is_authenticated bool False for anonymous, True for logged-in users
display_name str Shown in the topbar user menu
is_admin bool Controls visibility of the admin sidebar section
profile_picture str\|None Optional; used for avatar display

Writing a custom auth backend

Implement AuthInterface (extends Starlette's AuthenticationBackend):

from fastpluggy.core.auth.auth_interface import AuthInterface

class MyAuthManager(AuthInterface):
    @property
    def user_model(self):
        return MyUserModel

    async def authenticate(self, conn):
        # Return (AuthCredentials, user) or None
        ...

    async def on_authenticate_error(self, request):
        # Return a Response (e.g. redirect) or raise HTTPException
        ...

Then either pass it to FastPluggy(app, auth_manager=MyAuthManager()) or call fast_pluggy.set_auth_manager(MyAuthManager()) from a plugin's on_load_complete.