Skip to content

v0.4.0 — Security hardening

Choose a tag to compare

@jpmanson jpmanson released this 12 Oct 13:01
· 12 commits to master since this release
79ad0f4

Security hardening

EasyOIDC 0.4.0 closes gaps that every Keycloak-backed app had to patch by hand: PKCE, ID token validation, client roles, Keycloak 18+ logout, token refresh, and the OAuth state being reused as the session id.

uv add "easyoidc>=0.4.0"     # pulls joserfc (already required by Authlib 1.7+)

The public API is backwards compatible — existing code runs unchanged. Some defaults change (PKCE, nonce, ID token validation); each one can be turned off per Config. Please read Behaviour changes before upgrading.

Access policy and Keycloak client roles

auth = FastAPIOIDClient(
    app,
    auth_config=Config('.env', roles_source='client'),   # resource_access.<client_id>.roles
    session_storage=SessionHandler(mode='redis'),
    required_roles=['my-app-user', 'my-app-admin'],      # at least one
    require_email_verified=True,
    validate_session_on_each_request=False,              # trust the store, refresh on expiry
)

Available on the FastAPI, Flask, NiceGUI and Taipy clients. A user who authenticates but fails the policy gets a 403 page with an IdP logout link (access_denied_handler= to customise). Being authenticated in a realm with self-registration no longer means "allowed".

Added

  • PKCE S256 on the authorization code flow (use_pkce, default on).
  • Nonce + ID token validation (use_nonce, validate_id_token, default on): signature against the provider JWKS, iss, aud, exp, nonce, azp, sub consistency with userinfo.
  • OIDClient.begin_login() / complete_login() for low-level integrations.
  • required_roles, require_email_verified, access_denied_handler, auth_error_handler.
  • roles_source (realm / client / realm+client), roles_claims_from (userinfo / id_token / access_token), roles_client_id. roles_getter= now also on Flask and NiceGUI.
  • Token refresh: expired access tokens are refreshed and persisted; userinfo and the policy are re-checked, so a role removed in the IdP takes effect at the next refresh.
  • ?next=/path on the login route (local paths only), get_id_claims(), http_timeout (10 s), login_timeout_seconds (600 s).
  • AccessDeniedError, IDTokenValidationError, __version__ exported from EasyOIDC.

Fixed

  • Keycloak 18+ logout: client_id always sent; id_token_hint URL-encoded and omitted when absent (it was the literal string None).
  • IdP callback errors (?error=...) render an error page instead of a redirect loop to /login.
  • Open redirect on the post-login redirect.
  • The cookie now carries a random session id, rotated after login, instead of the OAuth state (which travels in URLs and logs).
  • Flask: crashes on get_user_roles() / is_authenticated() / get_userinfo() for anonymous users; referrer cookie is now HttpOnly / SameSite=Lax.
  • NiceGUI: blocking OIDC calls moved to a threadpool.
  • StreamlitODICClient did not inherit from OIDClient.
  • TaipyOIDClient overwrote unrestricted_routes and crashed with auth_config=None.
  • token_revoke() without a refresh token; dump_configuration() leaking cookie_secret_key.

Behaviour changes (read before upgrading)

Change Who is affected Keep 0.3.x behaviour
PKCE S256 sent by default Providers that reject code_challenge (rare; Keycloak, Auth0, Google, Entra accept it) use_pkce=False
nonce sent and checked Providers that don't echo nonce in the ID token use_nonce=False
ID token validated Providers whose iss/aud don't match discovery / client_id, or that omit id_token with the openid scope validate_id_token=False
Cookie holds a random session id, not the OAuth state Code that used session['easyoidc-session-state'] as the OAuth state Use get_userinfo() / get_current_token()
Store entries gain login, id_claims, roles keys Code comparing store entries for equality Read the keys you need. 0.3.x sessions survive the upgrade.
?error= callback returns HTTP 400 instead of redirecting to /login Apps relying on the redirect auth_error_handler=
403 for users without email_verified / required role Only if you opt in Don't pass required_roles / require_email_verified

Unchanged: default roles source (realm_access.roles from userinfo), route names, SESSION_STATE_VAR_NAME, require_roles, validate_session_on_each_request=True, SessionHandler.

auth_server_login() / get_token() are kept and do not add PKCE or nonce (they can't return the per-login secrets). Low-level integrations should move to begin_login() / complete_login().

Upgrade checklist

  1. uv add "easyoidc>=0.4.0".
  2. Keycloak clients with Full scope allowed off: set roles_source='client' (add roles_claims_from='userinfo,access_token' if the client roles mapper only targets the access token).
  3. Replace hand-written "check role / email_verified after login" code with required_roles= / require_email_verified=.
  4. Remove subclasses that only added PKCE, ?next= or a logout fix.
  5. Test login and logout once against your IdP. On an ID token error, check issuer and client_id first.

Full details: CHANGELOG.md · PR #6