Namespace per-provider session state as oidc:{provider}:... so several providers can share one session, and add an OIDCProviders mixin that wires multiple configs onto distinct /auth/{provider}/... routes. Logging out of one provider no longer signs the user out of the others.
141 lines
4.7 KiB
Markdown
141 lines
4.7 KiB
Markdown
# kaya-oidc
|
|
|
|
OpenID Connect authentication for the Kaya web framework.
|
|
|
|
Built on top of `kaya-session` and implements the OIDC **Authorization Code
|
|
Flow with PKCE**.
|
|
|
|
## Usage
|
|
|
|
```python
|
|
import os
|
|
from kaya.core import HttpContext, KayaApp
|
|
from kaya.session import SessionMixin, InMemorySessionStore
|
|
from kaya.oidc import OIDCConfig, OIDCMixin
|
|
|
|
session = SessionMixin(InMemorySessionStore())
|
|
oidc = OIDCMixin(
|
|
OIDCConfig(
|
|
issuer=os.environ['OIDC_ISSUER'],
|
|
client_id=os.environ['OIDC_CLIENT_ID'],
|
|
client_secret=os.environ.get('OIDC_CLIENT_SECRET'),
|
|
redirect_uri='http://localhost:8000/auth/callback',
|
|
fetch_userinfo=True,
|
|
),
|
|
session=session,
|
|
)
|
|
app = KayaApp(mixins=[session, oidc])
|
|
|
|
@app.GET('/')
|
|
async def home(ctx: HttpContext):
|
|
await ctx.send_str(200, 'public home')
|
|
|
|
@app.GET('/profile')
|
|
@oidc.require_auth
|
|
async def profile(ctx: HttpContext):
|
|
user = oidc.get_user(ctx)
|
|
await ctx.send_str(200, f'Hello {user.email or user.sub}')
|
|
```
|
|
|
|
`OIDCMixin` depends on `SessionMixin`; passing only `oidc` to `KayaApp(mixins=...)`
|
|
also works because the app applies mixin dependencies automatically.
|
|
|
|
## Multiple providers
|
|
|
|
Use `OIDCProviders` to run several OIDC providers in one application. Each
|
|
config's `provider` becomes a route segment under `base_path`
|
|
(`/auth/{provider}/login`, `/auth/{provider}/callback` and
|
|
`/auth/{provider}/logout`), and all providers share a single `SessionMixin`:
|
|
|
|
```python
|
|
from kaya.oidc import OIDCConfig, OIDCProviders
|
|
|
|
session = SessionMixin(InMemorySessionStore())
|
|
oidc = OIDCProviders(
|
|
[
|
|
OIDCConfig(
|
|
issuer='https://accounts.google.com',
|
|
client_id=os.environ['GOOGLE_CLIENT_ID'],
|
|
client_secret=os.environ['GOOGLE_CLIENT_SECRET'],
|
|
redirect_uri='http://localhost:8000/auth/google/callback',
|
|
provider='google',
|
|
),
|
|
OIDCConfig(
|
|
issuer='https://github.com/login/oauth', # or any OIDC issuer
|
|
client_id=os.environ['GITHUB_CLIENT_ID'],
|
|
client_secret=os.environ['GITHUB_CLIENT_SECRET'],
|
|
redirect_uri='http://localhost:8000/auth/github/callback',
|
|
provider='github',
|
|
),
|
|
],
|
|
session=session,
|
|
base_path='/auth', # optional
|
|
)
|
|
app = KayaApp(mixins=[session, oidc])
|
|
|
|
@app.GET('/google-profile')
|
|
@oidc.require_auth('google')
|
|
async def google_profile(ctx: HttpContext):
|
|
user = oidc.get_user(ctx, 'google')
|
|
await ctx.send_str(200, f"Hello {user.email or user.sub}")
|
|
|
|
@app.GET('/who')
|
|
async def who(ctx: HttpContext):
|
|
users = oidc.get_users(ctx) # {provider: OIDCUser}
|
|
providers = oidc.logged_in_providers(ctx) # ['google', ...]
|
|
await ctx.send_str(200, ', '.join(users))
|
|
```
|
|
|
|
Each provider's session state is namespaced as `oidc:{provider}:...`, so a user
|
|
can be logged into several providers at once. Logging out of one provider only
|
|
removes that provider's state and keeps the shared session alive while another
|
|
provider is still logged in. Make sure each config's `redirect_uri` points at
|
|
that provider's derived callback path.
|
|
|
|
The composable alternative is to create one `OIDCMixin` per provider, pointing
|
|
each at distinct `login_path`/`callback_path`/`logout_path` values and sharing
|
|
the same `SessionMixin`:
|
|
|
|
```python
|
|
google = OIDCMixin(google_config, session=session)
|
|
github = OIDCMixin(github_config, session=session)
|
|
app = KayaApp(mixins=[session, google, github])
|
|
```
|
|
|
|
> **Upgrading:** session keys are now prefixed with `oidc:{provider}:` (the
|
|
> default provider is `default`). Existing logged-in users are signed out once
|
|
> and simply re-authenticate.
|
|
|
|
## Features
|
|
|
|
- Generic OIDC discovery
|
|
- Authorization Code Flow with PKCE (S256)
|
|
- ID token signature validation with JWKS
|
|
- `state` and `nonce` protection
|
|
- Session fixation defense via `regenerate_id()` after login
|
|
- Optional userinfo endpoint fetch
|
|
- Refresh token support
|
|
- RP-initiated logout (when provider advertises `end_session_endpoint`)
|
|
- Multiple providers in one app via `OIDCProviders` (or several `OIDCMixin` instances)
|
|
- Composable with any other `KayaMixin` (RSGI, MCP, etc.)
|
|
|
|
## Security notes
|
|
|
|
- The `none` signing algorithm is rejected by default.
|
|
- Only algorithms listed in `OIDCConfig.allowed_id_token_algorithms` are accepted.
|
|
- Always use HTTPS for `redirect_uri` in production.
|
|
|
|
## Supported flows
|
|
|
|
Only the Authorization Code Flow with PKCE is supported. Implicit and Hybrid
|
|
flows are intentionally not implemented.
|
|
|
|
## Supported algorithms
|
|
|
|
ID token signature verification supports:
|
|
`RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`,
|
|
`ES256`, `ES384`, `ES512`, and `EdDSA`.
|
|
|
|
HMAC algorithms (`HS*`) are disabled by default and can be enabled by adding
|
|
them to `allowed_id_token_algorithms` if your provider uses them.
|