Релизы redb.Identity
Все релизы redb.Identity, от свежего к старому. Текст — как в репозитории, без пересказа.
3.4.0
Why the jump from 1.2.2 to 3.4.0 — aligning with the ecosystem, not a breaking rewrite. Until now redb.Identity carried its own 1.x line while the rest of the stack (
redbcore,redb.Route,redb.Tsak) moved together on 3.x. Running two numbering schemes made "which Identity works with which core/Route/Tsak" a lookup instead of a glance. From now on redb.Identity shares the ecosystem version: it jumps straight to 3.4.0 to sit on the same number as theredb.Route3.4.0 it is built against, and every future ecosystem release bumps Identity with it.This is a version realignment, not a semver-breaking release — there are no breaking API changes here (the only feature, JAR, is off by default and additive; see below). The major digit changes because the number is now the ecosystem's, not because Identity's contract broke. The old 1.x tags remain valid history; 3.4.0 simply continues the same package on the shared number.
Added
- JWT-Secured Authorization Request — JAR (Z7 / RFC 9101), off by default.
/connect/authorizenow accepts a signed authorization request, gated behindFeatures.EnableJar(defaultfalse, so every prior release behaves identically — arequestparameter is still answered withrequest_not_supported). When enabled:- a signed
requestobject (inline) orrequest_uri(by reference, fetched over HTTP) is verified against the client's registered keys, and the parameters inside the JWT take precedence over the query string (RFC 9101 §6.1); alg: noneis never accepted — an unsigned request object drops the integrity guarantee JAR exists for; nor is a wrong-key signature, a mismatched innerclient_id, an expired object, or (when the client declaresRequestObjectSigningAlgunderJarEnforcementMode=Enforce) a mismatched algorithm. Every failure isinvalid_request_object;request_urifetches are SSRF-guarded (OutboundUrlGuard) and size/timeout-bounded; PAR'surn:ietf:params:oauth:request_uri:*is left untouched;- discovery advertises
request_parameter_supported,request_uri_parameter_supportedandrequest_object_signing_alg_values_supported— only when JAR is on, so discovery never promises what the endpoint won't do. - The formerly write-only client fields
RequestObjectSigningAlgetc. now do something (see below). - New config:
JarOptions(Jarsection) — enforcement modeOff/LogOnly/Enforce, allowed algorithms (asymmetric only), clock skew, size limits, and therequest_uriSSRF knobs. - Implemented as OpenIddict server handlers (
ValidateRequestObjectHandler), replacing the built-in handlers that unconditionally rejectedrequest/request_uri. 15 handler tests + a live conformance demo (demo_jar_request_object.ps1, wired intorun_all).
- a signed
- Client public-key resolver —
IClientKeyResolver(Z7 / RFC 9101, phase 1). Resolves the keys that verify what a client signed: a JAR request object and aprivate_key_jwtassertion. Two sources — the inlineApplicationProps.JsonWebKeySet, orJwksUri, fetched and cached viaConfigurationManager<JsonWebKeySet>(background refresh on a TTL plus a rate-limited forced refresh on akidmiss, so a client rotating its keys does not break sign-in until the TTL expires).- Fails closed. An unreachable or malformed JWKS yields no keys, so there is nothing to verify against and the caller rejects. "Could not check" is never treated as "valid".
- A configured-but-broken inline JWKS does not fall back to
jwks_uri. A typo in the pasted key set must stay visible instead of being masked by a silent switch to another key source. - Asymmetric algorithms only. HMAC (
HS*) is impossible:ClientSecretis stored as a BCrypt hash and verifying an HMAC needs the original secret. FAPI 2.0 forbidsHS*for request objects anyway. - The service is inert for now — registered but called from no path, so server behaviour is
unchanged. Wiring it into JAR is phase 2; into
private_key_jwt, a separate step.
OutboundUrlGuard— SSRF filter for URLs that arrive from outside (jwks_uri, later the JARrequest_uri). Rejects anything that is not an absolute HTTPS URL, and anything resolving to a non-public address: loopback, RFC 1918, link-local — including169.254.169.254, the cloud metadata endpoint that hands instance credentials to whatever can reach it — CGNAT, IPv6 unique-local, and the IPv4-mapped forms (::ffff:10.0.0.1) that a naive check walks straight past. The check runs before a socket is opened. Known limitation: this does not close DNS rebinding, which would require pinning the validated address onto the connection itself.ClientKeysOptions(ClientKeyssection ofRedbIdentityOptions) — cache lifetime, minimum refresh interval, fetch timeout, maximum document size,RequireHttps,AllowPrivateNetworkTargets. The last two are development-only relaxations.- 40 tests (
tests/Security/): 29 for the SSRF guard — including one asserting that 172.32 and 172.15 are public and must NOT be blocked — and 11 for the resolver. All stay offline: attempting a network call fails the test by itself.
Docs
doc/JAR_RFC9101_PLAN.md— JAR implementation plan: six phases, risks (SSRF,alg:none, algorithm substitution), a stagedOff → LogOnly → Enforcerollout, and sizing. Records a verified finding: OpenIddict 6.3.0 does not support request objects — its assembly carriesValidateRequestParameterand therequest_not_supportedstring, but nothing that parses a request object or checks its signature — andrequest_uriexists only as a PAR URN.Z7now links here.ApplicationProps.RequestObjectSigningAlgis now enforced underJarEnforcementMode=Enforce(was previously stored and ignored)....EncryptionAlg/...EncryptionEncremain stored-only — request-object encryption (JWE) is the deferred phase 4.JwksUriis now resolved (phase 1).
Conformance impact (OpenID Foundation Basic OP)
Advertising JAR changed the local Basic-OP run from 1 SKIPPED / 4 REVIEW to 2 SKIPPED / 3 REVIEW
— still 0 FAILED. This is a net improvement, not a regression, and OPENID_CERTIFICATION.md §4.3
explains it in full for anyone reading the badge:
- Both SKIPPED modules test the unsigned (
alg:none) request object. The suite skips them when the server does not advertisenoneinrequest_object_signing_alg_values_supported— which is exactly our stance: we support signed request objects only, becausealg:noneis an unsigned JWT that defeats JAR's integrity guarantee and is forbidden by FAPI 2.0. SKIPPED here = "the server declined an unsafe mode", the secure answer. - One module (
ensure-request-object-with-redirect-uri) moved REVIEW→SKIPPED precisely because the server now genuinely processes signed request objects and advertises that honestly, instead of the earlier no-support state that sent it down a screenshot path. Turning either skip into a pass would require acceptingalg:none— a security regression we will not make.
1.2.2
Why the bump. No functional changes to redb.Identity — this release exists to pick up the fixed redb storage, and it is a patch because only dependencies moved.
redb.Identity.Core1.2.1 depends onredb.Core3.3.0, whose embeddedredb_init.sqlcarried anALTER FUNCTION ... OWNER TO postgresthat failed schema initialization under a non-superuser database owner — the whole first-start init rolled back. Identity is redb-backed (every store — Application, Authorization, Token, Scope, KeyRing, Session, Audit — lives in redb storage), so an Identity deployment on a least-privilege database could not start. The fix landed inredb.Core3.3.3, and without this rebuild it would never reach Identity users.1.2.2 is built against redb.Core / redb.Route / redb.Tsak 3.3.3 (was 3.3.0 / 3.3.1 / 3.3.1).
Why not 3.3.3. redb.Identity keeps its own 1.x line and does not follow the ecosystem number — but it is released together with the ecosystem, because it depends on redb storage. A fix in redb core is always a redb.Identity release too.
1.2.1
Token issuance is atomic again — and the addon story is now true in a shipped build. 1.2.0
documented (and the launch articles describe) an addon that mints a token through
direct-vm://identity-token inside its own WithRedbTx, committing its domain write and the token
issue as one transaction. In 1.2.0 that was not actually the case in the .tpkg topology — the fix
is here.
Fixed
The OpenIddict stores now enlist in the route transaction. In the
.tpkgtopology Identity resolves its services from a child container, and that container used to open a host scope of its own per DI scope — a second DB connection. A route-level redb transaction runs on the connection redb.Route caches on the exchange, so the token/authorization store writes landed on a different connection: no atomicity, and — because the second connection blocked on the row locks the first one held while the first awaited the call that opened the second — a self-deadlock that cleared only on the 30-second transaction timeout (~34 s on SQLite via the busy-timeout × retry cascade). This is whyWithRedbTxhad to be stripped from the token route in 1.2.0. It was never a SQLite quirk: the deadlock rule is written about Npgsql; SQLite's single writer only made it louder.The child scope now binds its
IRedbServiceto the exchange's instance (a newIdentityExchangeAccessorcarries the exchange in), so the stores write on the same connection the transaction was opened on.WithRedbTxis restored on the token route: the token entry and the authorization entry either both land or neither does. And an addon that wraps its own route inWithRedbTxand callsdirect-vm://identity-tokennow gets one atomic commit across its own write and Identity's token issue — the claim the docs make, now real. Resolution is also lazy now: a child scope that never touches the DB no longer pays for a connection.Token route was silently losing its RouteId. Caught by
RouteTransactionMarkingTestswhile restoring the wrap:WithRedbTx(From(...)).RouteId(...)set the id on the transaction definition, not the route, soidentity-tokenwould have registered with no id — breaking cluster locks, per-route metrics and the dashboard. Every demo still passed; only the guard test saw it. Fixed by moving.RouteId(...)insideFrom(...).
Notes
- Out-of-route callers (cleanup timers, hosted services, schema init) still get their own scope —
there is no ambient transaction to join, and taking somebody else's connection would be worse than
the bug. Covered by a new test (
ChildScope_WithoutExchange_GetsItsOwnRedbService), and the enlistment itself is proven byChildScope_BoundToExchange_ResolvesTheExchangeRedbService, which was verified to go red when the fix is disabled — a guard that cannot fail is not a guard. - The other six routes that carry no
WithRedbTx(Login, Authorize, Revoke, MfaVerify, MfaRecovery, MfaManage, ManageApps) are correctly left unwrapped and their comments now say why: a segment transaction already inside the processor (MFA), a single atomic store operation (Authorize / Revoke / apps), or a single write behind Argon2id where a route-level wrap would hold the writer lock over the CPU verify for no atomicity gain (Login). Seedoc/PERF_RULES.mdrule 1. - Test suite:
Passed: 1769, Skipped: 1, Failed: 0on PostgreSQL, MSSQL and SQLite.
1.2.0
Basic OP: 35 modules, zero failures. The official OpenID Foundation conformance suite was run against a live redb.Identity end to end — and it found real defects, which are fixed below. We also audited our own RFC catalogue line by line against the code, live discovery and the demo probes; what could not be proved was either built or struck. Both outcomes are in this release.
| Result | Modules |
|---|---|
| PASSED | 29 |
| REVIEW | 4 — manual-screenshot tests, pass-equivalent |
| WARNING | 1 — two deliberate id_token claims (see Notes) |
| SKIPPED | 1 — request objects (RFC 9101), which we do not advertise |
| FAILED | 0 |
⚠️ Breaking
- Scope-derived claims are no longer embedded in the id_token. In the
authorization-code flow the
profile/email/phone/addressclaims are now delivered from/connect/userinfoonly, per OIDC Core §5.4. This was a PII leak: an id_token is routinely forwarded to third parties and logged as proof of the authentication event, so the user's e-mail, phone and address travelled considerably further than the RP intended — and the conformance suite flags it as "may result in user data being exposed in unintended ways". If your RP read those claims out of the id_token, it will now read empty. Fetch them from/connect/userinfowith the access token (the RP loses nothing — the same claims are served there), or, if you need a specific claim in the id_token, request it explicitly via the newclaimsparameter (below):claims={"id_token":{"email":null}}. The id_token keeps what identifies the authentication event:sub,auth_time,acr,sid,nonce,at_hash.
Added
- OIDC Core §5.5 — the
claimsrequest parameter. An RP can now name the exact claims it needs instead of pulling in a whole scope to get one of them, and choose the delivery channel: theuserinfomember or theid_tokenmember.essential,valueandvaluesqualifiers are honoured. A claim we hold no value for is omitted (Essential is a statement of need, not a licence to invent); avalue/valuesconstraint we cannot satisfy means the claim is omitted rather than answered with a different value. Aclaimsrequest that pinssubto a different End-User is refused. Discovery now advertisesclaims_parameter_supported: true— it saidfalsebefore, honestly. Probe:demo_claims_parameter.ps1. - OIDC Core §5.1 — the complete
profileclaim set. All 14 claims, withupdated_atas a JSON number and the*_verifiedflags as JSON booleans, as the spec requires.UserPropsgains the missing OIDC fields — a props change, so no migration. - RFC 7643 §4.3 — SCIM Enterprise User extension.
department,manager,employeeNumber,costCenter,organization,division. This is what corporate provisioning actually sends: Okta, Entra ID and Workday push it on the very first sync, and a provider that advertises only the core schema makes them drop that data on the floor.manageris a complex attribute; its$refanddisplayNameare derived, not stored — §4.3 marks displayName read-only, and a stored copy would rot the moment the manager is renamed. Probe:demo_scim_enterprise.ps1. - RFC 8252 §7.3 — loopback redirect URIs, port not compared. Without this no
native or CLI client can use this provider at all: they ask the OS for an
ephemeral callback port at launch, so the port differs on every run and cannot
be registered in advance. The widening is exactly one port wide — it applies
only to a client that already registered a loopback URI; the host must be the
literal
127.0.0.1or[::1](not the namelocalhost, which §8.3 warns can be resolved elsewhere); userinfo in the URI is refused; and scheme, host, path, query and fragment must still match exactly. Toggle:RedbIdentityOptions.AllowLoopbackRedirectPortWildcard(defaulttrue— §7.3 is a MUST, not an opt-in). Probe:demo_loopback_redirect.ps1, deliberately mostly negative. - HTML error page at
/connect/authorize. RFC 6749 §4.1.2.1 says that when theredirect_uriis missing, unregistered or malformed the server must not redirect — it has nowhere trustworthy to send the user, so it must inform the resource owner itself. We already refused to redirect; we just informed the resource owner with a raw JSON error object. A human staring at{"error":"invalid_request"}has not been informed. Content-negotiated: browsers get the page, anything sendingAccept: application/jsonor*/*(curl, SDKs, the conformance suite) gets the byte-identical JSON it got before.
Fixed
- UserInfo leaked token plumbing. The response was copying claims through a
deny-list, so OpenIddict internals (
oi_*) and JWT protocol claims (jti,exp,iat,at_hash,nonce…) rode along into the userinfo document. Those describe the token, not the user; UserInfo (§5.3) returns the user's claims and nothing else. Now an explicit allow-list. /connect/userinfo(POST) rejected the access token in the request body (RFC 6750 §2.2) — the route was missing form-to-body mapping.prompt=nonecould still surface a login form. When aclaimsrequest pinnedsubto a different End-User, the rejection was deferred to the local/loginpage even though the RP had explicitly forbidden any interactive UI (OIDC §3.1.2.6). The error now flows back to the client'sredirect_uri.- SCIM discovery 404 under the management prefix.
/api/v1/identity/scim/v2registered only the list endpoints —ResourceTypes/{id}andSchemas/{id}were missing. A provisioning client is pointed at one base URL, walks ResourceTypes and then fetches each Schema by id — and got a 404 on the first schema it asked for. oi_au_idno longer leaks into the id_token (kept on the access_token, where introspection needs it).
Changed
- The RFC catalogue in the docs is now audited, not asserted. Five rows were
provably false. Three of them we chose to build rather than delete (the
claimsparameter, SCIM Enterprise, loopback). Two were struck, with the reasoning stated plainly:- Front-Channel Logout 1.0 — it signs RPs out through an iframe to each
client, so it rides on third-party cookies. Safari's ITP blocks them; Chrome
is burying them. The mechanism is broken by design in a modern browser. We
ship Back-Channel Logout — server to server, signed logout token, plus a
pull feed of revoked
sids for multi-replica RPs. It is strictly better and cookie-independent. The working mechanism beats the checkbox. - HOTP (RFC 4226) as a standalone method — counter-based OTP is effectively unused; the world runs on TOTP. RFC 4226 lives on as the foundation of our TOTP (160-bit secret per §4), which is what we now say.
- Front-Channel Logout 1.0 — it signs RPs out through an iframe to each
client, so it rides on third-party cookies. Safari's ITP blocks them; Chrome
is burying them. The mechanism is broken by design in a modern browser. We
ship Back-Channel Logout — server to server, signed logout token, plus a
pull feed of revoked
Notes
- Two conformance warnings are deliberate.
oidcc-serverfinishes WARNING on two non-requested id_token claims. These are warnings, not failures — OIDC Core does not forbid extra id_token claims, and the suite's own message concedes it may be seeing "an extension the conformance suite is not aware of". Neither is data about the user:oi_tkn_idis the token's store-entry id, and it is what makes the id_token revocable and drives back-channel logout;redb:user_idis a namespaced private claim (RFC 7519 §4.3) that lets a client join our internal bigint to its own user table without a round-trip. In the same run we deleted a third such claim (oi_au_id), because it was OpenIddict's internal authorization link, meaningless to a client, and there was nothing to defend. Keeping two and cutting one is a judgement, not a rationalisation. - Test suite:
Passed: 1767, Skipped: 1, Failed: 0on SQLite; one codebase across PostgreSQL / MSSQL / SQLite.
1.1.0
Preparing for OpenID Certification. We
stood up the official OpenID Foundation conformance suite against a live
redb.Identity and used it to validate — and harden — the OIDC / OAuth surface
end to end. Full state and per-module breakdown:
OPENID_CERTIFICATION.md.
Verified
- OpenID conformance — local runs of the official OIDF suite. Config OP reports zero failures over native HTTPS; the Basic OP authorization-code-flow modules pass. Every automatable module is green — the remainder are interactive re-auth flows (which behave to spec) and automation limits, not server gaps.
- .NET suite still green across all three providers —
Passed: 1767, Skipped: 1, Failed: 0(PostgreSQL / MSSQL / SQLite), one identical codebase. - Demos run over HTTPS out of the box. The
demos/suite is base-URL-switchable ($env:IDENTITY_BASE) and TLS-clean end to end.
Added
- Native HTTPS for the HTTP facade — TLS terminated in-process (Kestrel via
the redb.Route.Http connector:
ssl=true+ cert path in config), no reverse proxy required. SeeHTTPS.md. OPENID_CERTIFICATION.md— what redb.Identity implements toward OpenID Certification and the current conformance state.- Configurable PKCE (
RedbIdentityOptions.RequirePkce) — enforce proof-key per client, or relax for a non-PKCE Basic-profile run; S256-only when present.
Fixed
- Authorization error delivery (RFC 6749 §4.1.2.1). Errors are returned via
the client's registered
redirect_urionly — validated against the client before any redirect — closing an error open-redirect and deliveringinvalid_scope/invalid_requeston the correct channel. - Token-endpoint error codes (RFC 6749 §5.2). A reused / already-redeemed
authorization code now returns
400 invalid_grant(was401 invalid_token);invalid_clientmaps to 401. Cache-Control: no-store+Pragma: no-cache(RFC 6749 §5.1) on the token, PAR, introspection, revocation and device-authorization responses; discovery and JWKS stay cacheable by design.prompt=login/max_agere-authentication (OIDC §3.1.2.1). Both route the End-User back to/loginand complete on re-login — never leaked as an error to the RP. The signed re-auth marker is bound to the session id, so there is no redirect loop and no same-second bypass.- Boolean claims (OIDC §5.1).
email_verified/phone_number_verifiedare emitted as JSON booleans, not strings;acras"1"/"2". - id_token hygiene. OpenIddict's internal
oi_au_idauthorization-reference claim no longer leaks into the id_token (kept on the access_token for introspection linkage). - Discovery metadata completeness —
acr_values_supported,claim_types_supported, and the fulltoken/introspection/revocation_endpoint_auth_methods_supportedsets.
1.0.1
First public source release of redb.Identity.
Verified
- Full cross-provider parity. The test suite (1768 tests) is green on
all three redb storage providers — PostgreSQL, Microsoft SQL Server and
SQLite — from one identical codebase:
Passed: 1767, Skipped: 1, Failed: 0. The single skip is a known PostgreSQL-specific test-host teardown probe, not a product gap. The provider is chosen by the host worker; Identity references only theredb.CoreOSS abstraction and never a concrete provider.
Changed
- Public-repo framing (README). Documented the runnable
demos/suite and itsdemos/run_all.ps1driver; listed the SQLite provider alongside PostgreSQL / MSSQL; reconciled the RFC catalogue against the source tree; and made the deployment story explicit up front — the engine ships as.tpkgpackages for redb.Tsak, HTTP is merely the first transport facade, any in-process module can call Identity overdirect-vm://with zero network, andredb.Identity.Webis a reference BFF (Contracts + Client only, never Core/Http). - Publication tooling.
scripts/publish-identity-public.ps1prepares the public repo — copies the nine OSS source projects, the demo suite, the dev / build helper scripts and the fulltests/tree (all three test projects, so the 1767-passing multi-provider suite ships for the community to read and run), patches cross-reporedb.Core*/redb.*provider /redb.Route.*/redb.Tsak.*ProjectReferences to NuGet PackageReferences, strips the Proredb.licenseand sanitises sample config, and excludes onlydoc/(internal notes). A Cyrillic guard blocks the push if any non-English text slips in.