Metadata-Version: 2.1
Name: fastapi-auth-oauth
Version: 0.3.0
Summary: Authentication as a package for FastAPI — email/password, JWT, and Google OAuth in one install.
Home-page: https://github.com/arslanfayyaz/authkit-
License: MIT
Keywords: fastapi,authentication,jwt,oauth,google-login,login,user-authentication,python,security,sqlalchemy,csrf,cookie-auth,redis,token-revocation
Author: Arsalan
Author-email: arslanghouri500@gmail.com
Requires-Python: >=3.11,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Classifier: Topic :: Security
Classifier: Typing :: Typed
Provides-Extra: redis
Requires-Dist: bcrypt (>=4.1)
Requires-Dist: fastapi (>=0.115)
Requires-Dist: httpx (>=0.28)
Requires-Dist: pydantic[email] (>=2.9)
Requires-Dist: pyjwt (>=2.9)
Requires-Dist: redis (>=5.0) ; extra == "redis"
Requires-Dist: sqlalchemy (>=2.0)
Project-URL: Documentation, https://github.com/arslanfayyaz/authkit-
Project-URL: Repository, https://github.com/arslanfayyaz/authkit-
Description-Content-Type: text/markdown

# fastapi-auth-oauth

**Authentication as a package for FastAPI — email/password, JWT, and Google OAuth in one install.**

