Metadata-Version: 2.4
Name: litestar-keycloak
Version: 0.1.0
Summary: Keycloak OIDC/OAuth2 authentication plugin for Litestar
Project-URL: Homepage, https://github.com/smirnoffmg/litestar-keycloak
Project-URL: Documentation, https://smirnoffmg.github.io/litestar-keycloak/
Project-URL: Repository, https://github.com/smirnoffmg/litestar-keycloak
Project-URL: Issues, https://github.com/smirnoffmg/litestar-keycloak/issues
Project-URL: Changelog, https://github.com/smirnoffmg/litestar-keycloak/releases
Author-email: Maksim Smirnov <smirnoffmg@gmail.com>
License-Expression: MIT
Keywords: authentication,jwt,keycloak,litestar,oauth2,oidc
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: aiohttp>=3.9
Requires-Dist: litestar>=2.0
Requires-Dist: pyjwt[crypto]>=2.0
Description-Content-Type: text/markdown

# litestar-keycloak

Keycloak authentication plugin for [Litestar](https://litestar.dev/).
OIDC/OAuth2 integration using Litestar's native plugin protocol, dependency injection, and guard system.

## Features

- OIDC discovery and JWKS caching with automatic key rotation
- Bearer token validation (header or cookie)
- Realm and client role guards
- Scope-based access control
- `KeycloakUser` injection via Litestar DI
- Optional login/callback/logout route group
- Async HTTP via aiohttp for token exchange and JWKS requests

## Installation

```bash
pip install litestar-keycloak
```

## Quick Start

```python
from litestar import Litestar, get
from litestar_keycloak import KeycloakPlugin, KeycloakConfig, KeycloakUser

@get("/me")
async def me(current_user: KeycloakUser) -> dict:
    return {
        "sub": current_user.sub,
        "username": current_user.preferred_username,
        "roles": current_user.realm_roles,
    }

app = Litestar(
    route_handlers=[me],
    plugins=[KeycloakPlugin(
        KeycloakConfig(
            server_url="https://keycloak.example.com",
            realm="my-realm",
            client_id="my-app",
        )
    )],
)
```

Any route that declares `current_user: KeycloakUser` automatically requires a valid Bearer token.

## Guards

Restrict access by roles or scopes:

```python
from litestar import get
from litestar_keycloak import require_roles, require_scopes

@get("/admin", guards=[require_roles("admin")])
async def admin_panel() -> dict:
    return {"msg": "welcome, admin"}

@get("/reports", guards=[require_scopes("reports:read")])
async def reports() -> dict:
    return {"msg": "here are your reports"}
```

## Configuration

```python
KeycloakConfig(
    server_url="https://keycloak.example.com",
    realm="my-realm",
    client_id="my-app",
    client_secret="secret",            # confidential clients
    token_location=TokenLocation.HEADER,  # HEADER (default) or COOKIE
    jwks_cache_ttl=3600,               # JWKS cache lifetime in seconds
    algorithms=("RS256",),             # JWT signing algorithms
    include_routes=False,              # mount /auth/login, /callback, /logout
    optional_audiences=frozenset({"my-service"}),  # accept service tokens too
)
```

Full option list: [docs/configuration.md](docs/configuration.md).

When `include_routes=True`, the plugin mounts:

| Endpoint             | Description                        |
| -------------------- | ---------------------------------- |
| `GET /auth/login`    | Redirect to Keycloak authorize     |
| `GET /auth/callback` | Handle authorization code exchange |
| `POST /auth/logout`  | End session (Keycloak + local)     |
| `POST /auth/refresh` | Refresh access token               |

## Testing

### Unit tests (no Keycloak required)

Test utilities live in `tests/conftest.py`: `create_test_token()` and `MockKeycloakPlugin()`. Use them in your tests (e.g. when running pytest from this repo, or import from conftest):

```python
from tests.conftest import create_test_token, MockKeycloakPlugin

# Mint a fake JWT with arbitrary claims
token = create_test_token(sub="user-1", realm_roles=["admin"])

# Use MockKeycloakPlugin to skip real JWKS validation
app = Litestar(
    route_handlers=[...],
    plugins=[MockKeycloakPlugin()],
)
```

### Integration tests (real Keycloak via testcontainers)

```python
from testcontainers.keycloak import KeycloakContainer

kc = (
    KeycloakContainer("quay.io/keycloak/keycloak:26.0")
    .with_command("start-dev --import-realm")
    .with_volume_mapping(
        "tests/fixtures/realm-export.json",
        "/opt/keycloak/data/import/realm-export.json",
        "ro",
    )
)
kc.start()
```

See `tests/fixtures/realm-export.json` for a pre-configured realm with test users and roles.

## Service-to-service

To accept tokens from a service client (e.g. client_credentials), add its client ID to **optional_audiences**. Use the **raw_token** dependency to forward the caller's token to downstream APIs. See [Service-to-service](docs/guides/service-to-service.md).

## Documentation

- **Guides**: [Configuration](docs/configuration.md), [Guards](docs/guides/guards.md), [OIDC routes](docs/guides/oidc-routes.md), [Testing](docs/guides/testing.md). Build the site with `mkdocs build` (or `mkdocs serve`).
- **Example app** with docker-compose and smoke tests: [examples/](examples/).

## Dependencies

| Package                    | Purpose                         |
| -------------------------- | ------------------------------- |
| `litestar[standard]` ≥ 2.0 | Web framework (plugin target)   |
| `aiohttp`                  | Async HTTP for token/JWKS calls |
| `PyJWT[crypto]`            | JWT validation                  |

## License

MIT
