Files
kaya/packages/kaya-oidc
woggioni 3af2e7a975 Add support for multiple OIDC providers
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.
2026-10-01 08:23:40 +08:00
..

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

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:

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:

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.