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.
Back-channel logout
Set backchannel_logout_path to accept OIDC Back-Channel Logout tokens from
the provider:
OIDCConfig(
issuer=os.environ['OIDC_ISSUER'],
client_id=os.environ['OIDC_CLIENT_ID'],
client_secret=os.environ['OIDC_CLIENT_SECRET'],
redirect_uri='https://example.com/auth/callback',
backchannel_logout_path='/auth/backchannel-logout',
)
Register the corresponding absolute URL (for example
https://example.com/auth/backchannel-logout) as the client's back-channel
logout URI with the provider. This registration happens at the provider; it is
not part of OIDC discovery. With OIDCProviders, enabling the option derives
{base_path}/{provider}/backchannel-logout for that provider.
The endpoint accepts form-encoded POST requests containing logout_token.
It validates the JWT signature against the provider JWKS, the logout+jwt
type, issuer, audience, iat, jti, and the back-channel logout event, and
rejects tokens containing nonce. A token with sid logs out sessions for
that provider session; without sid, sub logs out all sessions for that
subject. Under multi-identity, only the named provider's identity is removed
from a shared session. Valid requests return 200 with
Cache-Control: no-store, including when the session is already gone. Invalid
requests return 400.
Back-channel logout requires a session store with index support. All built-in
stores (InMemorySessionStore, RedisSessionStore, and
MemcacheSessionStore) support it. OIDCMixin raises immediately if the
configured store does not. Sessions created before upgrading are indexed on
their next request.
Logout tokens must be asymmetrically signed; unsigned tokens, HMAC signatures,
and encrypted logout tokens (JWE) are not supported. jti is required, but no
replay cache is maintained: repeating a valid logout request is idempotent.
backchannel_logout_leeway controls the allowed clock skew (60 seconds by
default). For a templated Microsoft Entra issuer, the logout token must carry
tid, just as for ID token validation.
Pushed authorization requests (PAR)
OIDCConfig.par controls RFC 9126 Pushed Authorization Requests:
'never'(default): the authorization request travels as query parameters, as before.'auto': push the request whenever the provider advertises apushed_authorization_request_endpoint; otherwise fall back to the plain redirect.'always': always push; login raises aRuntimeErrorif the provider does not support PAR.
OIDCConfig(..., par='auto')
A provider that advertises require_pushed_authorization_requests: true must be
configured with par='auto' or par='always'. With par='never' the login
fails fast with a clear error instead of issuing a redirect the provider would
reject.
The same parameters as the plain authorization request (PKCE challenge,
state, nonce, scopes, ...) are POSTed directly to the provider, and the
browser is redirected with only client_id and the single-use request_uri.
Request parameters therefore no longer travel through the user agent. PKCE,
state, and nonce are still used and protect against request URI swapping.
Client authentication follows the token endpoint: HTTP Basic when
client_secret is set, or none for public clients. PAR with other client
authentication methods (private_key_jwt, mTLS) is not supported.
Features
- Generic OIDC discovery
- Authorization Code Flow with PKCE (S256)
- ID token signature validation with JWKS
- Issuer validation against discovery metadata, including support for
Microsoft Entra ID
{tenantid}issuer templates - Refreshed ID token validation on token refresh
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) - Back-channel logout (
sidandsubsession invalidation) - Pushed Authorization Requests (RFC 9126) via
OIDCConfig.par - Multiple providers in one app via
OIDCProviders(or severalOIDCMixininstances) single-identity(default) ormulti-identitysession strategy- Composable with any other
KayaMixin(RSGI, MCP, etc.)
Microsoft Entra ID
Entra ID v2 ID tokens carry an iss claim of the form
https://login.microsoftonline.com/<tenant-id>/v2.0, while the discovery
metadata for the multi-tenant endpoints advertises the templated issuer
https://login.microsoftonline.com/{tenantid}/v2.0. kaya-oidc validates the
iss claim against the discovered metadata and substitutes the token's tid
claim into {tenantid}, so both tenant-specific and multi-tenant issuers work:
OIDCConfig(
issuer='https://login.microsoftonline.com/<tenant-id-or-domain>/v2.0',
client_id=os.environ['OIDC_CLIENT_ID'],
client_secret=os.environ['OIDC_CLIENT_SECRET'],
redirect_uri='https://example.com/auth/callback',
)
Use a tenant-specific issuer (the tenant GUID or a verified domain name) to
restrict logins to that tenant. The common and organizations aliases are
multi-tenant: any tenant in that cloud can obtain an ID token that passes
validation. If you build a multi-tenant application, check the tid claim
against the tenants you trust.
Security notes
- The
issclaim is validated against theissuervalue of the provider's discovery document, not againstOIDCConfig.issuer(they can differ, for example for Entra ID multi-tenant endpoints). - Refreshed ID tokens are validated before being stored; the
nonceis also checked when the refreshed token carries one. - 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.