Skip to main content

Modern authentication for FastAPI

Project description

Belgie: FastAPI Authentication with OAuth, Sessions, and Typed Plugins

[!WARNING] This project is currently in beta. The APIs are still settling ahead of a stable v1.0 release, especially around optional plugin packages such as organization, team, and MCP support.

The name "Belgie" is a nod to Belgium's role as a crossroads for languages, trade, and institutions. In the same spirit, Belgie is built to sit at the center of a FastAPI application and connect authentication, session management, OAuth flows, and optional app-specific plugins without forcing you into a hosted identity platform.

Belgie brings Google and Microsoft OAuth, signed sliding-window sessions, route protection, and typed extension points into a single Python-first workflow. It is designed for teams that want app-owned auth routes, SQLAlchemy-friendly persistence, and a small surface area that stays easy to reason about in production.

Belgie combines a focused core package with optional workspace packages for SQLAlchemy adapters, OAuth client and server flows, organization and team management, and MCP integration. Whether you need a minimal Google sign-in flow for a FastAPI app or a larger self-hosted auth foundation with org and team concepts, Belgie keeps the API explicit and the integration path short.

Installation

uv add belgie

For the common SQLAlchemy-backed setup:

uv add belgie[alchemy]

For organization and team support:

uv add belgie[alchemy,organization,team]

Optional extras: alchemy, mcp, oauth, oauth-client, organization, sso, stripe, team, and all.

[!NOTE] This workspace targets Python >=3.12,<3.15.

Package Layout

  • belgie-core: Core auth client, settings, session manager, and plugin system.
  • belgie-alchemy: SQLAlchemy adapters and mixins for Belgie models.
  • belgie-oauth: OAuth client plugins, including Google and Microsoft sign-in support.
  • belgie-oauth-server: OAuth 2.1 authorization server building blocks.
  • belgie-organization: Organization plugin and request-scoped client APIs.
  • belgie-stripe: Stripe billing plugin with Checkout, Customer Portal, and webhook-backed subscription sync.
  • belgie-team: Team plugin and team management client APIs.
  • belgie-mcp: MCP integration for authenticated server deployments.
  • belgie-proto: Shared protocol interfaces used across the workspace.

Examples

  • auth: Basic FastAPI app with Google OAuth, sessions, and protected routes.
  • oauth: OAuth-focused example application.
  • oauth_server_custom_pages: OAuth server flow with app-owned pages.
  • organization_team: End-to-end organization and team example.
  • stripe: Local sign-in plus Stripe Checkout, billing portal, and webhook-backed sync.
  • mcp: MCP integration example.
  • oauth_client_plugin: Client plugin example for OAuth-driven flows.

Quick Start

Here's a complete example showing how to add Google sign-in, session-backed auth, and protected routes to a FastAPI app:

Project Structure:

my-app/
├── main.py
└── models.py

models.py:

from datetime import UTC, datetime
from uuid import UUID, uuid4

from sqlalchemy import JSON, ForeignKey, Index, Text, UniqueConstraint
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"

    id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
    email: Mapped[str] = mapped_column(Text, unique=True, index=True)
    name: Mapped[str | None] = mapped_column(Text, nullable=True)
    image: Mapped[str | None] = mapped_column(Text, nullable=True)
    email_verified_at: Mapped[datetime | None] = mapped_column(nullable=True)
    scopes: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
    created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
    updated_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))


class OAuthAccount(Base):
    __tablename__ = "accounts"
    __table_args__ = (
        UniqueConstraint("provider", "provider_account_id", name="uq_accounts_provider_provider_account_id"),
        Index("ix_accounts_user_id_provider", "user_id", "provider"),
    )

    id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
    user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
    provider: Mapped[str] = mapped_column(Text)
    provider_account_id: Mapped[str] = mapped_column(Text)
    access_token: Mapped[str | None] = mapped_column(Text, nullable=True)
    refresh_token: Mapped[str | None] = mapped_column(Text, nullable=True)
    access_token_expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
    refresh_token_expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
    token_type: Mapped[str | None] = mapped_column(Text, nullable=True)
    scope: Mapped[str | None] = mapped_column(Text, nullable=True)
    id_token: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))


class Session(Base):
    __tablename__ = "sessions"

    id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
    user_id: Mapped[UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
    expires_at: Mapped[datetime] = mapped_column(index=True)
    created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))


