Cirreum.Authorization.Oidc
1.0.10
dotnet add package Cirreum.Authorization.Oidc --version 1.0.10
NuGet\Install-Package Cirreum.Authorization.Oidc -Version 1.0.10
<PackageReference Include="Cirreum.Authorization.Oidc" Version="1.0.10" />
<PackageVersion Include="Cirreum.Authorization.Oidc" Version="1.0.10" />
<PackageReference Include="Cirreum.Authorization.Oidc" />
paket add Cirreum.Authorization.Oidc --version 1.0.10
#r "nuget: Cirreum.Authorization.Oidc, 1.0.10"
#:package Cirreum.Authorization.Oidc@1.0.10
#addin nuget:?package=Cirreum.Authorization.Oidc&version=1.0.10
#tool nuget:?package=Cirreum.Authorization.Oidc&version=1.0.10
Cirreum.Authorization.Oidc
Generic OIDC authentication for any OpenID Connect-compliant issuer — for both Web APIs and interactive Web Apps.
Overview
Cirreum.Authorization.Oidc is an authorization provider for the Cirreum framework that covers two scenarios from a single registrar, with no vendor-specific SDK dependency:
- Web API (resource server) — validates JWT bearer tokens locally using ASP.NET Core's
AddJwtBearer(). - Web App (Blazor Web App, MVC, Razor Pages) — interactive sign-in via Authorization Code
- PKCE using
AddOpenIdConnect()with a cookie session.
- PKCE using
The host picks the path. A Minimal API project gets the JWT bearer wiring; a traditional Blazor Web App gets the cookie + OIDC wiring. The configuration shape is the same for both.
When to use
| Token source | Use this? | Why |
|---|---|---|
| Descope | Yes | Standard OIDC issuer with .well-known/openid-configuration |
| Auth0 | Yes | Standard OIDC |
| Okta | Yes | Standard OIDC |
| Keycloak | Yes | Standard OIDC |
| Entra External ID (CIAM) | Yes | Standard OIDC issuer; no Microsoft-specific features needed |
Any .well-known/openid-configuration issuer |
Yes | That's all this provider needs |
| Entra Workforce (employees) | Use Cirreum.Authorization.Entra |
Needs Microsoft.Identity.Web for Graph, OBO, sovereign clouds |
| Customer-owned IdPs (B2B SaaS, dynamic tenants) | Use Cirreum.Authorization.External |
Needs dynamic tenant resolution at runtime |
How it works
Web API path (JWT bearer)
There's no network call to the IdP on every request:
1. Startup (or first request):
├─ AddJwtBearer middleware fetches {Authority}/.well-known/openid-configuration
├─ Reads jwks_uri → fetches the IdP's signing keys (JWKS)
└─ Caches the keys in memory (auto-refreshed every 24 h / on unknown kid)
2. Every request:
├─ Extract Authorization: Bearer <jwt>
├─ Validate signature against cached JWKS
├─ Validate issuer (iss) matches Authority
├─ Validate audience (aud) contains Audience
├─ Validate lifetime (nbf / exp)
└─ Optional: validate required scopes (scp / scope claim)
Everything happens locally in the API process. The IdP is only contacted for metadata
(once at startup) and for signing-key refreshes on kid rotation. This is what makes
JWT-based APIs fast and IdP-independent.
Web App path (Auth Code + PKCE)
1. User hits a protected page:
└─ OIDC middleware redirects to {Authority}/authorize
with response_type=code, PKCE challenge, scopes={openid profile + RequiredScopes}
2. User authenticates at the IdP, browser redirects back to /signin-oidc with a code
3. Middleware exchanges code + PKCE verifier at {Authority}/token
├─ Receives id_token + access_token (+ refresh_token if requested)
├─ Validates id_token signature/issuer/audience/lifetime
└─ Issues an authentication cookie (and persists tokens via SaveTokens = true)
4. Subsequent requests:
└─ Cookie carries the principal — no IdP round-trip until cookie expiry
The registrar registers a "Cookies" cookie scheme automatically — you don't need to call
AddCookie() yourself. ID-token signature validation uses the same JWKS caching as the API
path.
Installation
dotnet add package Cirreum.Authorization.Oidc
Configuration
The provider supports multiple named instances, each validating tokens from a different OIDC
issuer. Register them under Cirreum:Authorization:Providers:Oidc:Instances:
{
"Cirreum": {
"Authorization": {
"Providers": {
"Oidc": {
"Instances": {
"descope": {
"Enabled": true,
"Authority": "https://api.descope.com/<ProjectId>",
"Audience": "<ProjectId>"
},
"auth0": {
"Enabled": true,
"Authority": "https://myapp.auth0.com",
"Audience": "https://my-api"
}
}
}
}
}
}
}
The same configuration shape drives both the Web API path and the Web App path; the host
project picks which path runs. For Web App scenarios, Audience is reused as the OIDC
client_id (this overload is being split into a dedicated ClientId property in a future
version — see Notes for Web App scenarios).
Instance settings
| Property | Required | Description |
|---|---|---|
Enabled |
Yes | Enable/disable this instance without removing the block |
Authority |
Yes | OIDC issuer URL. Must publish .well-known/openid-configuration |
Audience |
Yes | Web API: expected JWT aud claim. Web App: OIDC client_id |
RequiredScopes |
No | Web API: scopes that must be present in the token (rejected at validation). Web App: scopes appended to the authorize request at sign-in time |
TokenValidationParameters |
No | Any property on Microsoft.IdentityModel.Tokens.TokenValidationParameters — e.g. ClockSkew, NameClaimType, RoleClaimType, ValidAlgorithms |
MapInboundClaims |
No | Whether to map short JWT claim names to legacy Microsoft URIs. Defaults to false |
Anything else on JwtBearerOptions or OpenIdConnectOptions (including ClientSecret,
client certificates, CallbackPath, custom events) can be set by adding it to the same
config section — the registrar binds the section onto the options after applying defaults.
For confidential-client web apps, put ClientSecret in user-secrets or Key Vault, not
appsettings.
Built-in defaults
The registrar applies these defaults so a minimal Authority + Audience config "just works"
for the common case. Each default can be overridden in configuration.
Both paths:
| Setting | Default | Why |
|---|---|---|
RequireHttpsMetadata |
true |
Discovery document must be served over HTTPS. Override only for local dev tunnels |
MapInboundClaims |
false |
Keeps JWT short claim names (sub, name, roles) rather than mapping to legacy Microsoft URIs |
TokenValidationParameters.NameClaimType |
"name" |
Standard OIDC core claim (profile scope). User.Identity.Name resolves out of the box |
TokenValidationParameters.RoleClaimType |
"roles" |
Common role-claim convention. [Authorize(Roles = "...")] and User.IsInRole(...) work without extra config |
Web App path only:
| Setting | Default | Why |
|---|---|---|
ResponseType |
"code" |
Authorization Code flow — implicit and hybrid are not used |
UsePkce |
true |
PKCE protects the code exchange. Required for public clients, recommended for confidential clients |
SaveTokens |
true |
Persists id/access/refresh tokens in the auth cookie so the app can call downstream APIs without re-prompting |
Scope |
[ "openid", "profile" ] |
Standard OIDC core scopes. RequiredScopes are appended additively |
SignInScheme / SignOutScheme |
"Cookies" |
A cookie scheme is registered automatically — no AddCookie() call needed |
Consumers whose IdP uses different claim names can override via config — see Full example — overriding TokenValidationParameters below.
Instance key = scheme name
The key under
Instances:(descope,auth0in the example above) is the ASP.NET Core authentication scheme name. It's auto-populated intoSchemeduring registration — do not setSchemein configuration. See the base package README for more on this convention.
Full example — overriding TokenValidationParameters
This example shows what's possible when the defaults don't fit — e.g., an IdP that emits
namespaced role claims (Auth0), a groups claim instead of roles (Okta), or you want
email as the display name:
{
"Cirreum": {
"Authorization": {
"Providers": {
"Oidc": {
"Instances": {
"auth0": {
"Enabled": true,
"Authority": "https://myapp.auth0.com",
"Audience": "https://my-api",
"TokenValidationParameters": {
"ClockSkew": "00:00:02",
"NameClaimType": "email",
"RoleClaimType": "https://myapp/roles",
"ValidAlgorithms": [ "RS256" ]
}
}
}
}
}
}
}
}
Anything on JwtBearerOptions, OpenIdConnectOptions, or TokenValidationParameters can be
set from config — the registrar applies its defaults first, then binds the instance section on
top, then re-pins Authority and Audience/ClientId from the typed settings.
Notes for Web App scenarios
A traditional Blazor Web App (.NET 8+ server-interactive), MVC app, or Razor Pages app uses
the Web App path automatically — there's nothing Blazor-specific in the registrar. The
AuthorizeRouteView / CascadingAuthenticationState / AuthenticationStateProvider
machinery in Blazor sits on top of the standard cookie principal that this registrar produces.
Confidential vs public clients. A traditional server-rendered web app is normally a
confidential client and needs a ClientSecret. Add it under the instance section
(via user-secrets / Key Vault, not appsettings):
"descope": {
"Authority": "https://api.descope.com/<ProjectId>",
"Audience": "<ProjectId>",
"ClientSecret": "..."
}
Public-client PKCE-only flows are also supported if your IdP allows them — just omit
ClientSecret.
Audience overload. On the Web App path, Audience is currently assigned to
OpenIdConnectOptions.ClientId. A future version of Cirreum.AuthorizationProvider will
introduce a dedicated ClientId property; until then, populate Audience with your OIDC
client_id for Web App instances.
Authorization
The library validates the token. It does not enforce per-endpoint authorization — that's ASP.NET Core's policy layer.
Role-based (typical)
For most Cirreum-built apps, role-based authorization is the primary enforcement mechanism.
Set RoleClaimType (see above) to whatever your IdP names the roles claim, then use the
standard attributes:
app.MapGet("/admin", [Authorize(Roles = "admin")] () => "ok");
app.MapGet("/users", [Authorize(Roles = "user,admin")] () => "ok");
Scope-based (advanced)
If you want per-endpoint scope enforcement, use ASP.NET Core policies:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("api:read", p => p.RequireClaim("scp", "api:read"))
.AddPolicy("api:write", p => p.RequireClaim("scp", "api:write"));
app.MapGet("/data", [Authorize(Policy = "api:read")] () => ...);
Advanced: scope enforcement
Most first-party Cirreum apps (single SPA + single API) rely on audience + signature + roles
and do not need RequiredScopes. Scope enforcement is most useful when:
- Tokens may come from multiple clients with different permission levels
- You have third-party integrations with delegated permissions (e.g., Descope Inbound Apps)
- You want an explicit contract between API and client even when audience already isolates
When you do enable it, RequiredScopes acts at the scheme level: every request that
authenticates through this scheme must carry all listed scopes. Tokens missing any of them
are rejected with 401.
How the IdP decides which scopes are in the token
The scope claim (scp or scope) is not a self-service field. It's emitted by the IdP
only if it approved the requesting client for those scopes at issuance time. Each IdP has
its own model for gating this:
| IdP | Scope approval model |
|---|---|
| Entra Workforce / External | API app registration exposes scopes; Client app has delegated permissions; admin/user consent |
| Auth0 | API resource declares permissions; Applications are authorized for specific permissions |
| Okta | Authorization Server defines scopes; Apps are assigned via scope groups |
| Descope (default OIDC app) | Pass-through of standard OIDC scopes; no app-level scope authorization. Use Inbound Apps for delegated-access scopes |
Before requiring a scope in config, confirm your IdP is actually issuing it. Decode a
fresh access token at jwt.ms and look for the scope in scp or scope.
If it's missing, the IdP doesn't emit it and RequiredScopes will reject every request.
Scope validation semantics
- AND semantics — all listed scopes must be present. There is no "any of" variant; use ASP.NET Core policies for that.
- Both claim names are checked —
scp(space-delimited, common in Microsoft/Entra) andscope(common elsewhere). Values from both claims are unioned. - Case-insensitive — scope comparisons use
StringComparer.OrdinalIgnoreCase.
Example
"entraExternal": {
"Enabled": true,
"Authority": "https://myapp.ciamlogin.com/<tenant-id>/v2.0",
"Audience": "<client-id>",
"RequiredScopes": [ "access_as_user" ]
}
Registration
The provider is registered automatically by the Cirreum authorization pipeline via configuration. No manual registration code is needed beyond the standard hosting setup:
// In HostingExtensions (framework-level)
builder.RegisterAuthorizationProvider<
OidcAuthorizationRegistrar,
OidcAuthorizationSettings,
OidcAuthorizationInstanceSettings>(authenticationBuilder);
Each enabled instance under Providers.Oidc.Instances is registered as a distinct ASP.NET
Core authentication scheme (named after the instance key). At request time,
Cirreum.Runtime.Authorization dispatches tokens to the correct scheme based on the aud
claim.
Contribution Guidelines
Be conservative with new abstractions The API surface must remain stable and meaningful.
Limit dependency expansion Only add foundational, version-stable dependencies.
Favor additive, non-breaking changes Breaking changes ripple through the entire ecosystem.
Include thorough unit tests All primitives and patterns should be independently testable.
Document architectural decisions Context and reasoning should be clear for future maintainers.
Follow .NET conventions Use established patterns from
Microsoft.Extensions.*libraries.
Versioning
Cirreum.Authorization.Oidc follows Semantic Versioning:
- Major - Breaking API changes
- Minor - New features, backward compatible
- Patch - Bug fixes, backward compatible
Given its foundational role, major version bumps are rare and carefully considered.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Cirreum Foundation Framework
Layered simplicity for modern .NET
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Cirreum.AuthorizationProvider (>= 1.0.26)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.7)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 10.0.7)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Cirreum.Authorization.Oidc:
| Package | Downloads |
|---|---|
|
Cirreum.Runtime.Authorization
The Runtime Authorization configuration. |
GitHub repositories
This package is not used by any popular GitHub repositories.