Skip to content

feat(users): OAuth/OIDC login (Google, GitHub, generic OIDC) - #132

Merged
antosubash merged 2 commits into
mainfrom
claude/oauth-openid-research-8GAbX
May 6, 2026
Merged

feat(users): OAuth/OIDC login (Google, GitHub, generic OIDC)#132
antosubash merged 2 commits into
mainfrom
claude/oauth-openid-research-8GAbX

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Summary

Adds social login to the users module on top of fastapi-users' existing cookie auth — Google, GitHub, and any OIDC-discovery-compliant provider (Keycloak, Authentik, Auth0, Zitadel, Entra ID, ...) — with no new top-level dependency.

  • Bumps fastapi-users[sqlalchemy]fastapi-users[sqlalchemy,oauth] (pulls in httpx-oauth).
  • Adds an OAuthAccount SQLModel table (users_oauth_account) that links a (provider, account_id) pair to a local User. Migration 3bf3f9db7f7f creates it.
  • Each provider is feature-flagged by env var pair: setting SM_USERS_OAUTH_<PROVIDER>_CLIENT_ID + _SECRET mounts the routes; unset = no-op. Generic OIDC additionally requires SM_USERS_OAUTH_OIDC_DISCOVERY_URL.
  • Registers one /api/users/auth/<provider>/{login,callback} pair per configured provider. The callback handler is a thin wrapper around UserManager.oauth_callback (find-or-create + email association from fastapi-users) that returns a 303 to login_redirect_url — the stock fastapi-users router returns 204, which dead-ends a server-rendered Inertia browser flow.
  • Bridges session["user_id"] (which AuthMiddleware reads) inside UserManager.on_after_login so OAuth and password flows share one path into the auth middleware. Existing wrapper login endpoints already do this; the new assignment is idempotent for them.
  • Login page renders one button per enabled provider, sourced from a lightweight enabled_provider_names() helper that does no network or client construction.

How to enable a provider

SM_USERS_OAUTH_GOOGLE_CLIENT_ID=...
SM_USERS_OAUTH_GOOGLE_CLIENT_SECRET=...

SM_USERS_OAUTH_GITHUB_CLIENT_ID=...
SM_USERS_OAUTH_GITHUB_CLIENT_SECRET=...

# OIDC-generic — works with Keycloak / Authentik / Auth0 / Zitadel / etc.
SM_USERS_OAUTH_OIDC_CLIENT_ID=...
SM_USERS_OAUTH_OIDC_CLIENT_SECRET=...
SM_USERS_OAUTH_OIDC_DISCOVERY_URL=https://idp.example.com/.well-known/openid-configuration
SM_USERS_OAUTH_OIDC_DISPLAY_NAME=Workspace SSO  # optional

Adding a new built-in provider later is one if block in users/oauth.py and one entry in enabled_provider_names().

Test plan

  • make lint (ruff format / ruff check / ty / biome / tsc / 300-line cap)
  • make test — Python 978 passed, JS 8 passed
  • New modules/users/tests/test_oauth.py (8 tests): provider-list helper, build_clients, OAuthAccount round-trip + FK cascade, UserManager.oauth_callback create-new + associate-by-email
  • make doctor clean — no new SM* warnings
  • App boots with no OAuth env vars set (no providers mount) and with creds set (Google/GitHub routes present)
  • Manual QA against a live IdP (deliberately not automated — would require mocking the full httpx_oauth token-exchange + profile-fetch pair; better validated against a real provider)

Out of scope (deferred)

  • Token refresh — OAuthAccount.refresh_token is stored but never proactively refreshed. Add when calling provider APIs on the user's behalf.
  • Account-unlinking UI — needs an account-settings page first.
  • Per-tenant OAuth configs — settings are global.

https://claude.ai/code/session_0117vVTBiz6EppNQ6RTT7haD


Generated by Claude Code

Builds on fastapi-users' existing cookie+DB auth backend by enabling its
``[oauth]`` extra and mounting one ``/auth/<provider>/{login,callback}``
pair per configured provider. Providers are gated by ``SM_USERS_OAUTH_*``
env vars — unset = unmounted, no boot-time failure.

The callback handler is a thin wrapper around ``UserManager.oauth_callback``
that returns a 303 redirect to ``login_redirect_url`` (the stock
fastapi-users router returns 204 No Content, which dead-ends a server-
rendered Inertia browser flow). Session-cookie bridge (``session["user_id"]``)
is now done in ``UserManager.on_after_login`` so OAuth and password flows
share the same path into ``AuthMiddleware``.

https://claude.ai/code/session_0117vVTBiz6EppNQ6RTT7haD
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 6, 2026

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: d7875b5
Status: ✅  Deploy successful!
Preview URL: https://1fa31928.simple-module-python.pages.dev
Branch Preview URL: https://claude-oauth-openid-research.simple-module-python.pages.dev

View logs

…esearch-8GAbX

# Conflicts:
#	modules/users/users/pages/Login.tsx
@antosubash
antosubash marked this pull request as ready for review May 6, 2026 20:33
@antosubash
antosubash merged commit b92cd26 into main May 6, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants