Add authentication¶
In this step you add a login endpoint that returns a JWT token, and require the token for creating, updating and deleting articles.
Create the keys¶
Generate an RSA key pair. Keep the private key secret.
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in private.pem -pubout -out public.pem
Configure the settings¶
from pathlib import Path
from joserfc import jwk
JWT_PRIVATE_KEY = jwk.RSAKey.import_key(Path("private.pem").read_text())
JWT_PUBLIC_KEY = jwk.RSAKey.import_key(Path("public.pem").read_text())
JWT_ISSUER = "blog-api"
JWT_AUDIENCE = "blog-clients"
In production, load the keys from environment variables or a secrets manager instead of files in the project.
Write the auth class¶
auth_handler runs after the token is verified. It returns the user, which
you then find in request.auth.
from django.conf import settings
from django.contrib.auth import get_user_model
from ninja_aio.auth import AsyncJwtBearer
User = get_user_model()
class JWTAuth(AsyncJwtBearer):
jwt_public = settings.JWT_PUBLIC_KEY
claims = {
"iss": {"essential": True, "value": settings.JWT_ISSUER},
"aud": {"essential": True, "value": settings.JWT_AUDIENCE},
"sub": {"essential": True},
}
async def auth_handler(self, request):
user_id = self.dcd.claims["sub"]
return await User.objects.filter(pk=user_id, is_active=True).afirst()
JWTAuth works for both sync and async viewsets.
Add a login endpoint¶
encode_jwt() signs the token with JWT_PRIVATE_KEY and adds the issuer and
audience from your settings.
from django.contrib.auth import authenticate
from ninja import Schema
from ninja.errors import AuthenticationError
from ninja_aio.auth import encode_jwt
class LoginIn(Schema):
username: str
password: str
class TokenOut(Schema):
access_token: str
@api.post("/auth/login", response=TokenOut, auth=None)
def login(request, data: LoginIn):
user = authenticate(request, username=data.username, password=data.password)
if user is None:
raise AuthenticationError()
token = encode_jwt({"sub": str(user.pk)}, duration=3600)
return {"access_token": token}
from django.contrib.auth import aauthenticate
from ninja import Schema
from ninja.errors import AuthenticationError
from ninja_aio.auth import encode_jwt
class LoginIn(Schema):
username: str
password: str
class TokenOut(Schema):
access_token: str
@api.post("/auth/login", response=TokenOut, auth=None)
async def login(request, data: LoginIn):
user = await aauthenticate(request, username=data.username, password=data.password)
if user is None:
raise AuthenticationError()
token = encode_jwt({"sub": str(user.pk)}, duration=3600)
return {"access_token": token}
Protect the viewset¶
auth applies to every endpoint. get_auth = None keeps reading public.
from .auth import JWTAuth
@api.viewset(model=Article)
class ArticleViewSet(APIViewSet):
auth = [JWTAuth()]
get_auth = None
You can also set post_auth, patch_auth and delete_auth one by one.
Custom actions follow the setting for their HTTP method.
Try it¶
# Get a token
curl -X POST localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "secret"}'
# Use it
curl -X POST localhost:8000/api/articles/ \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"title": "Hello", "body": "...", "category_id": 1}'
Without a valid token, write requests return 401.
In the interactive docs, click Authorize and paste the token to try the protected endpoints.
Tip
For browser apps, you can store the token in an HttpOnly cookie instead. See cookie authentication.