Skip to content

FastAPIAuth

Authentication for FastAPI applications.

FastAPIAuth

Bases: BaseAuth

Authentication for FastAPI apps.

RECOMMENDED: Use protect_app() to protect your entire application with one line.

Example (RECOMMENDED - protect entire app): from fastapi import FastAPI, Depends from cognito_auth.fastapi import FastAPIAuth

app = FastAPI()
auth = FastAPIAuth()
auth.protect_app(app, bypass={"/health"})  # Protects entire app except /health!

@app.get("/")
def index(user: User = Depends(auth.get_auth_user)):
    return {"message": f"Welcome {user.email}!"}

@app.get("/health")
def health():
    return {"status": "ok"}

Example (Alternative - protect specific routes): from fastapi import FastAPI, Depends from cognito_auth.fastapi import FastAPIAuth

app = FastAPI()
auth = FastAPIAuth()

@app.get("/public")
def public():
    return {"message": "Public page"}

@app.get("/protected")
def protected(user: User = Depends(auth.get_auth_user)):
    return {"message": f"Welcome {user.email}!"}
Source code in src/cognito_auth/fastapi.py
class FastAPIAuth(BaseAuth):
    """
    Authentication for FastAPI apps.

    RECOMMENDED: Use protect_app() to protect your entire application with one line.

    Example (RECOMMENDED - protect entire app):
        from fastapi import FastAPI, Depends
        from cognito_auth.fastapi import FastAPIAuth

        app = FastAPI()
        auth = FastAPIAuth()
        auth.protect_app(app, bypass={"/health"})  # Protects entire app except /health!

        @app.get("/")
        def index(user: User = Depends(auth.get_auth_user)):
            return {"message": f"Welcome {user.email}!"}

        @app.get("/health")
        def health():
            return {"status": "ok"}

    Example (Alternative - protect specific routes):
        from fastapi import FastAPI, Depends
        from cognito_auth.fastapi import FastAPIAuth

        app = FastAPI()
        auth = FastAPIAuth()

        @app.get("/public")
        def public():
            return {"message": "Public page"}

        @app.get("/protected")
        def protected(user: User = Depends(auth.get_auth_user)):
            return {"message": f"Welcome {user.email}!"}
    """

    def protect_app(self, app: FastAPI, bypass: set[str] | None = None) -> None:
        """
        Protect the entire application with authentication.

        This is the RECOMMENDED approach. Call this once after creating your app,
        and all routes will require authentication. Use get_auth_user() dependency
        to access the authenticated user.

        Args:
            app: FastAPI application instance
            bypass: Optional set of paths that skip authentication
                (e.g. {"/health", "/ready"}). Trailing slashes are ignored
                when matching, so "/health" will also match "/health/".

        Example:
            app = FastAPI()
            auth = FastAPIAuth()
            auth.protect_app(app, bypass={"/health"})

            @app.get("/")
            def index(user: User = Depends(auth.get_auth_user)):
                return {"message": f"Welcome {user.email}!"}

            @app.get("/health")
            def health():
                return {"status": "ok"}
        """
        bypass_paths = {p.rstrip("/") or "/" for p in bypass} if bypass else set()

        class AuthMiddleware(BaseHTTPMiddleware):
            def __init__(self, app, auth_instance):
                super().__init__(app)
                self.auth = auth_instance

            async def dispatch(self, request: Request, call_next):
                """Validate authentication before every request."""
                path = request.url.path.rstrip("/") or "/"
                if path in bypass_paths:
                    return await call_next(request)

                try:
                    headers = dict(request.headers)
                    user = self.auth._get_user_from_headers(headers)

                    if not self.auth._is_authorised(user):
                        logger.warning(
                            "User not authorised, redirecting: email=%s, groups=%s",
                            user.email,
                            user.groups,
                        )
                        return RedirectResponse(url=self.auth.redirect_url)

                    # Store user in request state (FastAPI's equivalent of Flask's g)
                    request.state.user = user

                    return await call_next(request)

                except Exception:
                    logger.error(
                        "Authentication failed, redirecting to %s",
                        self.auth.redirect_url,
                        exc_info=True,
                    )
                    return RedirectResponse(url=self.auth.redirect_url)

        app.add_middleware(AuthMiddleware, auth_instance=self)

    def get_auth_user(self, request: Request) -> User:
        """
        Get the authenticated and authorised user for this request.

        This method is designed to be used with FastAPI's Depends() for
        dependency injection.

        When using protect_app() (RECOMMENDED), this retrieves the user that was
        validated by the middleware.

        When not using protect_app(), this validates the user on-demand.

        Args:
            request: FastAPI Request object (automatically injected by Depends)

        Returns:
            Authenticated and authorised User

        Raises:
            HTTPException: 401 if authentication fails, 403 if unauthorised

        Example:
            auth.protect_app(app)

            @app.get("/protected")
            def protected_route(user: User = Depends(auth.get_auth_user)):
                return {"email": user.email}
        """
        # If protect_app() was used, user is stored in request.state
        if hasattr(request.state, "user"):
            return request.state.user

        # Otherwise, validate on-demand (for route-specific protection)
        try:
            headers = dict(request.headers)
            user = self._get_user_from_headers(headers)

            if not self._is_authorised(user):
                logger.warning(
                    "User not authorised: email=%s, groups=%s",
                    user.email,
                    user.groups,
                )
                raise HTTPException(
                    status_code=403,
                    detail="Access denied. You don't have permission.",
                )

            return user

        except HTTPException:
            # Re-raise HTTPException as-is
            raise
        except Exception as e:
            logger.error("Authentication failed: %s", e, exc_info=True)
            raise HTTPException(
                status_code=401,
                detail="Authentication failed. Unable to verify your identity.",
            ) from e

