Starlette basic authentication
Written by Walter on 24/01/2021
The starlette documentation is pretty nice. But for authentication I was missing a complete working example to wire it all up. Luckily I've found some examples from other people using fast-api that helped in getting all the puzzle pieces together. For this personal webpage I'm using starlette directly and don't have the need for the extra fast-api layer. For now I'm just doing basic auth here but I will soon be changing this into cookie/session based authentication.
Here a verry minimal lib/authorization.py that works:
from starlette.authentication import (
AuthenticationBackend, AuthenticationError, SimpleUser,
AuthCredentials
)
import base64
import binascii
# you can also encrypt these, or read them from an env var or from a database
ADMIN_USER = "Your username here"
ADMIN_PASS = "your password here"
class BasicAuthBackend(AuthenticationBackend):
async def authenticate(self, request):
if "Authorization" not in request.headers:
return
auth = request.headers["Authorization"]
try:
scheme, credentials = auth.split()
if scheme.lower() != 'basic':
return
decoded = base64.b64decode(credentials).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error):
raise AuthenticationError('Invalid basic auth credentials')
username, _, password = decoded.partition(":")
if(username, password) != (ADMIN_USER, ADMIN_PASS):
return
return AuthCredentials(["authenticated"]), SimpleUser(username)
To make this work in your main application, your starlette application needs some extra imports and have an AuthenticationMiddleware injected:
from lib.authorization import BasicAuthBackend
from starlette.authentication import requires
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
middleware = [
Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
]
app = Starlette(middleware=middleware)
Login route that pops up a basic authentication dialog, when logged in it redirects to your wanted administration route:
@app.route("/login")
async def login(request):
if request.user.is_authenticated:
return RedirectResponse(url=f"/admin", status_code=303)
else:
response = Response(headers={"WWW-Authenticate": "Basic"}, status_code=401)
return response
Admin page that only shows if correctly logged in (also in views and other places you can see request.user.is_authenticated boolean flag is available to show/hide buttons and other admin operations) the @requires is a decorator that protects your route if you visit it without correct credentials:
@app.route("/admin") @requires("authenticated") # just shows 401 unauthorized response # @requires("authenticated", redirect="access_denied") # specify a redirect route if not logged in
async def admin(request): return JSONResponse( { "authenticated": request.user.is_authenticated, "user": request.user.display_name, } )
The downside of basic auth is that logging out is not really supported. You can however use an incognito window and close it to log out(that's my current hacky workaround ;) ). Or use the link chrome://restart also makes your browser restart and forget your credentials.
Honestly rails generators makes this kind of thing much easier. Just install gem devise and cancan + running some generators gets you up and running way quicker imho. Even another framework I'm contemplating is amber framework makes this part also much easier amber authorization. However doing it all more from scratch with starlette isn't too much work as it turns out. I will however be changing it into something with a cookie and a proper login/logout pages soon or maybe oauth and jwt but I have to see if that plays nicely with ckeditor and its routes. For now the current simple basic auth does the job and plays nicely with the wysiwig editor stuff.