class OAuthState(Base):
    __tablename__ = "oauth_states"

    id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
    state: Mapped[str] = mapped_column(Text, unique=True, index=True)
    provider: Mapped[str | None] = mapped_column(Text, nullable=True)
    user_id: Mapped[UUID | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
    expires_at: Mapped[datetime] = mapped_column()
    code_verifier: Mapped[str | None] = mapped_column(Text, nullable=True)
    nonce: Mapped[str | None] = mapped_column(Text, nullable=True)
    intent: Mapped[str] = mapped_column(Text, default="signin")
    redirect_url: Mapped[str | None] = mapped_column(Text, nullable=True)
    error_redirect_url: Mapped[str | None] = mapped_column(Text, nullable=True)
    new_user_redirect_url: Mapped[str | None] = mapped_column(Text, nullable=True)
    payload: Mapped[dict[str, object] | None] = mapped_column(JSON, nullable=True)
    request_sign_up: Mapped[bool] = mapped_column(default=False)
    created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))

main.py:

from collections.abc import AsyncGenerator
from typing import Annotated

from fastapi import Depends, FastAPI, Security
from fastapi.responses import RedirectResponse
from sqlalchemy.engine import URL
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from belgie import Belgie, BelgieSettings
from belgie.alchemy import BelgieAdapter
from belgie.oauth.google import GoogleOAuth, GoogleOAuthClient
from models import OAuthAccount, OAuthState, Session, User

settings = BelgieSettings(
    secret="your-secret-key",
    base_url="http://localhost:8000",
)

engine = create_async_engine(URL.create("sqlite+aiosqlite", database="./app.db"))
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)


async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with session_maker() as session:
        yield session


auth = Belgie(
    settings=settings,
    adapter=BelgieAdapter(
        user=User,
        oauth_account=OAuthAccount,
        session=Session,
        oauth_state=OAuthState,
    ),
    database=get_db,
)

google_plugin = auth.add_plugin(
    GoogleOAuth(
        client_id="your-google-client-id",
        client_secret="your-google-client-secret",
        scopes=["openid", "email", "profile"],
    ),
)

app = FastAPI()
app.include_router(auth.router)


@app.get("/login/google")
async def login_google(
    google: Annotated[GoogleOAuthClient, Depends(google_plugin)],
    return_to: str | None = None,
):
    auth_url = await google.signin_url(return_to=return_to)
    return RedirectResponse(url=auth_url, status_code=302)


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


@app.get("/profile")
async def profile(user: User = Security(auth.user, scopes=["profile"])):
    return {"name": user.name, "email": user.email}

Belgie gives you the auth router, session validation, and request dependencies from one Belgie(...) instance. Add a plugin such as GoogleOAuth(...), include auth.router, and then protect routes with Depends(auth.user) or Security(auth.user, scopes=[...]).

Microsoft uses the same pattern with MicrosoftOAuth(...), MicrosoftOAuthClient, and the callback route at /auth/provider/microsoft/callback.

Run the app with uvicorn main:app --reload, visit /login/google, and Belgie will handle the OAuth callback, session creation, and subsequent authenticated requests.

Notes

  • Environment variables such as BELGIE_SECRET, BELGIE_BASE_URL, BELGIE_GOOGLE_CLIENT_ID, and BELGIE_GOOGLE_CLIENT_SECRET are loaded automatically by BelgieSettings().
  • Session lifetime is controlled by SessionSettings, and cookie security defaults are configured with CookieSettings.
  • The Google callback route is mounted at /auth/provider/google/callback.
  • Plugins no longer expose a bind() API; register them with auth.add_plugin(...).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

belgie-0.19.3.tar.gz (20.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

belgie-0.19.3-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

Details for the file belgie-0.19.3.tar.gz.

File metadata

  • Download URL: belgie-0.19.3.tar.gz
  • Upload date:
  • Size: 20.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for belgie-0.19.3.tar.gz
Algorithm Hash digest
SHA256 29fe0858c4340b1a5420c6c5b2af44f75a459d233ece2a2577f90ef73b40aad0
MD5 49132d1e3b271bb89b24cb7bb4dabfe6
BLAKE2b-256 515875b0e4a994546feb9c695bb06aeb6e292ba03a7a5728321767aff8fe0f2a

See more details on using hashes here.

File details

Details for the file belgie-0.19.3-py3-none-any.whl.

File metadata

  • Download URL: belgie-0.19.3-py3-none-any.whl
  • Upload date:
  • Size: 36.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for belgie-0.19.3-py3-none-any.whl
Algorithm Hash digest
SHA256 7caa0bb6484bc4895f2fd9d7269e592c4ae97f82870ead959120203e1dce803a
MD5 bb768dcfd1978d14c332ac27f359241f
BLAKE2b-256 3016498694666188461bd4dccf9a0a6c0359d6016ccac0953c6d10b0f81b1a5d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page