Functions

protect_app(app, bypass=None)

Protect the entire application with authentication.

This is the RECOMMENDED approach. Call this once after creating your app, and all routes will require authentication. Use get_auth_user() dependency to access the authenticated user.

Parameters:

Name Type Description Default
app FastAPI

FastAPI application instance

required
bypass set[str] | None

Optional set of paths that skip authentication (e.g. {"/health", "/ready"}). Trailing slashes are ignored when matching, so "/health" will also match "/health/".

None
Example

app = FastAPI() auth = FastAPIAuth() auth.protect_app(app, bypass={"/health"})

@app.get("/") def index(user: User = Depends(auth.get_auth_user)): return {"message": f"Welcome {user.email}!"}

@app.get("/health") def health(): return {"status": "ok"}

Source code in src/cognito_auth/fastapi.py
def protect_app(self, app: FastAPI, bypass: set[str] | None = None) -> None:
    """
    Protect the entire application with authentication.

    This is the RECOMMENDED approach. Call this once after creating your app,
    and all routes will require authentication. Use get_auth_user() dependency
    to access the authenticated user.

    Args:
        app: FastAPI application instance
        bypass: Optional set of paths that skip authentication
            (e.g. {"/health", "/ready"}). Trailing slashes are ignored
            when matching, so "/health" will also match "/health/".

    Example:
        app = FastAPI()
        auth = FastAPIAuth()
        auth.protect_app(app, bypass={"/health"})

        @app.get("/")
        def index(user: User = Depends(auth.get_auth_user)):
            return {"message": f"Welcome {user.email}!"}

        @app.get("/health")
        def health():
            return {"status": "ok"}
    """
    bypass_paths = {p.rstrip("/") or "/" for p in bypass} if bypass else set()

    class AuthMiddleware(BaseHTTPMiddleware):
        def __init__(self, app, auth_instance):
            super().__init__(app)
            self.auth = auth_instance

        async def dispatch(self, request: Request, call_next):
            """Validate authentication before every request."""
            path = request.url.path.rstrip("/") or "/"
            if path in bypass_paths:
                return await call_next(request)

            try:
                headers = dict(request.headers)
                user = self.auth._get_user_from_headers(headers)

                if not self.auth._is_authorised(user):
                    logger.warning(
                        "User not authorised, redirecting: email=%s, groups=%s",
                        user.email,
                        user.groups,
                    )
                    return RedirectResponse(url=self.auth.redirect_url)

                # Store user in request state (FastAPI's equivalent of Flask's g)
                request.state.user = user

                return await call_next(request)

            except Exception:
                logger.error(
                    "Authentication failed, redirecting to %s",
                    self.auth.redirect_url,
                    exc_info=True,
                )
                return RedirectResponse(url=self.auth.redirect_url)

    app.add_middleware(AuthMiddleware, auth_instance=self)

get_auth_user(request)

Get the authenticated and authorised user for this request.

This method is designed to be used with FastAPI's Depends() for dependency injection.

When using protect_app() (RECOMMENDED), this retrieves the user that was validated by the middleware.

When not using protect_app(), this validates the user on-demand.

Parameters:

Name Type Description Default
request Request

FastAPI Request object (automatically injected by Depends)

required

Returns:

Type Description
User

Authenticated and authorised User

Raises:

Type Description
HTTPException

401 if authentication fails, 403 if unauthorised

Example

auth.protect_app(app)

