Authentication¶
Without authorization callbacks the admin is open: /admin, ra.json and the whole API are
served to anyone. Plugin.check_auth turns authorization on for all of them.
from muffin_admin import Plugin
admin = Plugin(app)
@admin.check_auth
async def auth(request):
"""Return the current user, or raise when there is none."""
token = request.headers.get("authorization")
user = await User.select().where(User.email == token).first()
if not user:
raise APIError.FORBIDDEN()
return user
check_auth stores the callback on the Muffin-REST API (api.authorize) and sets
auth["required"] = True in ra.json. Every resource request calls it first; Muffin-REST
answers 403 when the callback raises. The value it returns becomes self.auth in the
handler, which is how prepare_collection and other hooks can scope data to the user.
The callback also receives file-download requests; see File downloads.
Identity¶
@admin.get_identity returns the user object react-admin shows in the top bar. It is called
with the original request after login:
@admin.get_identity
async def ident(request):
"""Get current user information."""
user = await auth(request)
if user:
return {"id": user.pk, "fullName": user.name, "avatar": user.picture}
id, fullName and avatar are the fields react-admin reads. Without get_identity the
admin never calls /admin/ident and only keeps the login token.
Login and logout¶
@admin.login implements the login page. It receives the form data and must return the value
that is stored as the auth token:
from muffin import ResponseJSON
from muffin_rest import APIError
@admin.login
async def login(request):
"""Login a user."""
data = await request.data()
user = await User.select().where(
User.email == data["username"], User.password == data["password"]
).first()
if not user:
raise APIError.UNAUTHORIZED()
return ResponseJSON(user.email)
Return a string token. An object is stringified when stored, so a scalar (email, session id,
JWT) is what the frontend sends back as the credential. To reject the credentials, raise an HTTP
error (APIError.UNAUTHORIZED, ResponseError.FORBIDDEN, ...): the admin keeps whatever the
endpoint returns, so returning null with status 200 stores the string "null" instead of
failing the login.
The login and identity routes answer 404 until the decorators are applied.
There is no server-side logout route. On logout the frontend clears the stored value and, when
logout_url is configured, navigates to it:
admin = Plugin(app, logout_url="/logout")
Storage¶
auth_storage chooses where the browser keeps the token:
| Value | Stored in | Sent as |
|---|---|---|
localstorage (default) |
localStorage[auth_storage_name] |
Authorization header on API and file requests |
cookies |
cookie auth_storage_name via js-cookie |
cookie on every request |
In cookies mode the server decides what to do with the cookie, because check_auth is the
only code that validates it:
admin = Plugin(app, auth_storage="cookies", auth_storage_name="shop_auth")
@admin.check_auth
async def auth(request):
token = request.cookies.get("shop_auth")
...
In cookies mode the plugin also guards the HTML page and ra.json: when check_auth
returns a falsy value, both redirect to login_url (or / when it is unset). Raising an
APIError still produces that error instead of a redirect. In localstorage mode the page is
served before the token is checked, and the frontend redirects itself to the login screen.
The cookie is a plain cookie: no signature, no encryption, no HttpOnly. admin.secret does
not sign it; the option is unused. Use HTTPS, keep the cookie short-lived, and validate it
server-side. See secret.
File downloads¶
A file action downloads the response with fetch, using the same Authorization header as
normal API calls. The current list filters are sent as the where query parameter:
/admin/api/message/data.csv?f&where=...
check_auth reads the token from the header, exactly like other requests:
token = request.headers.get("authorization")
With auth_storage="cookies" the cookie is sent automatically.
Roles¶
@admin.check_auth is the only server-side check. The frontend exposes a getPermissions
identity role read from ${auth_storage_name}_role in the storage, but the bundled resources
do not use it: there are no per-resource permissions. Hide operations with create, edit,
delete and show in Meta, and scope data in handler hooks.
Putting it together¶
from muffin import ResponseJSON
from muffin_rest import APIError
from muffin_admin import Plugin, PWAdminHandler
admin = Plugin(app)
@admin.check_auth
async def auth(request):
token = request.headers.get("authorization")
user = await User.select().where(User.email == token).first()
if not user:
raise APIError.FORBIDDEN()
return user
@admin.get_identity
async def ident(request):
user = await auth(request)
return {"id": user.email, "fullName": user.email}
@admin.login
async def login(request):
data = await request.data()
user = await User.select().where(
User.email == data["username"], User.password == data["password"]
).first()
if not user:
raise APIError.UNAUTHORIZED()
return ResponseJSON(user.email)
The complete version with a Peewee model lives in example/peewee_orm/admin.py.