Metadata-Version: 2.3
Name: mpxnk-auth
Version: 1.0.0
Summary: JWT RS256 session auth for FastAPI with Redis-backed refresh tokens
License: MIT
Keywords: fastapi,jwt,rs256,redis,auth,session
Author: meowthpxnk
Requires-Python: >=3.12,<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.12
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Dist: cryptography (>=43.0.0,<45.0.0)
Requires-Dist: fastapi (>=0.116.1,<0.117.0)
Requires-Dist: pydantic-settings (>=2.10.1,<3.0.0)
Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
Requires-Dist: redis (>=5.0.0,<6.0.0)
Requires-Dist: typing-extensions (>=4.12.0,<5.0.0)
Project-URL: Documentation, https://github.com/meowthpxnk/mpxnk-auth#readme
Project-URL: Homepage, https://github.com/meowthpxnk/mpxnk-auth
Project-URL: Issues, https://github.com/meowthpxnk/mpxnk-auth/issues
Project-URL: Repository, https://github.com/meowthpxnk/mpxnk-auth
Description-Content-Type: text/markdown

# mpxnk_auth

JWT session auth for FastAPI: RS256 access tokens, Redis-backed refresh sessions, and a ready `/auth` router.

## Stack

- Python 3.12
- FastAPI
- Redis 7
- JWT RS256
- Pydantic Settings

## Auth registration

The library does not ship `/login`. After `AuthAPI(...)` you declare it as a normal FastAPI route on `auth.router`, call `issue_session`, then `include_router`. Login body and `authorize` stay in the app.

```python
auth_api = AuthAPI(redis_client, UserSchema)


@auth_api.router.post("/login")
async def login(
    user: UserLoginSchema,
    response: Response,
) -> AccessTokenResponseSchema:
    try:
        user_data = authorize(user)
    except AppException as e:
        raise HTTPException(status_code=401, detail=str(e)) from e
    return await auth_api.issue_session(user_data, response)


app.include_router(auth_api.router)
```

`authorize` returns your `UserBase` subclass. `issue_session` creates the Redis session, sets the refresh cookie, and returns `{ "access_token": "..." }`. Declare `/login` **before** `include_router`.

Subclass `UserBase` with your claims (do not define `session_id`, `exp`, or `type`).

## Routes

| Method | Path | Auth |
| --- | --- | --- |
| POST | `/auth/login` | none (you declare this route) |
| POST | `/auth/refresh-token` | refresh cookie `Refresh-Token` |
| GET | `/auth/me` | Bearer access token |
| POST | `/auth/logout` | Bearer access token |
| POST | `/auth/revoke-all-sessions` | Bearer access token |
| POST | `/auth/revoke-all-sessions-except-current` | Bearer access token |

Refresh token is an HttpOnly cookie (`secure=True`, `samesite=None`). Default router prefix is `/auth`.

## Custom login

Same FastAPI route, any body schema. Still call `issue_session` when the user is authenticated:

```python
@auth.router.post("/login")
async def login(
    payload: MySchema, response: Response
) -> AccessTokenResponseSchema:
    user = await my_custom_auth(payload)
    return await auth.issue_session(user, response)
```

## Protecting your routes

```python
@app.get("/secret")
def secret(data: auth.BearerAuthDepends):
    return auth.session_service.payload_to_userdata(data)
```

`auth.BearerAuthDepends` decodes the access JWT. `auth.RefreshTokenDepends` reads the refresh cookie.

## RSA keys

Tokens are signed with RS256. On startup the library reads two files from `keys_path` (default `jwt_keys/`, relative to the process working directory):

```
jwt_keys/
  private.pem    # PKCS#1 PEM, used to sign
  public.pem     # OpenSSH public key, used to verify
```

Create the directory and a 4096-bit pair:

```bash
mkdir jwt_keys
openssl genrsa -out jwt_keys/private.pem 4096
ssh-keygen -y -f jwt_keys/private.pem > jwt_keys/public.pem
```

`private.pem` must be `-----BEGIN RSA PRIVATE KEY-----`. `public.pem` must be a single `ssh-rsa ...` line (not a PEM `BEGIN PUBLIC KEY` block) — that is what `load_ssh_public_key` expects.

Another path: set `JWT_KEYS_PATH` or `JWTAuthSettings(keys_path="...")` to an absolute directory that contains the same two filenames. Do not commit `private.pem`.

## Settings

`JWTAuthSettings` (Pydantic Settings) aliases:

| Field | Env | Default |
| --- | --- | --- |
| `access_ttl` | `ACCESS_TOKEN_TTL` | 900 |
| `refresh_ttl` | `REFRESH_TOKEN_TTL` | 2419200 |
| `max_user_sessions` | `MAX_USER_SESSIONS` | 5 |
| `keys_path` | `JWT_KEYS_PATH` | `jwt_keys` |
| `algorithm` | `JWT_ALGORITHM` | `RS256` |

Pass an instance into `AuthAPI(..., settings=JWTAuthSettings())` or rely on the default (env-loaded).

## Exceptions

- `NotFoundSession` — Redis session missing (logout → HTTP 404)
- `FailedRefreshSession` — refresh token does not match Redis
- `SessionLimitReached` — over `max_user_sessions` (oldest sessions are evicted)
- `NotValidTokenType` — access/refresh type mismatch

## Development

```bash
poetry install --with dev
poetry run ruff check mpxnk_auth tests
poetry run pytest
poetry run pytest -m unit
poetry run pytest -m api
```

RSA keys for local runs: see **RSA keys** above. Tests generate ephemeral keys under a temp directory and use `fakeredis`.

## Install

```bash
pip install mpxnk-auth
```

## Build locally

```bash
poetry build
# artifacts: dist/mpxnk_auth-*.whl and dist/mpxnk_auth-*.tar.gz
```

Bump `version` in `pyproject.toml` and `__version__` in `mpxnk_auth/__init__.py` together before a release.

