Udap.Server
0.8.5
dotnet add package Udap.Server --version 0.8.5
NuGet\Install-Package Udap.Server -Version 0.8.5
<PackageReference Include="Udap.Server" Version="0.8.5" />
<PackageVersion Include="Udap.Server" Version="0.8.5" />Directory.Packages.props
<PackageReference Include="Udap.Server" />Project file
paket add Udap.Server --version 0.8.5
#r "nuget: Udap.Server, 0.8.5"
#:package Udap.Server@0.8.5
#addin nuget:?package=Udap.Server&version=0.8.5Install as a Cake Addin
#tool nuget:?package=Udap.Server&version=0.8.5Install as a Cake Tool
Udap.Server
UDAP logo
๐ฆ NuGet Package: Udap.Server
This package adds UDAP Dynamic Client Registration (DCR) and metadata capabilities to authorization servers 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 1ใใซM 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
The example below shows a typical setup. 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
Install 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- The validator 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. Knowing what is stored (and when it is updated) helps with 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) |
โ |
Other standard Duende Client fields are also populated: ClientId (generated), ClientName, AllowedGrantTypes, AllowedScopes, RedirectUris, LogoUri, RequirePkce, RequireDPoP, and 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), UdapJwtSecretValidator invokes RolloverClientSecrets. 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). 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 required to run 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.5)
- Udap.Common (>= 0.8.5)
- Udap.Model (>= 0.8.5)
- Udap.Server.Storage (>= 0.8.5)
-
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.5)
- Udap.Common (>= 0.8.5)
- Udap.Model (>= 0.8.5)
- Udap.Server.Storage (>= 0.8.5)
-
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.5)
- Udap.Common (>= 0.8.5)
- Udap.Model (>= 0.8.5)
- Udap.Server.Storage (>= 0.8.5)
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.5 | 168 | 6/16/2026 |
| 0.8.4 | 166 | 5/30/2026 |
| 0.8.3 | 188 | 5/29/2026 |
| 0.8.2 | 167 | 5/22/2026 |
| 0.8.1 | 168 | 5/20/2026 |
| 0.8.0 | 183 | 5/15/2026 |
| 0.7.13 | 161 | 5/15/2026 |
| 0.7.12 | 374 | 4/1/2026 |
| 0.7.11 | 173 | 3/31/2026 |
| 0.7.10 | 162 | 3/31/2026 |
| 0.7.9 | 177 | 3/31/2026 |
| 0.7.8 | 171 | 3/30/2026 |
| 0.7.7 | 161 | 3/30/2026 |
| 0.7.6 | 167 | 3/30/2026 |
| 0.7.5 | 173 | 3/30/2026 |
| 0.7.4 | 156 | 3/29/2026 |
| 0.7.3 | 152 | 3/29/2026 |
| 0.7.2 | 162 | 3/28/2026 |
| 0.7.1 | 135 | 3/23/2026 |
| 0.7.0 | 131 | 3/22/2026 |