All notable changes to this project will be documented in this file.
- Contract:
AuthScheme::Saml("sa") andSamlParamsinRealmAuthParams.saml_params, mirrored in admin-ui types andopenapi.yaml.SamlParams.sp_signing_keydoes not exist: the key is server configuration only. - Feature gate: optional, off-by-default
samlCargo feature (samael); realm requests carryingsaml_paramsget400on builds without it. SAML is never an administrator credential: no admin is found byAuthScheme::Samland SAML on realm_is refused. - Endpoints (under
/saml, only when[saml_sp_params]is set, rate limited like/login):GET /saml/{realm_id}/login(checksreturn_toagainst the realm origin allowlist, stores the pending request for 10 min, redirects with an RSA-SHA256-signedAuthnRequest),POST /saml/{realm_id}/acs(_ea_samlcookie must matchRelayStateagainst login CSRF; issues the_ea_session and returns via a same-site page to keep theSameSite=Strictcookie) andGET /saml/{realm_id}/metadata. - Realm validation: IdP entityID, HTTPS Redirect SSO URL, signing certificates and NameID format are always derived from the pasted metadata. Expired, oversized or multi-entity metadata, a wrong ACS URL, return URLs outside the https allowlist, bad claim names and transient NameIDs without a subject attribute get a field-named
400. - SP key:
[saml_sp_params](RSA key ≥ 2048 bits and matching certificate PEM paths) is checked at startup and required to enable SAML. - Response validation: samael checks signature,
InResponseTo, issuer, validity window, recipient, destination and status. We add: IdP certificate required,AudienceRestrictionnaming our entity ID andAuthnStatementrequired, RSA/ECDSA with SHA-256/384/512 only, replay rejection (5 min afterIssueInstant, 3 min skew). Failures return401AuthError::Saml. - Identity mapping: subject is the configured attribute (exactly one non-empty value) or the NameID (transient refused), optionally lowercased; roles come from the role attribute; only
attribute_claim_mapattributes become extra claims, within the 4 KiB budget. SamlRequestStore: pendingAuthnRequests and a per-realm replay cache on SQLite, PostgreSQL, MySQL and Redis; atomic single-use, expired entries refused, exact-match IDs on MySQL, periodic purge on the stale-session collector's interval.- Admin UI: SAML realm settings with in-browser metadata parsing and preview, copyable SP entity ID/ACS URL, SP metadata download, per-field checks and server-error mapping; realm cards show a SAML tag and flag IdP certificates expiring within 30 days.
saml_paramsis optional inRealmAuthParams, restoring the type-check. Fixed the realm drawer enabling TOTP when saving a realm whosetotp_paramsthe server omitted. - Packaging: static OpenSSL-only xmlsec 1.3.5 / libxml2 2.13.4 (
nix/xmlsec-static.nix); separateauth-verifier-static-samlanddocker-image-samlNix builds (default artifacts unchanged);--variant default|samlonnix.sh, packaging scripts and mise tasks (outputresult-<format>-static-saml/,-samlsuffix;--variant saml --link dynamicrejected); the DMG script skipsresult-*dirs. - Helm chart
charts/cosmian-auth-verifier: rendersauth_verifier.toml, injects DB/session-store URLs from Secrets, mounts TLS/JWT/certificate/SAML key Secrets, rejects settings the server would refuse, and switches to theauth-verifier-samlimage whensaml.enabled. - CI:
cargo-samlNix job (clippy and tests with--features saml) on Linux x86_64/aarch64 and macOS, with<nixpkgs>pinned so macOS uses a Nix bash;static-samlpackages (deb, rpm, dmg) built and published; image pushed asghcr.io/cosmian/auth-verifier-saml; package tests start with[saml_sp_params](test_docker_image.shchecks it whenEXPECT_SAML=1); newSAML e2eworkflow (Keycloak) on Linux x86_64/aarch64, weekly and on SAML-related pushes/PRs, uploading Playwright traces on failure. - Tests: request-store suite (in-memory SQLite by default; PostgreSQL/MySQL/Redis via
TEST_SESSIONS_STORE), xmlsec smoke test, settings-validation, response-validation (one broken property at a time) and identity-mapping tests, HTTPS end-to-end tests (signed redirect, login, replay, login-CSRF, return-URL allowlist, SP metadata, cross-realm ACS), a fake signing IdP helper (tests::saml_idp), admin UI tests with a Playwright realm-creation scenario, and real-Keycloak e2e (mise run test:saml-e2e, Keycloak 26.4 digest-pinned, newe2e/package). - Docs:
docs/saml.mdandserver/saml_testing.md; SAML added as flow 8 and to the config, flows, API and client references, overview and README; commented[saml_sp_params]in packaged/dev configs;/samlendpoints inopenapi.yamland proxied in admin-ui dev/preview servers; fixed broken Mermaid diagrams; ADR-0003 (samaelwith bundled xmlsec).
- Claim model: a realm is unclaimed until it has its first admin and claimed afterwards. Not persisted: recomputed on every request from the admins'
realmslists, so removing a realm's last admin immediately restores super-admin access. - Access rule: a direct member of a realm always has access. A super admin who is not a member has access only while the realm is unclaimed (bootstrap). Applies to
/realms/{realm}/…(userpass, TOTP),/sessions/…,PUT /admins/realms/{id}, and admin CRUD whose targetrealmsinclude the realm. - Permanent super-admin exemptions (safety net against orphaned realms):
GET /admins/realms,GET /admins/realms/{id}andDELETE /admins/realms/{id}. A realm's own admins may delete it too. Realm creation and first-admin bootstrap stay super-admin-only. - Admin CRUD: the exclusive-ownership rule is claim-aware (every realm in the target's
realmsmust pass the check;PUTchecks current state and incoming body); an admin with an emptyrealmslist is manageable by super admins only. - Behaviour changes: realm admins can now
PUTtheir own realm's config (was super-admin-only); the userpass list is filtered to realms the requester can manage, using onelist_admins()call per request. - Code: new
can_manage_realm,can_manage_admin_realmsandrealm_manageable_given_claimsinserver/src/server/endpoints/mod.rs.Admin::can_administer_realmstays in the client crate as a static, DB-less UI hint, not an authorization decision. - Docs and tests:
authorization_and_administration.mdgains a Realm-Claim Lifecycle section and updated diagrams;openapi.yamlupdated; admin, super-admin and TOTP API tests rewritten for the claimed/unclaimed matrix.
- Admin scopes (
/whoami,/certify,/sessions/*,/admins/*,/admins/realms/*,/auth/approle/*,/auth/kubernetes/*) now sendAccess-Control-Allow-Credentialsfor configuredallowed_origins(build_admin_cors()was missing.supports_credentials()), so cross-origin admin UIs can read cookie-authenticated responses. Origins are always explicit, never*./loginwas unaffected.
misetask runner (mise.toml,.mise/): thin wrapper over.github/scripts/nix.shplusui:*admin-ui tasks; pins the admin-ui Node/pnpm toolchain while Rust stays inrust-toolchain.tomland Nix stays the build engine. Packaging workflows usemise run …viajdx/mise-action@v2, pinned to mise2026.9.2(2026.9.3binaries were unpublished, giving404). Documented inAGENTS.md,.github/copilot-instructions.mdandadmin-ui/DEV_GUIDE.md.- Dependabot: admin-ui npm minor/patch and security updates grouped into single PRs; PRs labelled
frontendinstead of the nonexistentadmin-ui. - Lockfile repairs: restored the
overridespins (ws,esbuild,js-yaml,postcss) that Dependabot dropped fromadmin-ui/pnpm-lock.yaml(every Admin UI CI job failed withERR_PNPM_LOCKFILE_CONFIG_MISMATCH); worked around pnpm losing them in the Nix sandbox (nix/admin-ui.nix); regenerated the staleadmin-ui.pnpm.*Nix hashes, now documented inAGENTS.md, the expected-hashes README and the admin-ui agent instructions. - Pre-commit: new
admin-ui-lockfilehook (frozen, lockfile-onlypnpm install);pnpm-lock.yamlexcluded fromyamlfmt; nested list in the admin-ui agent instructions flattened (markdownlint and prettier disagreed); Helm templates excluded fromcheck-yaml/yamlfmt. - Security:
rustls0.23.45 → 0.23.46 for RUSTSEC-2026-0349 and RUSTSEC-2026-0348 (the latter not reachable,brotlifeature off);spinmoved off yanked 0.9.8; RUSTSEC-2026-0285 patched and unused source orgs removed fromdeny.toml. - Admin UI tests: fixed the
window is not definedfailure by no-op'ing antd staticmessage.*toasts in test setup and keeping a 50 ms teardown settle.
- ADR-0001/0002 moved to
server/documentation/adr/, outside the public mdBook; navigation links and code comments cleaned up.
POST /certify: a new authenticated (session-cookie) endpoint that certifies a caller-supplied verification key under the caller's ownrealm_id/sub/auth_scheme, returning a long-lived, ES256-signed certificate. It is signed with a dedicated certificate signing key (certificate_jwt_params), entirely separate from the session JWT key, so it can never be presented back as a session cookie/token even if algorithms collide.- Added
GET /.well-known/certificate-jwks.json, a JWKS document for the new certificate signing key, kept separate from the existing session/.well-known/jwks.jsonfor the same isolation reason. - Added a per-realm
certificate_max_age_secondsfield controlling how long issued certificates remain valid (defaults to one year), following the same pattern as the existingsession_max_age_seconds. POST /login's per-IP rate limiter is now configurable viaServerParams::login_rate_limit_per_second/login_rate_limit_burst(both optional, default to the previous hardcoded values: 5 req/s, burst of 10).UserPassgainedextra_claims, arbitrary claims a realm admin sets at enrollment that are merged into the session JWT on username/password login (rejected with400on collision with a typed claim or if serialized size exceeds 4 KiB), andPOST /certifygained aclaimsfield (plusexclude_sub) to selectively copy a session's extra claims into an isolated, separately-signed certificate.UserPassgainedpassword_input, aPasswordInputenum (Plaintext(String)orHashed(String)) letting a caller provision a credential either from a plaintext password or from an already-computed Argon2id PHC string without ever sending its plaintext to this server; the latter is rejected with400unless it uses exactly this server's own algorithm/version/cost parameters.CredentialModal(create mode): added a plaintext/pre-hashed password toggle (PasswordFields) so an admin can provision a credential from an already-computed Argon2 PHC string instead of a plaintext password, and a key/value extra-claims editor (ExtraClaimsEditor) forUserPass.extra_claims, matching the new server-sidepassword_input/extra_claimsfields.
POST /admins,POST /admins/realms, andPOST /realms/{realm_id}/userpassused to leak a raw500with database internals on a duplicate ID/username; all three now return a clean409 Conflict(theuserpasscase unconditionally, since distinguishing a genuine conflict from a byte-for-byte resubmission would require checking the submitted password against the stored hash on this unrate-limited, admin-authenticated endpoint, turning it into a password-guessing oracle).CredentialModal's roles-fetch effect had no unmount guard: if thelist()call resolved after the modal/component was torn down, the resultingsetAvailableRolescall could fire against a dead environment. Added the samecancelledguard already used by the form-validity effect right below it.- Column migrations (e.g.
userpass.roles) now use the same atomicADD COLUMN IF NOT EXISTSon PostgreSQL as other columns instead of aSELECT-then-ALTERcheck missing a schema filter, and all three backends (PostgreSQL, MySQL, SQLite) now propagate a failure of that check instead of silently treating it as "column missing". - Plaintext passwords are now wrapped in
Zeroizingthrough the HTTP Basic Auth extraction path and theuserpasscreate/update handlers, reducing how long they remain in cleartext in application-held memory. - Raised the server's Argon2id parameters to RFC 9106 §4's recommended
t=3, m=64 MiB (65536), p=4(wast=3, m=4 MiB (4096), p=1, a wrongly-documented value inherited from the crate's own default) and now pin/validate the algorithm version (v=19) in addition to the variant and cost parameters when accepting a pre-hashedpassword_input: { hashed: ... }. - Bumped
h2from 0.4.13 to 0.4.16 (hyper/reqwest instance) to fixRUSTSEC-2026-0258(unbounded empty DATA frames, low severity); the other instance, pulled in transitively viaactix-http3.13.1 (pinned toh2 ^0.3, no patched 0.3.x release exists), is now explicitly ignored indeny.toml. Also removed a staleRUSTSEC-2023-0071ignore entry. - Bumped eight
admin-uidependencies flagged by Dependabot to pick up upstream fixes:antd,react-router,@testing-library/react,@types/react-dom,@types/node,eslint,eslint-plugin-react-refresh, andtypescript-eslint.
- Removed
public_key_pemfromPOST /loginand theas_pkprivate claim it populated on the session JWT: this was dead weight (no caller ever set it) and the wrong shape for the job — VELO, the intended consumer, now uses/certifyinstead, which produces a purpose-built, long-lived, cryptographically isolated certificate rather than piggybacking on the short-lived session token. UserPass.password/hashed_password(two separate, manually-mutually-exclusive fields) replaced by a singlepassword_input: Option<PasswordInput>field, enforcing the plaintext-vs-pre-hashed exclusivity at the type level instead of at runtime — a request providing both is now structurally impossible rather than a400caught after the fact.- Renamed
UserPass.passwordtopassword_hashand changed its type fromVec<u8>toString: the stored value has always been the full Argon2id PHC string, so the old name/type obscured what it actually holds and forced an unnecessary byte/string round-trip. The three database backends still store it as a binary column (BYTEA/BLOB, unchanged — no migration needed) and convert at the Rust boundary.
- Added
certify_tests.rs: session requirement, empty-key rejection (400), missing server configuration (500), certificate JWKS availability, and cryptographic isolation (a certificate signed with the certificate key is rejected by the session JWT decoding key). - Added a userpass duplicate-creation regression test and renamed a misleading pre-existing duplicate-admin test for clarity.
- Fixed a flaky
admin-uiunit test (window is not defined,RealmContext.test.tsx) by guardingRealmContext'sfetchRealmsagainst post-unmountsetStateand extending the scheduler-drain loop inafterEach.
- Added
SECURITY.md, a hand-maintained security policy and vulnerability-disclosure ledger, so the auth server has the same advisory-tracking surface as the KMS repository. - Documented the advisory-ledger lifecycle in AGENTS.md §11 and mirrored a short pointer in
.github/copilot-instructions.md, defining theCOSMIAN-AUTH-<YYYY>-NNNID scheme, the released-vs-unreleased rule, and the three-part internal-consistency requirement.
- Regenerated the Nix
admin-uipnpm dependencies hash (nix/expected-hashes/admin-ui.pnpm.*.sha256) for all three platforms after several Dependabot bumps toadmin-ui/pnpm-lock.yamlwent unmatched by a hash regeneration, breaking the NixPackagingCI job; also corrected a staledarwinhash. - Fixed the
admin-uipackage-update pipeline: regeneratedpnpm-lock.yamlto record thepnpm.overridesblock (was causingERR_PNPM_LOCKFILE_CONFIG_MISMATCH), added a browserUser-Agentto Nix'sfetchurl/importCargoLockcalls to stop crates.io HTTP 403s, and pinnednpm_config_node_version=22.12.0so pnpm's engine check picks up the@rolldown/binding-linux-x64-gnuoptional dependency.
- Recorded previously shipped and fixed vulnerabilities in
SECURITY.md: COSMIAN-AUTH-2026-001 and COSMIAN-AUTH-2026-002 (plaintext password storage via thecreate_userpass/update_userpassendpoints), COSMIAN-AUTH-2026-003 (TLS private key baked into the Docker image), and COSMIAN-AUTH-2026-004 (vulnerable admin UI transitive dependencies).
- AppRole / Kubernetes / Token machine-auth API (SPIRE/SPIFFE support): new database tables (
app_tokens,approle_roles,app_secret_ids,k8s_roles); endpoints under/auth/approle/,/auth/kubernetes/, and/auth/token/; tokens stored asSHA-256(hvs.<random>), never persisted raw; token-validation middleware inserts authenticated entity into request extensions. - Kubernetes JWT validation: explicit algorithm allowlist (
RS256/RS384/RS512/ES256/ES384),nbfclaim validation,kid-based key pre-selection per RFC 7517 §4.5. - Machine Credentials admin UI page (AppRole, Kubernetes, token self-service tabs) backed by new admin read endpoints; client DTOs and
openapi.yamlupdated. - Configurable log level via optional
[log]section in server TOML (level, defaults to"info", overridable byRUST_LOG). - Admin-ui Nix derivation (
nix/admin-ui.nix) usingpnpm_9.fetchDepsfor hermetic builds; static assets bundled into Docker image (/srv/admin-ui/) and DEB/RPM packages (/usr/share/auth_verifier/admin-ui/). - Docker image entrypoint generates self-signed TLS certificates at runtime; no private key baked into the image.
- Admin userpass auto-link: server now auto-links/unlinks
admin.userpassonPOST/DELETE /realms/_/userpass; admin UI gained a "Credential" button and optional password fields in the "New Admin" drawer. create_userpass/update_userpassnow hash passwords before storage: plaintext bytes were previously stored verbatim, breaking all logins for API-created credentials; emptypassword: []on update preserves the existing hash.- TOCTOU race in SQLite secret-ID consumption:
consume_secret_idnow usesBEGIN IMMEDIATE+rows_affected()check to prevent two concurrent AppRole logins both succeeding against a single-use secret ID. - Fixed
AppRoleLoginRequest.secret_idas requiredString(brokebind_secret_id: falselogin); changed toOption<String>with#[serde(default)]. - Removed fragile RS256 fallback loop in
validate_k8s_jwtthat silently accepted weak-algorithm tokens. - Test harness: replaced fixed
AtomicU16port counter with OS-assigned ephemeral ports; changedhost_nameto"127.0.0.1"; addedDroponTestsContextto stop server on panic. - Resolved three Dependabot security advisories via pnpm overrides (
js-yaml@5.2.2,postcss@8.5.25,brace-expansion@5.0.9); migrated toreact-routerv8.3.0 (GHSA-qwww-vcr4-c8h2); bumpedreact/react-domto 19.2.8. - Miscellaneous: fixed clippy
unnecessary_unwrap, packagingchown root:root, admin-uichmod -R u+wbefore cleanup,/etc/cosmian/dev/creation moved tofakeRootCommands.
auth_verifier.servicenow runs asroot, usesStateDirectory=cosmian-authandWorkingDirectory=/var/lib/cosmian-auth;postinstsimplified (no cosmian user/group creation).- Docker image default port changed from
8443to8080.
- 17 integration tests (
app_auth_tests.rs) covering AppRole, Kubernetes, and token self-service workflows. - 13 database unit tests for all new machine-auth tables.
- Regression tests for password-hashing fix (
test_create/update_userpass_then_authenticate); test helpers updated to send plaintext bytes.
server/documentation/restructured as an mdBook (pages underdocs/,nav.yml,book.toml,SUMMARY.md,doc-themesubmodule).- Authentication flows doc extended to cover AppRole (Flow 5), Kubernetes (Flow 6), and token self-service (Flow 7).
- OpenAPI fixes:
secret_idremoved fromAppRoleLoginRequest.required;expected_issuer/bound_audiencesadded toK8sRoleRequest;/v1/auth/paths corrected to/auth/.
- Added
.github/dependabot.ymlfor weekly admin-ui npm updates. - Added
verify_running_ui.sh, admin-UI verification steps inpackaging-tests.yml, and asset checks in DEB/RPM smoke tests. - Added
build_admin_ui()topackage_common.sh; updated pnpm store hashes for all platforms; improvedtest_docker_image.sh.
- Ephemeral TLS certificate generation at container startup removes the private key that was previously baked into the Docker image.
-
create_userpassendpoint now hashes the password before storage: previously the plaintext password bytes sent by the client were stored verbatim in the database, whilevalidate_userpasscompared the stored value against an Argon2 hash of the incoming password — causing all username/password logins for API-created credentials to fail. The endpoint now callshash_password_with_argon2before passing the record to the database layer. -
update_userpassendpoint now hashes the new password before storage: same root cause as above. If the client sends non-empty password bytes (password reset), they are hashed before storage. If the client sendspassword: [](roles/flags-only update, the common case since GET always returns an empty password field), the existing hash is preserved so that authentication continues to work.
-
Added
test_create_userpass_then_authenticateregression test: creates credentials via the HTTP endpoint with plaintext password bytes and asserts that a subsequent login with the same password succeeds. -
Added
test_update_userpass_then_authenticateregression test: covers password update (old password rejected, new password accepted) and roles-only update (empty password field preserves existing hash and authentication continues to work). -
Updated all test helpers that built
UserPassstructs for HTTP API calls (helpers::create_userpass,make_expired_userpass) to send plaintext bytes instead of pre-hashed values, matching the intended client contract.
server/src/database/impls/{postgres,mysql}.rs: fix a column-name mismatch inget_adminandlist_adminswhere the row was read withtry_get("client_certificate")while theSELECTprojects thecertificatecolumn, causing a runtime "column not found" error that blocked admin lookups and session validation on PostgreSQL and MySQL backends (SQLite was unaffected).
- Add first version of the Cosmian Authentication Verifier with main features:
- Realm management — isolated authentication domains, each with its own allowed auth methods, session lifetime, and TOTP configuration
- Username / password authentication — Argon2id password hashing, per-realm allow-expired-passwords policy
- JWT / OIDC authentication — JWKS validation against multiple IdPs per realm, configurable audience and auto-refreshing key cache
- mTLS client-certificate authentication — EC P-256 client certificates verified during TLS handshake
- Two-factor authentication (TOTP) — per-realm TOTP configuration (algorithm, time step), generate / verify / disable enrollment flow, TOTP challenge step in the login flow
- Server-side session management — opaque
_ea_session cookie, session validation endpoint (GET /sessions/{id}), per-realm and per-user bulk revocation, stale-session background collector - Role-based claims — roles stored per user credential, surfaced in the JWT issued at login
- Two-tier administration model — super admins administer all realms; realm admins are scoped to their own realms with exclusive-ownership enforcement
- Multiple database backends — SQLite, PostgreSQL, MySQL (runtime selection via configuration)
- Multiple session-store backends — SQLite, PostgreSQL, MySQL, Redis
- Admin UI — React / Ant Design web application for managing realms, credentials, admins, and sessions