Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
2.3.0 - 2026-07-08
Added
oauth21security profile (#68): opt-in draft-OAuth-2.1 protocol strictness alongsidedevandstricter-dev— PKCE required on the authorization code flow with S256 only (draft-ietf-oauth-v2-1 §4.1.1, §7.5.2), refresh token rotation forced on (§4.3.1), the password grant removed (RFC 6749 §5.2) and absent from discovery, and registered redirect URIs mandatory at/authorize. Protocol behavior lives in derivedSettingsproperties consumed by both the routes and the shared discovery builder, so the profile means the same thing from--profileorsettings.yamland discovery can never advertise what the endpoints refuse. Deliberately orthogonal tostricter-dev(runtime hardening).- Registered redirect URIs with exact matching (#67): clients gain an
optional
redirect_urislist; when non-empty,/authorizecompares the requestedredirect_uriwith simple string comparison (RFC 6749 §3.1.2.3, OAuth 2.1 §4.1.1) and answers a mismatch with400 invalid_requestdirectly — never by redirecting to the unvalidated URI (§3.1.2.4). Exposed in the web UI, MCP client tools and YAML. - Signed AuthnRequest verification (#69): with
saml.want_authn_requests_signed: trueand PEM certificates insaml.sp_certificates, nanoidp requires and verifies AuthnRequest signatures under both bindings — the HTTP-Redirect query-string signature over the raw transmitted fragment (SAML 2.0 Bindings §3.4.4.1; rsa-sha256/rsa-sha512/legacy rsa-sha1) and the HTTP-POST envelopedds:Signature(Core §5) — rejecting unsigned or invalid requests with 400, failing closed without registered certificates. The verified Redirect request is bound server-side in the session, so the inline-login leg only accepts byte-identical values. Metadata advertisesWantAuthnRequestsSigned="true"if and only if enforcement is on.examples/gen_sp_keypair.pygenerates a test SP keypair. - E2E workflow in CI (#79): every PR now boots real servers and runs
examples/test_agent.pyagainst them (default profile,--oauth21,--saml-signedwith a generated SP keypair) plus an MCP stdio smoke test (examples/mcp_smoke_test.py) driving the real transport — the regression guard for the class of bug where the stdio entrypoint crashed unnoticed because unit tests bypass it (#56). - Coverage gate in CI (#71, #72):
--cov-fail-under, introduced at 70 and ratcheted to 75 after the wizard went from 0% to 99% coverage; measured coverage 78%. The dead Codecov upload (never configured, failed silently since inception) was removed in favor of in-CI enforcement. - Documentation site: mdBook on GitHub Pages (https://cdelmonte-zg.github.io/nanoidp/) with getting-started, guides and a full reference; canonical docs are symlinked so there is a single source of truth, and the README became a landing page.
- Web UI parity (#94):
require_pkceandrefresh_token_rotationtoggles on the settings page; SP-certificates and signed-AuthnRequests fields (#69);redirect_urison the client form (#67); the dashboard badge distinguishes theoauth21profile. - MCP:
get_settingsreportssecurity_profile;update_settingscovers the SAML verification fields; client tools carryredirect_uris.
Changed
src/is fully annotated and mypy runs with a globaldisallow_untyped_defs(#70) — new unannotated code fails CI.- Internal architecture (behavior-invariant, #83–#86): one shared YAML
serialization path for
ConfigManagerand the UI writer; the token endpoint dispatches to per-grant handlers with device-flow and revocation state in dedicated services (DeviceCodeStore,RevocationStore); a singleaudit_eventhelper replaced 58 duplicated audit blocks (invariance proven by a before/after snapshot harness); the Pydantic models moved tomodels.pywith compatibility re-exports. security_profileis now read fromsettings.yaml(top-level key) and round-trips on save; the CLI--profilestill wins. A YAML-declaredstricter-devnow applies its runtime hardening (previously the YAML value was silently ignored).
Fixed
ConfigManager.save()was lossy (#87): the save path behind MCPsave_configrewrotesettings.yamlfrom scratch, silently deleting every section it didn't own —jwt(external keys!),session,logginglevels,server.debugand custom keys. Saving is now read-modify-write and preserves them, atomically and with a.bakbackup like the UI path always did.
2.2.0 - 2026-06-11
Added
- The refresh_token grant now re-issues an ID Token when the original grant
included the
openidscope (OIDC Core §12.2, #39). The granted scope is persisted in the refresh token claims and recovered on refresh; ascopeform parameter may narrow, but never broaden, the original grant (RFC 6749 §6 — broadening is rejected with400). The refreshed ID Token carries nononce(it binds the original authentication request). Refresh tokens minted before this change have no persisted scope and keep the old behavior. - ID Tokens now carry
auth_timeandat_hash(#42).auth_timereflects when the end-user actually authenticated: the login page for the authorization code flow, the/deviceverification for the device flow, the request itself for the password grant — and it is preserved unchanged across refreshes (OIDC Core §12.2), carried in the refresh token claims like the scope.at_hashbinds the ID Token to the access token issued alongside it (left half of SHA-256, base64url — §3.1.3.6). Discoveryclaims_supportednow also advertisesauth_time,nonceandat_hash. - Optional refresh token rotation (#46): with
oauth.refresh_token_rotation: true(default off), each refresh atomically invalidates the consumed refresh token, so its reuse fails with 401; reuse of a consumed token revokes its whole rotation family, including the live descendant (RFC 9700 §4.14.2). - PKCE enforcement (#47): new
require_pkcesetting (enabled by thestricter-devprofile, persisted insettings.yaml) rejects/authorizerequests without acode_challenge;stricter-devalso rejectscode_challenge_method=plain— explicit or implicit via the RFC 7636 §4.3 omitted-parameter default — and discovery only advertisesS256there. Unsupported methods are rejected at the authorization endpoint (§4.4.1). Default profile unchanged. - MCP audit & key tools (#48):
get_audit_log,get_audit_stats,clear_audit_log,get_keys_infoandrotate_keysmirror the HTTP API, so agent workflows can inspect what the IdP recorded and exercise JWKS refresh handling.clear_audit_log/rotate_keyscount as mutating tools (admin secret / readonly rules apply). MCPget_settings/update_settingsexpose the newrefresh_token_rotationandrequire_pkcesettings, andgenerate_tokenaccepts an optionalscopeargument and returns anid_tokenwhenopenidis included, matching the HTTP token endpoint. - CI now lints with ruff (#45) and type-checks
src/with mypy (#55, documented gradual-adoption baseline inpyproject.toml). The codebase is lint-clean — 153 findings fixed (#49): deprecateddatetime.utcnow()replaced (removes 80 DeprecationWarnings), unused imports/variables dropped, imports sorted and moved to module level,Optional[...]type hints in the crypto service,verify_jwtaccepts an array audience, exceptions re-raised withfrom e— and mypy-clean (40 baseline errors fixed; the baseline also surfaced the brokennanoidp-mcpentrypoint below).
Fixed
- The
nanoidp-mcpstdio entrypoint crashed at startup ("a coroutine was expected"):stdio_server()is an async context manager yielding the message streams, not a coroutine. Verified with a JSON-RPC initialize handshake. - Review follow-ups of the 2026-06-11 merge block (#56):
- Refresh tokens are bound to their client: the issuing
client_idis persisted in the refresh token claims and the refresh grant rejects any other client (RFC 9700 §4.14) — which also guarantees the refreshed ID Token keeps the originalaud(OIDC Core §12.2). Tokens minted before the claim existed keep working. - Rotation is atomic and revokes families on reuse: the revocation
check and the claim of the consumed token now happen in one critical
section, so two concurrent refreshes of the same token can no longer
both succeed. Each grant starts a refresh-token family (
rt_familyclaim, stable across rotations); reusing an already-consumed token revokes the whole family, including the live descendant (RFC 9700 §4.14.2). - PKCE
plaincan no longer slip through stricter-dev by omitting the method: per RFC 7636 §4.3 an absentcode_challenge_methoddefaults toplain; the method is now normalized before validation, and unknown methods are rejected at the authorization endpoint (§4.4.1). require_pkceis persisted: it is now read from and written tosettings.yaml(oauth section), soupdate_settings→save_config→reload_configno longer silently reverts it.- The token response reports the scope actually granted (RFC 6749
§5.1) instead of a hardcoded
"openid"; when no scope was involved the parameter is omitted, and a narrowed refresh reports the narrowed scope.
- Refresh tokens are bound to their client: the issuing
- The token endpoint validates
expandextrabefore the grant dispatch: with rotation enabled, a malformed value can no longer consume the refresh token without delivering its replacement (the last tradeoff noted in the #56 review). Validation is semantic, not just syntactic:extramust be a JSON object (a scalar/array used to raise aTypeError500 later) andexpmust be an integer within the same1..1440bounds the Settings model enforces (non-numeric values used to be an unhandledValueError500; astronomical ones anOverflowError500). - Thread-safety hardening for shared in-memory state (#43): the authorization code store now performs its check-then-mark sequence under a lock (one-time use can no longer be defeated by concurrent redemptions), device codes are claimed/transitioned atomically and pruned when expired, and the lazily-created service singletons (config, token, crypto, audit, auth codes) use double-checked locking so concurrent first access creates exactly one instance.
- The MCP
get_oidc_discoverytool now returns the exact same document as the HTTP/.well-known/openid-configurationendpoint (#40). Both build it via a new shared helper (services.discovery.build_discovery_document), so the MCP tool now advertisesclaims_supported(includingazp),response_types_supported,id_token_signing_alg_values_supported,code_challenge_methods_supportedand the endpoint auth methods — and the two documents can no longer drift apart. - Discovery no longer advertises the
tokenresponse type (#41): the implicit flow was never implemented (/authorizeonly acceptsresponse_type=code) and is deprecated by the OAuth 2.0 Security BCP, so advertising it misled clients.response_types_supportedis now["code"].
Documentation
- The MCP tools tables in the README and
docs/MCP_WORKFLOW.mdnow list all 24 tools (#44, #48); the README was missingcreate_client,update_client,delete_client,update_user,update_settingsandsave_config.
2.1.0 - 2026-05-26
Added
- ID Tokens are now issued for the password and device (RFC 8628) grants
when
openidscope is requested, not justauthorization_code(#36). These grants authenticate an end-user, so an ID Token is meaningful;client_credentialsstill never emits one (no end-user).
Fixed
- Friendlier loading of client
additional_audiencesfromsettings.yaml(#35): a scalar value (additional_audiences: api://x) is coerced to a one-element list, and an unsupported shape (e.g. a non-string item) now fails with a clear, client-scoped error instead of an opaque PydanticValidationErrorat startup. - Minor hardening/polish from the #32 review (#37):
OAuthClientnow validates on direct attribute assignment (validate_assignment), discovery advertisesazpinclaims_supported, and the MCP_normalize_audiencesrejects falsy non-list inputs instead of silently returning an empty list.
Security
- Harden the ID Token vs access-token boundary (#34). The resource audience
(
oauth.audience) is now filtered out of the ID Tokenaudeven if a client lists it inadditional_audiences, and every token carries atoken_usemarker (access/id/refresh)./userinforejects tokens marked as ID or refresh tokens and/introspectreports ID Tokens as inactive, so an ID Token can no longer be spent as an access token. (Refresh tokens stay introspectable per RFC 7662.)
2.0.0 - 2026-05-25
Changed
- ID Token
audnow contains the requesting client'sclient_id, as required by OpenID Connect Core 1.0 §2 (was previously the staticoauth.audience). This makes it possible to test multiple clients and brings nanoidp in line with the OIDC spec.- Breaking: relying parties that validated the ID Token
audagainst the old staticoauth.audiencevalue must now expect their ownclient_id. - The access token
audis unchanged and still reflectsoauth.audience(the resource audience, per RFC 9068 §2.2).
- Breaking: relying parties that validated the ID Token
Added
additional_audiencesper-client setting: extra audiences appended to the ID Tokenaud. If this produces more than one distinct audience value,audis emitted as an array and nanoidp also emitsazpequal to theclient_id, so clients can test authorized-party handling.
1.4.0 - 2026-04-28
Added
- Environment variable substitution in
settings.yamlusing${NAME}/${NAME:default}syntax PORTenv var honoured in the Docker image via shell expansion inCMD
1.3.3 - 2026-04-22
Fixed
- Return
id_tokenin /token response for Authorization Code Flow whenopenidscope is requested, as required by OIDC Core spec (Section 3.1.3.3) - Include
nonceclaim inid_tokenwhen provided by the client
Changed
- Use
pyproject.tomlas single source of truth for version number - Remove outdated version label from Dockerfile
1.3.2 - 2026-03-27
Fixed
- Token endpoint now rejects requests when
client_idcannot be determined from either the request body or theAuthorizationheader - Token endpoint now rejects requests where
client_idin the body conflicts with the authenticated client in theAuthorizationheader
Added
- Tests for client_id mismatch and missing client_id edge cases
1.3.1 - 2026-03-26
Fixed
- Allow authorization code flow without
Authorizationheader for PKCE public clients (RFC 6749 §2.1)- Libraries like authlib send
client_idin the request body instead of the header when no client secret exists - Auth header validation is now only enforced for grant types other than
authorization_code
- Libraries like authlib send
Added
- Test for PKCE plain flow without auth header (
test_pkce_plain_flow_no_auth_header)
1.3.0 - 2026-03-25
Added
- GitHub Actions workflow to build and publish Docker images to GitHub Container Registry (GHCR)
- Triggered on version tags (
v*), builds multi-platform images (linux/amd64,linux/arm64) latesttag published only for non-prerelease versions
- Triggered on version tags (
- Docker usage instructions in README (
docker pullanddocker runexamples)
Changed
- Dockerfile healthcheck switched from Python
urllibtocurlfor Podman compatibility and reduced overhead - Updated
actions/checkoutfrom v4 to v6 in publish workflow
1.2.3 - 2026-03-03
Fixed
- Dockerfile and docker-compose.yml: replaced
curlwith Python'surllibfor healthcheck — avoids addingcurlas a system dependency in the image
Docs
- Added mascotte/logo images to the project
1.2.2 - 2026-01-19
Added
- New
strict_saml_bindingsetting to enforce SAML 2.0 binding compliance- When
false(default): lenient mode accepts GET with uncompressed data (useful for debugging) - When
true: strict mode rejects non-compliant requests per SAML spec
- When
- Setting exposed in UI (Settings page), REST API (
/api/config), and MCP server - Exclusive C14N (
exc_c14n) is now the default XML canonicalization algorithm- Standard for SAML 2.0 signatures, handles namespace isolation correctly
- Available algorithms:
exc_c14n(Exclusive C14N 1.0, default),c14n(C14N 1.0),c14n11(C14N 1.1)
- UI select dropdown for C14N algorithm in Settings page
strict_saml_bindingandverbose_loggingnow persist correctly on save/reload- Comprehensive E2E test coverage for all SAML flows in
test_agent.py:test_saml_metadata_bindings- verifies both HTTP-POST and HTTP-Redirect advertisedtest_saml_sso_post_binding- SP-initiated SSO with HTTP-POST (InResponseTo verification)test_saml_sso_redirect_binding- SP-initiated SSO with HTTP-Redirect (InResponseTo verification)test_saml_idp_initiated_not_supported- documents IdP-initiated SSO is not supportedtest_saml_strict_binding_mode- tests strict/lenient binding behaviortest_saml_attribute_query_verification- verifies actual attributes returned
- Unit tests for inline login flow (
test_inline_login_flow_preserves_post/redirect_binding) - Unit test for strict mode + inline login (
test_strict_mode_inline_login_preserves_redirect_binding) - Unit test for Exclusive C14N configuration (
test_c14n_algorithm_configurable_to_exclusive)
Fixed
- SAML SSO now correctly handles both HTTP-POST and HTTP-Redirect bindings
- Parser always tries DEFLATE decompression first, falls back to raw XML (handles all edge cases)
- Strict mode now works with inline login by passing original HTTP verb via hidden field
- Fixes: GET compressed → login form → POST would fail in strict mode
- Stateless: no server-side session needed, works in CI/CD pipelines
- Explicit
|eescape filter in login template hidden fields (XSS defense-in-depth) - Normalized
original_verbhandling (uppercase, validated to GET/POST) - Quick-fill username buttons use
tojsonfilter to handle special characters safely
1.2.1 - 2026-01-16
Fixed
- SAML SSO now correctly handles HTTP-POST binding (uncompressed SAMLRequest)
- Previously,
_parse_saml_requestunconditionally attempted DEFLATE decompression, causing parsing to fail for POST requests - Now uses HTTP method to determine binding type: GET = HTTP-Redirect (compressed), POST = HTTP-POST (uncompressed)
Changed
- E2E test agent now verifies actual SAML parsing (InResponseTo matching) instead of just endpoint availability
- Added separate tests for HTTP-POST and HTTP-Redirect bindings in
test_agent.py
Changed (Architecture)
- Inline login for SAML SSO:
/saml/ssonow shows login form directly instead of redirecting to/login- This preserves SAML binding context naturally (no redirect = no method change)
- Follows the pattern used by Keycloak and other IdPs
- Removes the complex edge cases caused by redirect-based login
/loginendpoint simplified - now only used for direct web UI access, not SAML flows- Login form now posts to current URL (no hardcoded action) - works for both
/loginand/saml/sso
Changed
- SAML metadata now advertises both HTTP-POST and HTTP-Redirect bindings for SingleSignOnService
- Audit stats now track SAML SSO and Attribute Query separately (
saml_sso_requests,saml_attribute_queries) - Dashboard shows combined SAML total with SSO/AttrQuery breakdown
- E2E test agent expanded to 35 tests (was 28), now covering all SAML flows with parsing verification
1.2.0 - 2026-01-14
Added
- Configurable
verbose_loggingsetting to control sensitive data in logs verbose_loggingexposed in MCPget_settingsandupdate_settingstoolslogging.verbose_loggingexposed in REST API/api/configendpoint- MCP tests (
tests/test_mcp.py) with 8 tests for MCP functionality - Verbose logging test in E2E test agent
Changed
- Replaced deprecated
defusedxml.lxmlwith native lxml secure parser for XXE protection - Added
html.escapefor XSS prevention in SAML responses - Audit logging now respects
verbose_loggingsetting (usernames/client_ids only when enabled)
Security
- XXE (XML External Entity) protection using secure lxml parser configuration
- XSS prevention in SAML response forms
- Configurable sensitive data logging (verbose_logging defaults to true for dev convenience)
1.1.1 - 2026-01-14
Added
- Configurable XML canonicalization algorithm via
saml.c14n_algorithmsetting
1.1.0 - 2026-01-14
Added
- Configurable SAML response signing via
saml.sign_responsessetting - UI toggle for SAML signing in Settings page (
/settings) sign_responsesexposed in/api/configendpoint- Test agent (
examples/test_agent.py) for comprehensive endpoint testing
Changed
- SAML SSO and AttributeQuery endpoints now respect
sign_responsesconfiguration - Changed default XML canonicalization to C14N 1.0 for maximum compatibility
- Updated documentation with SAML signing configuration instructions
1.0.0 - 2025-12-04
Added
- Initial release
- OAuth2/OIDC support (Authorization Code, Password, Client Credentials, Refresh Token, Device Flow)
- PKCE support (S256 and plain methods)
- Token Introspection (RFC 7662) and Revocation (RFC 7009)
- OIDC Logout / End Session endpoint
- Device Authorization Grant (RFC 8628)
- SAML 2.0 SSO and AttributeQuery endpoints with signed assertions
- MCP Server integration for Claude Code
- Web UI for configuration (users, clients, settings, keys, audit log)
- YAML-based configuration
- Attribute-based access control with configurable authority prefixes
- Audit logging
- Docker support
- Security profiles (
devandstricter-dev) - Key rotation with JWKS support for multiple keys
- External key import support