Skip to content

Using with Template Engines (Jinja2)#

AuthX works seamlessly with server-side rendered (SSR) applications using template engines like Jinja2. This guide covers the key differences from API authentication and provides a complete example.

Key Differences from API Authentication#

Aspect API (JSON) Templates (SSR)
Token Location Headers Cookies
Response Type JSON HTML/Redirect
Error Handling HTTP status codes Redirect to login
CSRF Optional Required for forms

Quick Setup#

1. Configuration#

For template-based apps, use cookie authentication:

from authx import AuthX, AuthXConfig

config = AuthXConfig(
    JWT_SECRET_KEY="your-secret-key",
    # Cookie-based auth for SSR
    JWT_TOKEN_LOCATION=["cookies"],
    JWT_COOKIE_SECURE=False,      # True in production (HTTPS)
    JWT_COOKIE_HTTP_ONLY=True,    # Prevent JS access to token
    # CSRF protection for forms
    JWT_COOKIE_CSRF_PROTECT=True,
    JWT_CSRF_CHECK_FORM=True,     # Check CSRF in form data
    JWT_ACCESS_CSRF_FIELD_NAME="csrf_token",
)

auth = AuthX(config=config)

2. Setup Jinja2 Templates#

from fastapi import FastAPI
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

Authentication Flow#

Login Page (GET)#

Render the login form:

@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, error: str = None):
    return templates.TemplateResponse(
        "login.html",
        {"request": request, "error": error}
    )

Login Submit (POST)#

Handle form submission, set cookies, redirect:

from fastapi import Form
from fastapi.responses import RedirectResponse

@app.post("/login")
async def login_submit(
    response: Response,
    username: str = Form(...),
    password: str = Form(...),
):
    # Validate credentials
    if not validate_user(username, password):
        return templates.TemplateResponse(
            "login.html",
            {"request": request, "error": "Invalid credentials"},
            status_code=401
        )

    # Create token and set cookie
    access_token = auth.create_access_token(uid=username)

    redirect = RedirectResponse(url="/dashboard", status_code=303)
    auth.set_access_cookies(access_token, redirect)

    return redirect

Logout#

Clear cookies and redirect:

@app.post("/logout")
async def logout():
    response = RedirectResponse(url="/login", status_code=303)
    auth.unset_cookies(response)
    return response

Protected Routes#

Redirect-Based Auth Dependency#

Create a dependency that redirects to login instead of returning 401:

from authx.exceptions import AuthXException

class RedirectToLogin(Exception):
    def __init__(self, url: str = "/login"):
        self.url = url

@app.exception_handler(RedirectToLogin)
async def redirect_handler(request: Request, exc: RedirectToLogin):
    return RedirectResponse(url=exc.url, status_code=303)

async def require_auth(request: Request) -> dict:
    """Dependency that redirects to login if not authenticated."""
    try:
        token = await auth.get_access_token_from_request(request)
        payload = auth.verify_token(token, verify_csrf=False)
        return {"username": payload.sub}
    except AuthXException:
        # Redirect to login with return URL
        raise RedirectToLogin(f"/login?next={request.url.path}")

Protected Template Route#

from fastapi import Depends

@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(
    request: Request,
    user: dict = Depends(require_auth)
):
    csrf_token = request.cookies.get("csrf_access_token", "")

    return templates.TemplateResponse(
        "dashboard.html",
        {
            "request": request,
            "user": user,
            "csrf_token": csrf_token,  # Pass to template for forms
        }
    )

CSRF Protection in Forms#

When using cookie-based auth, CSRF protection is essential for form submissions.

Include CSRF Token in Templates#

<form action="/submit" method="post">
    <!-- Hidden CSRF token field -->
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">

    <input type="text" name="data">
    <button type="submit">Submit</button>
</form>

Handling Form Submissions#

@app.post("/submit")
async def submit_form(
    request: Request,
    user: dict = Depends(require_auth),
    data: str = Form(...),
    csrf_token: str = Form(...),  # CSRF validated automatically
):
    # Process form data
    return RedirectResponse(url="/dashboard", status_code=303)

JavaScript AJAX Requests#

For JavaScript requests, read the CSRF token from the cookie:

function getCsrfToken() {
    return document.cookie
        .split('; ')
        .find(row => row.startsWith('csrf_access_token='))
        ?.split('=')[1];
}

fetch('/api/action', {
    method: 'POST',
    credentials: 'include',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': getCsrfToken()
    },
    body: JSON.stringify({ data: 'value' })
});

Complete Example#

Here's a minimal complete example:

from fastapi import Depends, FastAPI, Form, Request, Response
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates

from authx import AuthX, AuthXConfig
from authx.exceptions import AuthXException

app = FastAPI()
templates = Jinja2Templates(directory="templates")

config = AuthXConfig(
    JWT_SECRET_KEY="secret",
    JWT_TOKEN_LOCATION=["cookies"],
    JWT_COOKIE_CSRF_PROTECT=True,
    JWT_CSRF_CHECK_FORM=True,
)
auth = AuthX(config=config)


# Simple user store
USERS = {"admin": "password123"}


class RedirectToLogin(Exception):
    pass


@app.exception_handler(RedirectToLogin)
async def handle_redirect(request, exc):
    return RedirectResponse("/login", status_code=303)


async def require_auth(request: Request):
    try:
        token = await auth.get_access_token_from_request(request)
        return auth.verify_token(token, verify_csrf=False)
    except AuthXException:
        raise RedirectToLogin()


@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
    return templates.TemplateResponse("login.html", {"request": request})


@app.post("/login")
async def login(username: str = Form(...), password: str = Form(...)):
    if USERS.get(username) != password:
        return RedirectResponse("/login?error=1", status_code=303)

    token = auth.create_access_token(uid=username)
    response = RedirectResponse("/dashboard", status_code=303)
    auth.set_access_cookies(token, response)
    return response


@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request, user=Depends(require_auth)):
    csrf = request.cookies.get("csrf_access_token", "")
    return templates.TemplateResponse(
        "dashboard.html",
        {"request": request, "user": user.sub, "csrf_token": csrf}
    )


@app.post("/logout")
async def logout():
    response = RedirectResponse("/login", status_code=303)
    auth.unset_cookies(response)
    return response

Template Examples#

login.html#

<!DOCTYPE html>
<html>
<head><title>Login</title></head>
<body>
    <h1>Login</h1>
    <form method="post" action="/login">
        <input type="text" name="username" placeholder="Username" required>
        <input type="password" name="password" placeholder="Password" required>
        <button type="submit">Login</button>
    </form>
</body>
</html>

dashboard.html#

<!DOCTYPE html>
<html>
<head><title>Dashboard</title></head>
<body>
    <h1>Welcome, {{ user }}!</h1>

    <form method="post" action="/logout">
        <button type="submit">Logout</button>
    </form>

    <!-- Example form with CSRF -->
    <form method="post" action="/action">
        <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
        <button type="submit">Do Action</button>
    </form>
</body>
</html>

Full Working Example#

For a complete working example with styled templates, see:

Run the example:

cd examples/examples
python jinja2_templates.py

Then visit http://localhost:8000 and login with admin / admin123.