@app.get("/protected") def protected_route(user: User = Depends(auth.get_auth_user)): return {"email": user.email}

Source code in src/cognito_auth/fastapi.py
def get_auth_user(self, request: Request) -> User:
    """
    Get the authenticated and authorised user for this request.

    This method is designed to be used with FastAPI's Depends() for
    dependency injection.

    When using protect_app() (RECOMMENDED), this retrieves the user that was
    validated by the middleware.

    When not using protect_app(), this validates the user on-demand.

    Args:
        request: FastAPI Request object (automatically injected by Depends)

    Returns:
        Authenticated and authorised User

    Raises:
        HTTPException: 401 if authentication fails, 403 if unauthorised

    Example:
        auth.protect_app(app)

        @app.get("/protected")
        def protected_route(user: User = Depends(auth.get_auth_user)):
            return {"email": user.email}
    """
    # If protect_app() was used, user is stored in request.state
    if hasattr(request.state, "user"):
        return request.state.user

    # Otherwise, validate on-demand (for route-specific protection)
    try:
        headers = dict(request.headers)
        user = self._get_user_from_headers(headers)

        if not self._is_authorised(user):
            logger.warning(
                "User not authorised: email=%s, groups=%s",
                user.email,
                user.groups,
            )
            raise HTTPException(
                status_code=403,
                detail="Access denied. You don't have permission.",
            )

        return user

    except HTTPException:
        # Re-raise HTTPException as-is
        raise
    except Exception as e:
        logger.error("Authentication failed: %s", e, exc_info=True)
        raise HTTPException(
            status_code=401,
            detail="Authentication failed. Unable to verify your identity.",
        ) from e

Quick Start

from fastapi import FastAPI, Depends
from cognito_auth import User
from cognito_auth.fastapi import FastAPIAuth

app = FastAPI()

# Auto-loads from environment variables
auth = FastAPIAuth()
auth.protect_app(app, bypass={"/health"})  # Protects entire app except /health!

@app.get("/")
def index(user: User = Depends(auth.get_auth_user)):
    return {"message": f"Welcome {user.name}!"}

@app.get("/health")
def health():
    return {"status": "ok"}

Configuration

FastAPIAuth inherits from BaseAuth and accepts these parameters:

  • authoriser (optional): Pre-configured Authoriser instance. If not provided, auto-loads from environment variables
  • redirect_url (optional): Where to redirect unauthorised users (default: "https://public.gds-idea.io/401.html")
  • region (optional): AWS region (default: "eu-west-2")
from cognito_auth import Authoriser
from cognito_auth.fastapi import FastAPIAuth

# Custom configuration
authoriser = Authoriser.from_lists(allowed_groups=["developers"])
auth = FastAPIAuth(
    authoriser=authoriser,
    redirect_url="https://myapp.com/unauthorised",
    region="us-east-1"
)

Behavior

FastAPIAuth uses dependency injection with Depends(). When authentication or authorisation fails:

  • With protect_app(): Middleware redirects to redirect_url before any route executes. Use the bypass parameter to exclude paths like health checks (e.g. bypass={"/health"}).
  • Without protect_app(): Routes with Depends(auth.get_auth_user) raise HTTPException (401 for auth failure, 403 for unauthorised)

The user is stored in request.state.user, making it efficient to call get_auth_user() multiple times.

Development Mode

Enable dev mode for local development without ALB. See Development Mode for full details.

export COGNITO_AUTH_DEV_MODE=true

Complete Example

from fastapi import FastAPI, Depends, HTTPException
from cognito_auth import User
from cognito_auth.fastapi import FastAPIAuth

app = FastAPI()

# Initialize and protect entire app
auth = FastAPIAuth()
auth.protect_app(app, bypass={"/health"})

@app.get("/health")
def health():
    return {"status": "ok"}

@app.get("/")
def index(user: User = Depends(auth.get_auth_user)):
    return {
        "message": f"Welcome {user.name}!",
        "groups": user.groups,
        "is_admin": user.is_admin
    }

@app.get("/admin")
def admin_only(user: User = Depends(auth.get_auth_user)):
    if not user.is_admin:
        raise HTTPException(status_code=403, detail="Admin access required")
    return {"message": "Admin panel"}

Protect Specific Routes Only

from fastapi import FastAPI, Depends
from cognito_auth import User
from cognito_auth.fastapi import FastAPIAuth

app = FastAPI()
auth = FastAPIAuth()
# Note: NOT calling protect_app()

@app.get("/public")
def public():
    return {"message": "Public endpoint"}

@app.get("/protected")
def protected(user: User = Depends(auth.get_auth_user)):
    return {"email": user.email}