Files
kaya/packages/kaya-oidc/README.md
T
woggioni 15dc4b6dad Add single/multi identity strategy to OIDC providers
OIDCProviders and OIDCMixin accept strategy='single-identity' (default) or 'multi-identity'. Single-identity evicts other providers' identities and tokens after a successful login so the session holds exactly one user, matching Spring Security's last-login-wins behavior. Ephemeral flow keys are preserved so in-flight logins can still complete.
2026-10-01 08:23:40 +08:00

5.7 KiB

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}:.... Make sure each config's redirect_uri points at that provider's derived callback path.

Identity strategy

strategy controls how many identities may coexist in the shared session:

  • 'single-identity' (default): completing a login evicts every other provider's identity and tokens, so the session holds exactly one user — last login wins, like Spring Security. After a GitHub login, get_user(ctx, 'google') returns None and require_auth('google') redirects to the Google login.
  • 'multi-identity': each provider stays logged in independently. Use it for linked accounts, aggregating several provider APIs, or step-up flows. Logging out of one provider keeps the shared session alive while another provider is still logged in.
oidc = OIDCProviders([google_config, github_config], session=session,
                     strategy='multi-identity')

Ephemeral flow keys (state, nonce, code_verifier) are never evicted, so logins already in flight can still complete; under 'single-identity' the last callback to finish wins. Under either strategy, get_users(ctx) only reports providers actually present in the session.

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. The same strategy keyword is accepted and defaults to 'single-identity':

google = OIDCMixin(google_config, session=session)
github = OIDCMixin(github_config, session=session, strategy='multi-identity')
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)
  • single-identity (default) or multi-identity session strategy
  • 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.