Requiring Gafaelfawr authentication#

Safir provides two FastAPI dependencies intended for applications designed to run behind a Kubernetes ingress configured to authenticate requests with Gafaelfawr.

Getting the authenticated user#

Gafaelfawr sets the X-Auth-Request-User HTTP header to the username of the user authenticated with a Gafaelfawr token. To get that username, use the auth_dependency FastAPI dependency, as follows:

from typing import Annotated

from fastapi import Depends

from safir.dependencies.gafaelfawr import auth_dependency


@app.get("/route")
async def get_rounte(
    user: Annotated[str, Depends(auth_dependency)],
) -> Dict[str, str]:
    # Route implementation using user.
    return {"some": "data"}

If the request does not have the Gafaelfawr header set, it will be rejected by FastAPI with a 422 status code.

To safely use this dependency, the application must be configured so that all requests will go through an ingress configured to use Gafaelfawr. If this is not the case (if, for example, the application is directly exposed to the Internet), the person sending the request could include the header that would be set by Gafaelfawr and assert any identity that they chose, which is obviously insecure.

Including the authenticated user in logging#

To include the authenticated user in log messages from a handler, use the auth_logger_dependency dependency instead of the logger_dependency. It works the same way, but additionally binds the user context variable to the authenticated user, obtained via auth_dependency.

For more details, see Logging in request handlers.

Testing applications using this dependency#

When testing an application that uses this dependency, any web requests to the authenticated routes of the application must include the X-Auth-Request-User header, or the test call will be rejected. For example (assuming use of httpx.AsyncClient for testing):

r = await client.get("/", headers={"X-Auth-Request-User": "someuser"})

The value of the header should be the username of the user the test is simulating. Optionally, if a test will be simulating many requests from the same authenticated user, this header can be added as a default header sent by the client. This is most easily done by defining a fixture, as follows. (This fixture assumes the FastAPI application under test is available via another fixture named app.)

from collections.abc import AsyncIterator

import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient


@pytest_asyncio.fixture
async def client(app: FastAPI) -> AsyncIterator[AsyncClient]:
    async with AsyncClient(
        transport=ASGITransport(app=app),  # type: ignore[arg-type]
        base_url="https://example.com/",
        headers={"X-Auth-Request-User": "user"},
    ) as client:
        yield client

Tests can then use client as a fixture and don’t have to provide the X-Auth-Request-User header with every call. Individual calls that need to use a different X-Auth-Request-User header set in that call, and that will override the default set in the AsyncClient object.