Release v1.1.0
Cirreum.Runtime.Wasm 1.1.0 — client canonicalization of provisioned claims
Why this release exists
The Cirreum Identity family now lets a server-side provisioner mint an application's own claims
into the issued token under a custom* namespace (customRoles, customName, customTenant, …),
collision-safe against native identity-provider claims. This release is the client counterpart:
it aliases those custom* claims back to the native names the rest of the client already reads.
What's new
CommonClaimsPrincipalFactory gained a built-in canonicalization phase that runs during
CreateUserAsync, before the ClaimsPrincipal is built and before any app IClaimsExtender:
this.MapIdentity(identity, account);
CustomClaimCanonicalizer.Canonicalize(identity); // customRoles -> roles, customName -> name, …
this.ExtendClaims(identity, account);It does two things, both universal boilerplate every app would otherwise re-implement:
- Alias each
custom*claim to its native name. A minted claim iscustom+ an upper-cased
first character, socustomRoles/customNamematch while an unrelatedcustomerclaim is left
untouched. The two named claims follow the identity's configured claim types —customRoles
→RoleClaimType,customName→NameClaimType(theoptions.RoleClaim/options.NameClaimthe
provider set) — soIsInRoleandIdentity.Nameresolve even when an OIDC app overrode them via
AddOidcAuth(roleClaimType:, nameClaimType:). Every other claim uses its derived name
(customTenant→tenant). - Split a
custom*array claim — which arrives as a single JSON-array string
(["admin","user"]) — into individual claims, asIsInRolerequires. Provisioned arrays are
contractually non-blank string arrays, so null, empty, and non-string entries are dropped (a
boolean or object never becomes a role), and an empty scalar mints nothing. Each alias preserves
the source claim's issuer, original issuer, and properties, with its value type pinned to String.
Because the factory is inherited by both MsalClaimsPrincipalFactory (Entra) and
OidcClaimsPrincipalFactory (Descope / generic OIDC), both providers get it with no wiring.
Additive by design — the app owns precedence
The phase is purely additive and idempotent; it never removes a claim and never adds an exact
(type, value) duplicate (re-running it — or aliasing onto a claim type the identity already
carries — is safe). When both a native claim and its minted custom* counterpart are present,
both survive. The framework can't know whether the native
value is stale-and-to-be-replaced or complementary, so it doesn't guess — resolving that precedence
is the application's decision, made in its own IClaimsExtender, which runs after canonicalization
with full visibility of every claim (the original custom*, the native one, and the alias). The
framework owns the boilerplate; the app owns the judgment.
It is inert when unneeded: an app that canonicalizes at the IdP (Azure token config, a Descope
flow step) ships a token already in native form, so the phase finds no custom* claims and does
nothing. Always safe to run, never a dependency.
Also fixed in this release
An adversarial review of CommonClaimsPrincipalFactory alongside this work surfaced two shipped
defects, both fixed here:
- The deduplication window returned unprocessed principals. A repeated
CreateUserAsyncfor
the same user within 30 seconds skipped identity mapping and claims extension but still handed
the resulting principal to Blazor as the authentication state — on MSAL that meant a principal
with no role claims at all until the next call outside the window, diverging fromIUserState.
The claim transforms now always run; the window deduplicates only state publication, and
ClientUsermutation and subscriber notification form one logical publication, always
performed together. The deduplication key is the user id plus an order-insensitive fingerprint
of the processed claims (including the identity's name/role claim-type configuration): an
identical duplicate call (the login double-fire) skips re-publication, while any content
change — refreshed roles, different extender output, a recovered transform — publishes in
full even within the window. Tracking records exactly what was published, so a persistent
optional-transform failure dedupes instead of re-publishing on every duplicate call. - JSON
nullaccount members produced empty-string claims (a dead null-check inherited from
the upstream factory template). They are now skipped.
The account-claim conversion was also tightened: JSON string members map to unquoted claim
values, JSON null/undefined are skipped, arrays keep their raw JSON (what the canonicalizer
splits), and the fallback path is reflection-free for trimmed/AOT WASM publishing.
Compatibility
- No API changes.
IClaimsExtenderandAddClaimsExtender<T>()are unchanged; app extenders now
simply run against already-canonicalized claims. - Pairs with
Cirreum.IdentityProvider 2.0.0,Cirreum.Identity.EntraExternalId 2.1.0, and
Cirreum.Identity.Oidc 1.1.0. No coordinated client change is required beyond consuming this
version — the two provider packages inherit the behavior by re-pin.
See also
Cirreum.IdentityProvider 2.0.0— the server-side provisioning mint this canonicalizes.Cirreum.Identity.EntraExternalId 2.1.0/Cirreum.Identity.Oidc 1.1.0— the wire projections.