Udap.Server
0.8.4
dotnet add package Udap.Server --version 0.8.4
NuGet\Install-Package Udap.Server -Version 0.8.4
<PackageReference Include="Udap.Server" Version="0.8.4" />
<PackageVersion Include="Udap.Server" Version="0.8.4" />
<PackageReference Include="Udap.Server" />
paket add Udap.Server --version 0.8.4
#r "nuget: Udap.Server, 0.8.4"
#:package Udap.Server@0.8.4
#addin nuget:?package=Udap.Server&version=0.8.4
#tool nuget:?package=Udap.Server&version=0.8.4
Udap.Server
📦 Nuget Package: Udap.Server
This package adds UDAP Dynamic Client Registration (DCR) and metadata capabilities to an authorization server built on Duende IdentityServer. It provides the .well-known/udap metadata endpoint and the /connect/register DCR endpoint as extensions to the IdentityServer pipeline.
Note: Duende IdentityServer requires a license for production use above $1M annual revenue.
Features
- UDAP metadata endpoint (
.well-known/udap) - Dynamic Client Registration (create, update, cancel)
- Multi-community trust anchor support
- Authorization Extension Object (AEO) enforcement via
IUdapAuthorizationExtensionValidator - Optional
udap_communityaccess-token claim (see Community Claim) - Tiered OAuth support
Profile-Specific Validation
For SSRAA or TEFCA community-specific validation rules, add the corresponding packages:
Udap.Ssraa.Server— HL7 v3 PurposeOfUse enforcementUdap.Tefca.Server— TEFCA Exchange Purpose (XP) code validation, SAN matchingUdap.Tefca.Model— TEFCA extension models (tefca-ias, XP constants)
Full Example
Below is a full example. See also the Udap.Auth.Server example project.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlite(connectionString,
dbOpts => dbOpts.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlite(connectionString,
dbOpts => dbOpts.MigrationsAssembly(migrationsAssembly));
})
.AddResourceStore<ResourceStore>()
.AddClientStore<ClientStore>()
.AddTestUsers(TestUsers.Users)
.AddUdapServer(
options =>
{
var udapServerOptions = builder.Configuration.GetOption<ServerSettings>("ServerSettings");
options.DefaultSystemScopes = udapServerOptions.DefaultSystemScopes;
options.DefaultUserScopes = udapServerOptions.DefaultUserScopes;
options.ForceStateParamOnAuthorizationCode = udapServerOptions
.ForceStateParamOnAuthorizationCode;
},
options =>
options.UdapDbContext = b =>
b.UseSqlite(connectionString,
dbOpts =>
dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName)),
baseUrl: "https://localhost:5002/connect/register"
);
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseUdapServer();
app.UseIdentityServer();
app.UseAuthorization();
app.MapRazorPages().RequireAuthorization();
app.Run();
Community Validation Rules
UDAP supports multiple trust communities, each with its own validation rules for token requests and client registration. The validation pipeline is pluggable via ICommunityTokenValidator and ICommunityRegistrationValidator.
Built-in profiles
Two profile packages are available:
| Package | Communities | POU codes | Max POU | Registration checks |
|---|---|---|---|---|
Udap.Ssraa.Server |
SSRAA / standard UDAP | 62 HL7 v3 codes | unlimited | none |
Udap.Tefca.Server |
TEFCA | 12 XP codes | 1 | SAN URI XP code validation |
Registering community validators
Add the profile packages and map communities to their validation pipelines:
// SSRAA rules for standard UDAP communities
builder.Services.AddUdapSsraaValidation(options =>
{
options.Communities.Add("udap://fhirlabs.net");
});
// TEFCA rules (register model extensions first)
builder.Services.AddUdapTefcaExtensions();
builder.Services.AddUdapTefcaValidation(options =>
{
options.Communities.Add("tefca://test-community");
});
How it works at runtime
- A client requests a token with authorization extensions (e.g.,
hl7-b2bwithpurpose_of_use) DefaultUdapAuthorizationExtensionValidatorresolves the client's community from the registration store- Iterates through registered
ICommunityTokenValidatorimplementations until one matches viaAppliesToCommunity() - The matching validator returns
CommunityValidationRulesspecifying required extensions, allowed POU codes, and max POU count - The framework enforces those rules, then calls the validator's
ValidateAsync()for any domain-specific checks
Custom community validators
Implement ICommunityTokenValidator for custom rules:
public class MyValidator : ICommunityTokenValidator
{
public bool AppliesToCommunity(string communityName)
=> communityName == "udap://my-community";
public CommunityValidationRules? GetValidationRules(string? grantType)
=> new CommunityValidationRules
{
RequiredExtensions = grantType == "client_credentials"
? new HashSet<string> { "hl7-b2b" } : null,
AllowedPurposeOfUse = new HashSet<string> { /* your codes */ },
MaxPurposeOfUseCount = 1
};
public Task<AuthorizationExtensionValidationResult> ValidateAsync(
UdapAuthorizationExtensionValidationContext context)
=> Task.FromResult(AuthorizationExtensionValidationResult.Success());
}
// Register it
builder.Services.AddSingleton<ICommunityTokenValidator, MyValidator>();
See the Udap.Ssraa.Server and Udap.Tefca.Server READMEs for detailed documentation on each profile.
Client Storage During Registration
When a client registers via UDAP Dynamic Client Registration, the server creates a Duende IdentityServer Client entity with UDAP-specific secrets and properties. Understanding what is stored and when it is updated is important for admin tooling and certificate lifecycle management.
What is stored
| Storage Type | Duende Type | Key / Type Field | Value | Expiration |
|---|---|---|---|---|
| Client Secret | ClientSecret |
UDAP_SAN_URI_ISS_NAME |
The URI Subject Alternative Name (SAN) from the client's X.509 certificate — used as the issuer identity | Certificate NotAfter |
| Client Secret | ClientSecret |
UDAP_COMMUNITY |
The community ID (integer as string) the client registered under | Certificate NotAfter |
| Client Secret | ClientSecret |
X509CertificateBase64 (UDAP_X509_CERTIFICATE) |
Base64 DER-encoded public certificate from the client's x5c chain — stored for admin visibility (expiration monitoring, revocation checking) | Certificate NotAfter |
| Client Property | ClientProperty |
org |
Organization identifier — the query parameter name on the registration endpoint (see Organization / Data Holder scoping) | — |
| Client Property | ClientProperty |
data_holder |
Data holder identifier — the query parameter value on the registration endpoint (see Organization / Data Holder scoping) | — |
| Client Property | ClientProperty |
community |
The community name (URI) the client registered under — written only when ServerSettings.IncludeCommunityClaim is enabled (see Community Claim) |
— |
Standard Duende Client fields are also populated: ClientId (generated), ClientName, AllowedGrantTypes, AllowedScopes, RedirectUris, LogoUri, RequirePkce, RequireDPoP, Created.
Client identity matching
A client is uniquely identified by the combination of four values: SAN URI (UDAP_SAN_URI_ISS_NAME), community (UDAP_COMMUNITY), organization (org), and data holder (data_holder). When a registration request matches an existing client on all four, the server performs an upsert — updating scopes, grant types, redirect URIs, and the stored certificate rather than creating a new client. When any of the four differ, a new client (new client_id) is created.
Organization / Data Holder scoping
The org and data_holder properties are how a deployer controls whether multiple
registrations collapse into one client_id or stay separate. They come from a single
query parameter on the registration endpoint, using an unusual encoding:
The query parameter name becomes
org; its value becomesdata_holder.
https://as.example.com/connect/register?SurescriptsDirectory=BobsClinic
└──── org ────┘ └ data_holder ┘
→ org = "SurescriptsDirectory", data_holder = "BobsClinic"
Where the value comes from. The server reads this query string first from the
registration software statement's aud claim, then falls back to the actual POST URL
(UdapDynamicClientRegistrationValidator.ResolveOrgAndDataHolder). Because a conformant
client sets aud equal to the registration_endpoint it discovered in your metadata,
whatever query string you publish in registration_endpoint is what gets stored as
org/data_holder.
Default. If no query parameter is present, both org and data_holder default to
empty (DefaultOrgMap), so all such clients share the same org/data-holder pair.
Scope. This query parameter is read only at /connect/register. It is ignored at
/connect/token and /connect/authorize, where the client is identified by its issued
client_id and authenticated by the signed private_key_jwt client assertion.
Choosing one client_id vs. many
Because org + data_holder are part of the identity 4-tuple,
they are the lever for sharing or splitting registrations across endpoints (e.g. a client
that discovers two FHIR base URLs served by the same authorization server and community):
One
client_id, one set of scopes (per org name). If a client should resolve to a single registration across endpoints that belong to the same organization, publish the identicalorg(=data_holder) query string in theregistration_endpointof every one of those endpoints'.well-known/udapdocuments (or omit it everywhere, so all default toempty). The four values then match, the server upserts, and the originalclient_idis returned — so the client ends up with one registration and oneAllowedScopesset keyed to that org name, no matter how many endpoints it discovered.Different scopes → register again under a different key. If an endpoint needs a distinct scope set (or any distinct registration), publish a different
org=data_holderquery string for it. The differing key produces a separateclient_idwith its ownAllowedScopes, independent of the first.
In short: same org=data_holder key ⇒ one shared client_id and one scope set; a
different key ⇒ a separate client_id you can scope independently. If clients are
registering more times than you expect, diff the registration_endpoint query strings
across your metadata documents — a mismatch (including "present at one endpoint, absent at
another") is the usual cause.
Certificate rollover
UDAP allows certificate rotation without re-registration. When a client authenticates at the token endpoint with a new certificate (different from the one used at registration), the RolloverClientSecrets method is invoked by UdapJwtSecretValidator. This updates:
- The
Expirationon theUDAP_SAN_URI_ISS_NAMEandUDAP_COMMUNITYsecrets to match the new certificate'sNotAfter - The
ValueandExpirationon theX509CertificateBase64secret to reflect the new certificate
Rollover only occurs if the new certificate is currently valid (NotBefore < now < NotAfter). The existing PKI chain validation against community trust anchors is unchanged — rollover is purely a metadata update.
What is NOT stored
- The client's private key — only the public certificate is stored
- The full certificate chain — intermediates and anchors are managed separately in the UDAP trust store
- Certificate thumbprint — not stored as a separate field (can be derived from the stored certificate)
Community Claim
UDAP clients register under a specific trust community, but by default nothing surfaces that
community to a resource server. Enabling the IncludeCommunityClaim setting on ServerSettings
turns this on, with two effects:
- At registration — the community name (URI) is written to the client's
communityproperty (see the storage table above) for admin visibility. - At token time — a
udap_communityclaim is added to issued access tokens for UDAP clients, on both theclient_credentialsandauthorization_codeflows.
The claim value is resolved from the client's stored community id at token time rather than from the registration-time property, so if a community is later renamed the claim automatically reflects the new name without re-registering the client.
builder.Services.AddUdapServer(
options =>
{
var udapServerOptions = builder.Configuration.GetOption<ServerSettings>("ServerSettings");
options.DefaultSystemScopes = udapServerOptions.DefaultSystemScopes;
options.DefaultUserScopes = udapServerOptions.DefaultUserScopes;
options.IncludeCommunityClaim = udapServerOptions.IncludeCommunityClaim; // default false
},
/* ... */);
Or via configuration:
{
"ServerSettings": {
"IncludeCommunityClaim": true
}
}
The setting defaults to false, so existing tokens are unchanged unless it is explicitly enabled.
The emitted claim is unprefixed (udap_community, not client_udap_community).
Database Configuration
EF Core migration projects are available for both database providers:
- UdapDb.SqlServer — SQL Server migrations
- UdapDb.Postgres — PostgreSQL migrations
These projects create all UDAP and Duende IdentityServer tables and seed data needed for running local tests. See SeedData.cs for details.
Examples
- FHIR® is the registered trademark of HL7 and is used with the permission of HL7. Use of the FHIR trademark does not constitute endorsement of the contents of this repository by HL7.
- UDAP® and the UDAP gear logo, ecosystem gears, and green lock designs are trademarks of UDAP.org.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. 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
- Duende.IdentityServer (>= 7.4.7)
- Duende.IdentityServer.AspNetIdentity (>= 7.4.7)
- Duende.IdentityServer.EntityFramework.Storage (>= 7.4.7)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 10.0.8)
- Microsoft.AspNetCore.DataProtection.EntityFrameworkCore (>= 10.0.8)
- Microsoft.Bcl.Memory (>= 10.0.8)
- Microsoft.EntityFrameworkCore (>= 10.0.8)
- System.IdentityModel.Tokens.Jwt (>= 8.18.0)
- Udap.Client (>= 0.8.4)
- Udap.Common (>= 0.8.4)
- Udap.Model (>= 0.8.4)
- Udap.Server.Storage (>= 0.8.4)
-
net8.0
- Duende.IdentityServer (>= 7.4.7)
- Duende.IdentityServer.AspNetIdentity (>= 7.4.7)
- Duende.IdentityServer.EntityFramework.Storage (>= 7.4.7)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 8.0.27)
- Microsoft.AspNetCore.DataProtection.EntityFrameworkCore (>= 8.0.27)
- Microsoft.Bcl.Memory (>= 10.0.8)
- Microsoft.EntityFrameworkCore (>= 9.0.16)
- System.IdentityModel.Tokens.Jwt (>= 8.18.0)
- Udap.Client (>= 0.8.4)
- Udap.Common (>= 0.8.4)
- Udap.Model (>= 0.8.4)
- Udap.Server.Storage (>= 0.8.4)
-
net9.0
- Duende.IdentityServer (>= 7.4.7)
- Duende.IdentityServer.AspNetIdentity (>= 7.4.7)
- Duende.IdentityServer.EntityFramework.Storage (>= 7.4.7)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 9.0.16)
- Microsoft.AspNetCore.DataProtection.EntityFrameworkCore (>= 9.0.16)
- Microsoft.Bcl.Memory (>= 10.0.8)
- Microsoft.EntityFrameworkCore (>= 9.0.16)
- System.IdentityModel.Tokens.Jwt (>= 8.18.0)
- Udap.Client (>= 0.8.4)
- Udap.Common (>= 0.8.4)
- Udap.Model (>= 0.8.4)
- Udap.Server.Storage (>= 0.8.4)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Udap.Server:
| Package | Downloads |
|---|---|
|
Udap.UI
Package is a part of the UDAP reference implementation for .NET. |
|
|
Udap.Tefca.Server
TEFCA community-specific validators for UDAP registration and token issuance. |
|
|
Udap.Ssraa.Server
SSRAA community-specific validators for UDAP token issuance with HL7 v3 PurposeOfUse enforcement. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.4 | 47 | 5/30/2026 |
| 0.8.3 | 54 | 5/29/2026 |
| 0.8.2 | 156 | 5/22/2026 |
| 0.8.1 | 156 | 5/20/2026 |
| 0.8.0 | 160 | 5/15/2026 |
| 0.7.13 | 150 | 5/15/2026 |
| 0.7.12 | 365 | 4/1/2026 |
| 0.7.11 | 164 | 3/31/2026 |
| 0.7.10 | 154 | 3/31/2026 |
| 0.7.9 | 169 | 3/31/2026 |
| 0.7.8 | 164 | 3/30/2026 |
| 0.7.7 | 153 | 3/30/2026 |
| 0.7.6 | 158 | 3/30/2026 |
| 0.7.5 | 165 | 3/30/2026 |
| 0.7.4 | 146 | 3/29/2026 |
| 0.7.3 | 145 | 3/29/2026 |
| 0.7.2 | 153 | 3/28/2026 |
| 0.7.1 | 125 | 3/23/2026 |
| 0.7.0 | 121 | 3/22/2026 |
| 0.6.16 | 131 | 3/22/2026 |