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.
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')returnsNoneandrequire_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 isdefault). 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
stateandnonceprotection- 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 severalOIDCMixininstances) single-identity(default) ormulti-identitysession strategy- Composable with any other
KayaMixin(RSGI, MCP, etc.)
Security notes
- The
nonesigning algorithm is rejected by default. - Only algorithms listed in
OIDCConfig.allowed_id_token_algorithmsare accepted. - Always use HTTPS for
redirect_uriin 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.