Orleans.Lattice.Membership.Oidc
This page documents Orleans.Lattice.Membership.Oidc 9.9.0, in the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at README.md, and llms.txt lists every page.Generic OpenID Connect credential authenticator for Orleans.Lattice.Membership.
What is it?
Orleans.Lattice.Membership.Oidc adds a provider-agnostic OIDC authenticator (OidcCredentialAuthenticator, registered through AddLatticeOidc) that plugs into the membership credential-authenticator seam. It specializes the built-in JWT authenticator rather than reimplementing token validation, so it inherits the same audience, signing-key, and lifetime checks and only layers on the OIDC-specific concerns. Nothing in it is tied to a particular vendor: everything it needs about a provider is read from that provider's OpenID Connect discovery document, so Okta, Auth0, Keycloak, Ping, Google, and any other conformant issuer are configured the same way.
It is an additive sibling to Orleans.Lattice.Membership.Entra, not a replacement. Neither package depends on the other, and a silo can register both - plus the JWT and anonymous authenticators - at the same time.
What it does
- Discovery-document-driven metadata and signing-key rotation. Validation parameters are resolved from the provider's OpenID Connect discovery document. The JSON Web Key Set is cached and refreshed on its own interval through a configuration manager, so signing keys rotate automatically without a metadata fetch on every call.
- Exact-issuer selection and validation. The authenticator claims a credential only when the token's
issclaim is an ordinal exact match for the configuredIssuer, or when the credential carries the explicitly configuredSchemeHint. There is no prefix form, no wildcard, and no catch-all, so, while noSchemeHintis set, registering two OIDC issuers on one silo is unambiguous regardless of registration order, and a generic OIDC authenticator never claims an Entra token (a hint claims every credential carrying that scheme - see the security notes below). The same exactness is enforced during validation, not just selection: the issuer the discovery document advertises is not accepted unless it is also the configured issuer. - Fail-closed signature-algorithm pinning. The accepted
algset is pinned on every validation. It comes fromAlgorithmswhen that is populated and from the discovery document'sid_token_signing_alg_values_supportedotherwise. An empty set means reject every token, never accept any algorithm - which is what closes the algorithm-confusion gap (CWE-347) that an unpinned validator leaves open, including the classic attack of re-signing a token withHS256using the provider's published public key as the HMAC secret. - Standard OIDC claim conventions. The subject is taken from
sub, and group membership fromgroups,roles, androle. Both lists are configurable per issuer, so a provider that emits membership under a namespaced claim such ashttps://example.com/claims/teamsneeds configuration, not code. Every claim the validated token carries - the subject and group claims included - is also copied into the subject's flat claim bag, which is keyed by claim name, so a claim that repeats keeps only its last value. - Reserved-subject safety. A validated token whose
subis missing, or collides with a reserved well-known sentinel (anonymousorsystem), resolves to the anonymous subject with no groups rather than to an anonymous-labelled principal that still carries the token's groups.
Registration
Register the authenticator on the silo builder after the base membership services. Registration guards this ordering and fails fast with a clear message when membership is not present. When the add-on is not registered it has zero runtime cost.
using Orleans.Lattice.Membership;
using Orleans.Lattice.Membership.Oidc;
siloBuilder.AddLatticeMembership();
siloBuilder.AddLatticeOidc(options =>
{
options.Authority = "https://dev-123456.okta.com/oauth2/default";
options.Issuer = "https://dev-123456.okta.com/oauth2/default";
options.Audiences.Add("api://lattice");
});
Call it once per issuer. Several OIDC authenticators coexist, and because selection is an exact issuer match they never compete for the same token:
using Orleans.Lattice.Membership;
using Orleans.Lattice.Membership.Oidc;
siloBuilder.AddLatticeMembership();
siloBuilder.AddLatticeOidc(options =>
{
options.Authority = "https://dev-123456.okta.com/oauth2/default";
options.Issuer = "https://dev-123456.okta.com/oauth2/default";
options.Audiences.Add("api://lattice");
});
siloBuilder.AddLatticeOidc(options =>
{
options.Authority = "https://keycloak.example.com/realms/lattice";
options.Issuer = "https://keycloak.example.com/realms/lattice";
options.Audiences.Add("lattice-api");
options.GroupClaimTypes.Clear();
options.GroupClaimTypes.Add("groups");
});
Claim types are matched by exact name, not by JSON path. A claim type is compared against the claim names the validated token actually produced, and the token handler does not flatten nested JSON objects into dotted names. A Keycloak realm role therefore cannot be read with
GroupClaimTypes.Add("realm_access.roles"): the token carriesrealm_accessas a single nested object, no claim is ever namedrealm_access.roles, and the entry silently matches nothing - so the caller is resolved with no asserted groups. This fails closed (it under-grants, never over-grants), but it fails silently, so configure the provider to emit the memberships as a top-level claim instead. In Keycloak that is a dedicated group or realm-role protocol mapper on the client, with "Token Claim Name" set to a flat name such asgroupsand "Add to access token" enabled.
Choosing between this package and the Entra package
| Situation | Use |
|---|---|
| Tokens are issued by Microsoft Entra ID (Azure AD) | Orleans.Lattice.Membership.Entra - it adds tenant allow-listing, the templated multi-tenant issuer, oid subject mapping, and groups-overage resolution, none of which is expressible generically. |
| Tokens are issued by any other conformant OIDC provider | This package. |
| Both, on the same silo | Both. They are independent packages, and while neither sets a SchemeHint their authenticators never claim each other's tokens. |
Security notes
- Audience validation is always on. There is no
ValidateAudienceswitch.Audiencesmust contain at least one entry, and an empty list throws at construction rather than silently accepting a token minted for a different relying party. - Algorithm pinning is always on. See "fail-closed signature-algorithm pinning" above. Pin
Algorithmsexplicitly when you want a set narrower than what the provider advertises. - A
SchemeHintshort-circuits issuer selection. When a credential's scheme matches the hint, this authenticator claims the credential before the issuer is read. The token is still fully validated - a foreign token claimed this way fails validation and resolves to anonymous - but it no longer falls through to another authenticator. LeaveSchemeHintunset unless a head genuinely tags its credentials. - Selection parses the token, and that parse is bounded. Unlike the base JWT authenticator, a scheme that does not match the hint does not end selection: it falls through to the exact-issuer match, because the credential bridges stamp the scheme from operator configuration rather than from the caller, so an authenticator that leaves
SchemeHintunset would otherwise never be selected. Selection therefore reads the token on a pre-authentication path that runs once per registered authenticator. To keep that from being an amplification lever for an unauthenticated caller, a credential longer than the validating handler's own maximum token size (256,000 characters) is declined without being parsed - a credential that large would have been rejected by validation regardless, so nothing that could have authenticated is ever turned away.
Reference
- Configuration - every public options property, its type, and its default.
- Membership documentation - the base identity and authorization add-on this package extends.
- Entra authenticator - the Microsoft Entra ID sibling.
Orleans.Lattice.Auth- the policy store and access gate that consume the subjects this package resolves.