[![PyPI version](https://img.shields.io/pypi/v/fastapi-auth-oauth)](https://pypi.org/project/fastapi-auth-oauth/)
[![Python versions](https://img.shields.io/pypi/pyversions/fastapi-auth-oauth)](https://pypi.org/project/fastapi-auth-oauth/)
[![License](https://img.shields.io/pypi/l/fastapi-auth-oauth)](https://github.com/arslanfayyaz/authkit-/blob/main/LICENSE)
[![Downloads](https://static.pepy.tech/badge/fastapi-auth-oauth)](https://pepy.tech/project/fastapi-auth-oauth)
[![CI](https://github.com/arslanfayyaz/authkit-/actions/workflows/ci.yml/badge.svg)](https://github.com/arslanfayyaz/authkit-/actions/workflows/ci.yml)

Install with `pip install fastapi-auth-oauth`, import as `authkit`.

Install it, point it at a database and a secret key,
and get email/password auth, JWT tokens, and Google sign-in — without writing any of it
yourself.

```python
from fastapi import FastAPI, Depends
from authkit import AuthKit

app = FastAPI()
auth = AuthKit(app, secret_key="...", database_url="postgresql+psycopg://...")
auth.include_routes()

@app.get("/dashboard")
async def dashboard(user=Depends(auth.login_required)):
    return {"user": user.email}
```

Nine routes, one dependency, zero boilerplate:

```
/auth/register              /auth/login               /auth/refresh
/auth/logout                /auth/me                  /auth/password-reset/request
/auth/password-reset/confirm                            /auth/email/verify/request
/auth/email/verify/confirm  /auth/google/login          /auth/google/callback
```

## Table of contents

- [Features](#features)
- [Installation](#installation)
- [Quickstart](#quickstart)
- [Configuration reference](#configuration-reference)
- [API reference](#api-reference)
- [System design](#system-design)
- [Data model](#data-model)
- [Roadmap](#roadmap)
- [Development](#development)
- [Contributing](#contributing)
- [License](#license)

## Features

- **Email + password auth** — register, login, `is_active` gating
- **JWT access + refresh tokens** — short-lived access token, longer-lived refresh token,
  independent expiry configuration
- **`login_required` dependency** — one `Depends()` call protects any route and injects the
  current user
- **Logout / token revocation** — blacklist so a logged-out token can't be reused; in-memory
  by default, Redis-backed for multi-worker deployments (`token_blacklist="redis://..."`)
- **Cookie or bearer transport** — tokens via `Authorization: Bearer` (default), HttpOnly
  cookies with double-submit CSRF protection, or both
- **Google sign-in (OAuth 2.0)** — full authorization-code flow, CSRF-protected state, account
  linking so a user can have both a password login and a Google login on the same account
- **Password reset** — request/confirm flow with single-use, hashed, expiring tokens
- **Email verification** — same token mechanism, separate purpose
- **Rate limiting** — per-IP sliding window on `/login` and `/register`
- **Pluggable side effects** — AuthKit never sends an email or decides how to redirect after
  OAuth; you supply the hook, AuthKit calls it
- **Bring your own database** — any SQLAlchemy-supported engine; tables are namespaced
  (`authkit_*`) so they never collide with your app's own schema
- **Sync and async SQLAlchemy** — pass an async URL (`postgresql+asyncpg://`,
  `sqlite+aiosqlite://`) and every route and DB call runs natively async; no config flag needed

## Installation

```bash
pip install fastapi-auth-oauth

# with Redis-backed token blacklist support:
pip install "fastapi-auth-oauth[redis]"
```

The PyPI distribution name is `fastapi-auth-oauth` — but you always `import` it as `authkit`:

```python
from authkit import AuthKit
```

To install straight from the latest commit on GitHub instead (e.g. to try unreleased changes):

```bash
pip install git+https://github.com/arslanfayyaz/authkit-.git
```

## Quickstart

```python
from fastapi import FastAPI, Depends
from authkit import AuthKit

app = FastAPI()

auth = AuthKit(
    app,
    secret_key="change-me-to-a-real-secret",
    database_url="sqlite:///./app.db",
)
auth.include_routes()

@app.get("/dashboard")
async def dashboard(user=Depends(auth.login_required)):
    return {"email": user.email, "verified": user.email_verified}
```

```bash
uvicorn main:app --reload

curl -X POST localhost:8000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "supersecret123"}'

curl -X POST localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "supersecret123"}'

curl localhost:8000/dashboard \
  -H "Authorization: Bearer <access_token from login response>"
```

## Configuration reference

```python
auth = AuthKit(
    app,
    secret_key="...",                     # required — used to sign JWTs and OAuth state
    database_url="postgresql+psycopg://user:pass@host/db",
    algorithm="HS256",
    access_token_expires_minutes=15,
    refresh_token_expires_days=7,
    action_token_expires_minutes=30,      # password reset / email verification token TTL
    auto_create_tables=True,              # False if you manage migrations yourself
    engine_options=None,                  # forwarded to sqlalchemy.create_engine
    token_blacklist="memory",             # or "redis://localhost:6379/0", or a custom backend
    token_transport="bearer",             # or "cookie" (HttpOnly cookies + CSRF) or "both"
    cookie_secure=True,                   # cookie mode: only send cookies over HTTPS
    cookie_samesite="lax",
    cookie_domain=None,
    google_oauth={
        "client_id": "...",
        "client_secret": "...",
        "redirect_uri": "https://yourapp.com/auth/google/callback",
    },
    on_password_reset_requested=lambda user, token: send_email(user, token),
    on_email_verification_requested=lambda user, token: send_email(user, token),
    on_oauth_login_complete=lambda user, tokens, redirect_after: my_redirect(user, tokens, redirect_after),
)
```

| Parameter | Required | Default | Notes |
|---|---|---|---|
| `secret_key` | ✅ | — | Signs JWTs and OAuth state tokens |
| `database_url` | ✅ | — | Any SQLAlchemy-compatible URL. An async driver (`+asyncpg`, `+aiosqlite`) switches the whole stack to native async — with `auto_create_tables=True` tables are then created on app startup |
| `google_oauth` | — | `None` | Omit entirely to disable Google sign-in — its routes won't be mounted |
| `auto_create_tables` | — | `True` | Set `False` and run your own migration if you use Alembic |
| `token_blacklist` | — | `"memory"` | `"memory"`, a `redis://` URL, or any object with `revoke()`/`is_revoked()`. Use Redis when running more than one worker |
| `token_transport` | — | `"bearer"` | `"bearer"`, `"cookie"`, or `"both"`. Cookie mode sets HttpOnly access/refresh cookies and enforces double-submit CSRF |
| `cookie_secure` / `cookie_samesite` / `cookie_domain` | — | `True` / `"lax"` / `None` | Cookie attributes (cookie mode only) |
| `on_password_reset_requested` | — | no-op | `(user, raw_token) -> None` — send the email yourself |
| `on_email_verification_requested` | — | no-op | Same shape as above |
| `on_oauth_login_complete` | — | redirect with base64 payload | `(user, tokens, redirect_after) -> Response` |

### Cookie mode

With `token_transport="cookie"` (or `"both"`), login/register/refresh set three cookies:
`authkit_access` and `authkit_refresh` (HttpOnly — invisible to JavaScript) and
`authkit_csrf` (readable by JavaScript on purpose). The browser then authenticates
requests automatically; no `Authorization` header needed.

For any state-changing request (POST/PUT/PATCH/DELETE) authenticated via cookie, the client
must echo the CSRF cookie back in a header — the standard double-submit pattern:

```js
fetch("/auth/logout", {
  method: "POST",
  headers: { "X-CSRF-Token": getCookie("authkit_csrf") },
});
```

The refresh cookie is path-scoped to the `/auth/refresh` endpoint, so it is never sent
anywhere else. `POST /auth/refresh` with no body uses it automatically.

## API reference

| Method | Path | Auth required | Description |
|---|---|---|---|
| `POST` | `/auth/register` | No | Create an account, returns user + token pair |
| `POST` | `/auth/login` | No | Authenticate, returns user + token pair |
| `POST` | `/auth/refresh` | No (refresh token) | Exchange a refresh token (body, or cookie in cookie mode) for a new pair |
| `POST` | `/auth/logout` | Yes | Revoke the current access token; pass `{"refresh_token": ...}` to revoke the pair. Clears auth cookies in cookie mode |
| `GET` | `/auth/me` | Yes | Return the current user |
| `POST` | `/auth/password-reset/request` | No | Issue a password reset token via the configured hook |
| `POST` | `/auth/password-reset/confirm` | No (reset token) | Consume the token, set a new password |
| `POST` | `/auth/email/verify/request` | Yes | Issue an email verification token via the configured hook |
| `POST` | `/auth/email/verify/confirm` | No (verification token) | Consume the token, mark the email verified |
| `GET` | `/auth/google/login` | No | Redirect to Google's consent screen |
| `GET` | `/auth/google/callback` | No | Handle Google's callback, create/link the user, redirect |

All error responses are `{"detail": "..."}` with the appropriate HTTP status — 401 for bad
credentials/tokens, 403 for inactive accounts, 409 for duplicate registration, 429 for rate
limiting.

## System design

AuthKit is a single facade (`AuthKit`) composing four independent layers. Each layer only
depends on the layers below it — there are no upward or circular imports, which keeps every
piece independently testable and replaceable.

```
                      ┌───────────────────────────────────────────┐
                      │                AuthKit (core.py)            │
                      │   the only class an integrator touches       │
                      └───────────────────┬───────────────────────┘
                                          │ wires together
        ┌─────────────────────┬─────────┼─────────────────┬─────────────────────┐
        ▼                     ▼                             ▼                     ▼
┌───────────────┐   ┌──────────────────┐        ┌──────────────────┐   ┌──────────────────┐
│    routes/      │   │    services/       │        │      oauth/        │   │  dependencies.py  │
│  HTTP contracts  │──▶│  business logic    │        │  provider adapters  │   │  FastAPI Depends()  │
│  auth.py         │   │  AuthService        │        │  base / google /    │   │  login_required     │
│  google.py       │   │  TokenService       │        │  state / redirect   │   │  get_session         │
└───────────────┘   │  ActionTokenService │        └──────────────────┘   └──────────────────┘
                      └─────────┬──────────┘
                                │ reads/writes through
                                ▼
                      ┌──────────────────┐
                      │  repositories/      │
                      │  UserRepository     │   ← the only layer that writes SQL
                      └─────────┬──────────┘
                                ▼
                      ┌──────────────────┐
                      │     models.py        │   User · UserIdentity · ActionToken
                      └─────────┬──────────┘
                                ▼
                      ┌──────────────────┐
                      │      db.py            │   SQLAlchemy engine + session factory
                      └──────────────────┘

  cross-cutting, used by routes/services directly:
  config.py (typed settings) · security.py (hashing + JWT primitives)
  exceptions.py (typed errors → HTTP status) · rate_limit.py · token_blacklist.py
```

**Why layered this way:**

- **Routes never touch the database.** They validate input, call a service, and shape the
  HTTP response. All persistence goes through `repositories/`.
- **Services own business rules**, not SQL or HTTP. `AuthService.authenticate` doesn't know
  it's being called from a FastAPI route — it just raises a typed exception on failure.
- **One exception hierarchy (`AuthKitError`) maps to HTTP status codes in one place** — a
  single `@app.exception_handler(AuthKitError)` registered in `core.py`. No route handler
  contains a raw `HTTPException`.
- **OAuth providers implement one interface** (`oauth/base.py: OAuthProvider`). Google is the
  only implementation today; adding GitHub or Microsoft is a new file in `oauth/`, not a change
  to `core.py` or the route layer.
- **Every side effect the host application needs to control is a hook, not a hardcoded
  integration.** AuthKit does not send emails or decide how to redirect after OAuth login — it
  calls `on_password_reset_requested`, `on_email_verification_requested`, and
  `on_oauth_login_complete`, which the integrator supplies. AuthKit's job is auth, not email
  delivery.

## Data model

- **`User`** — one row per account. `hashed_password` is nullable, because an OAuth-only user
  has no password.
- **`UserIdentity`** — links a `User` to a third-party provider (`provider`,
  `provider_user_id`). A user can have a password login **and** a Google login on the same
  account, matching how real products behave.
- **`ActionToken`** — single-use, hashed, expiring tokens for password reset and email
  verification, keyed by purpose.

Tables are prefixed `authkit_*` so they never collide with the host application's own schema.

## Roadmap

Deliberately out of scope for v1, planned as the next layer on top of this one:

- [ ] Two-factor authentication (TOTP)
- [ ] API keys (create, scope, rotate, revoke)
- [ ] Roles & permissions
- [ ] Multi-tenant organizations (teams, invitations, membership)
- [ ] Additional OAuth providers (GitHub, Microsoft)
- [x] Persistent token blacklist (Redis-backed, for multi-process deployments) — shipped in 0.2.0
- [x] Cookie token transport with CSRF protection — shipped in 0.2.0
- [x] Async SQLAlchemy support — shipped in 0.3.0
- [ ] Google service integrations — Google Ads, Google Business Profile (GMB), and Local Services Ads (LSA) account linking on top of the Google OAuth flow

## Development

Requires Python 3.11+.

```bash
git clone https://github.com/arslanfayyaz/authkit-.git
cd authkit
python3 -m venv venv && source venv/bin/activate
pip install -e .
pip install pytest
pytest
```

The test suite runs against an in-memory SQLite database — no external services required.

## Contributing

Issues and pull requests are welcome. Before opening a PR:

1. Run `pytest` and make sure everything passes
2. Keep the layering intact — routes call services, services call repositories, nothing skips
   a layer
3. Match the existing style: no comments, self-explanatory names, typed function signatures

## License

MIT — see [LICENSE](LICENSE).

