Authentication
AdminAuth holds three hooks — login, authenticate, and logout — and AdminPanel wires them into the panel's routes for you.
from openadmin.fastapi import AdminAuth
auth = AdminAuth()No protection by default
An AdminAuth() instance's three hooks are no-op stubs until you override them: login and logout do nothing, and authenticate never raises — so it lets every request through. Passing auth=None to AdminPanel (the default) is equivalent: no /auth/* routes are even mounted, and /api/* is completely open. Either way, nothing is actually gated until you decorate all three hooks yourself.
The three hooks
@auth.login()
def login(req: Request, login_req: LoginReq) -> None: ...
@auth.authenticate()
def authenticate(req: Request) -> None: ...
@auth.logout()
def logout(req: Request) -> None: ...Each decorator just stores the function you give it — it doesn't wrap or alter it, so the function can still be called or tested directly like any other. Each hook may be sync or async (None | Awaitable[None]).
login_func(req, login_req)— receives aLoginReq({username: str, password: str}, a pydantic model). Raise anHTTPExceptionto reject the credentials; return normally to accept them. This is where you'd typically write something intoreq.session.authenticate_func(req)— runs as a dependency on every request under/api/*, i.e. every stat, table, form, action, chart, and markdown endpoint on every page. Raise anHTTPException(typically 401) to reject the request; return normally to allow it.logout_func(req)— typically clearsreq.session. It runs behindauthenticate_funcitself, so a caller must already be authenticated to log out.
How AdminPanel wires them up
Passing auth= to AdminPanel(...) does three things:
- Mounts
POST /auth/login, calling yourlogin_funcand returning204 No Contenton success. - Mounts
POST /auth/logout, calling yourlogout_func, itself gated behindauthenticate_func. - Adds
authenticate_funcas a router-level dependency on the entire/apirouter — so it runs before any component endpoint, panel-wide, with no per-page or per-component opt-in needed.
The frontend's static assets (served at /) and the login endpoint itself are intentionally not gated, since a client needs to load the login screen and call /auth/login before it has anything to authenticate with.
Example
# admin/auth.py
from fastapi import HTTPException, Request, status
from openadmin.fastapi import AdminAuth, LoginReq
auth = AdminAuth()
@auth.login()
def login(req: Request, login_req: LoginReq) -> None:
if login_req.username == "admin" and login_req.password == "admin":
req.session.update({"token": "admin-token"})
else:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED, "Invalid username or password"
)
@auth.authenticate()
def authenticate(req: Request) -> None:
if req.session.get("token") != "admin-token":
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Unauthorized")
@auth.logout()
def logout(req: Request) -> None:
req.session.clear()req.session comes from Starlette's SessionMiddleware, added on the outer application — not on admin.app — since middleware on the outer app also covers requests routed into the mounted sub-app:
# main.py
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
from openadmin.fastapi import AdminPanel
from .admin.auth import auth
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="change-me")
admin = AdminPanel("My Admin", auth=auth)
app.mount("/admin", admin.app)DANGER
Cookie-based sessions are only as secure as secret_key. Never hardcode it — load it from an environment variable or secret store — and compare credentials with a real user store and hashed passwords, not the plaintext check shown above.
See Implementing Auth for a full step-by-step recipe.