Skip to content

Should You Extend Duende's DynamicClientRegistrationValidator #812

Description

@JoeShook

TL;DR

No. The overlap is too small and the fundamental models are too different.
You'd override 80%+ of the behavior, which isn't "extending" — it's replacing
while pretending to extend.


Fundamental Mismatches

1. Request Model

Duende's endpoint parses JSON directly into DynamicClientRegistrationRequest
with individual fields (grant_types, redirect_uris, scope, etc.). UDAP's
request is a software_statement JWT that must be decoded, signature-verified
against an X.509 certificate, and chain-validated before you even get to those
fields. You'd have to either:

  • Override the endpoint to do JWT parsing before the validator sees it, then map
    claims into Duende's request model
  • Or override ValidateSoftwareStatementAsync to do ALL the heavy lifting, then
    back-populate context.Request

Either way, you're fighting the data flow.

2. Authentication Model

Duende's context requires a ClaimsPrincipal caller — the authenticated entity
making the request. UDAP registration is anonymous HTTP; the certificate chain
in the JWT IS the authentication. You'd need to pass a dummy/synthetic
principal.

3. Client Lifecycle

Duende only creates new clients (HTTP 201). UDAP has three outcomes:

Outcome HTTP Status
New registration 201 Created
Upsert (same cert re-registers) 200 OK
Cancellation (grant_types: []) 200 OK

The processor and response generator have no concept of upsert or cancellation.
You'd override ProcessAsync entirely to add these, and override the response
generator for status code selection.

4. Secret Model

Duende generates random hashed secrets (CryptoRandom → SHA256). UDAP stores
certificate metadata as secrets:

  • UDAP_SAN_URI_ISS_NAME — the SAN URI from the cert
  • UDAP_COMMUNITY — resolved community ID
  • Both expire when the certificate expires

You'd override AddClientSecret and GenerateSecret completely.

5. Trust Infrastructure

UDAP needs community trust anchors, intermediate certificates, and
TrustChainValidator — none of which exist in Duende's DCR. These would need
to be injected into your subclass and called during validation.


Override Analysis

Duende Virtual Method Override? Reason
ValidateSoftwareStatementAsync Yes, completely Becomes the entire UDAP JWT + cert chain validation
SetGrantTypesAsync Yes Cancel registration semantics, different error handling
SetRedirectUrisAsync Partially Similar logic, but UDAP-specific error messages
SetScopesAsync Yes Wildcard expansion, aggregation
SetDefaultScopes Yes UDAP server settings for default scopes
SetSecretsAsync Yes, completely JWK-based → X.509-based secrets
SetClientNameAsync Minimal Similar
SetLogoutParametersAsync No-op UDAP doesn't use these
SetMaxAgeAsync No-op UDAP doesn't use this
SetUserInterfaceProperties Partially Logo validation is much stricter
SetPublicClientProperties No-op N/A for UDAP
SetAccessTokenProperties No-op N/A
SetIdTokenProperties No-op N/A
SetServerSideSessionProperties No-op N/A
Processor ProcessAsync Yes, completely Upsert, cancel, UDAP secrets
Processor AddClientId Partially Similar
Processor GenerateSecret Yes, completely Certificate-based, not random
Response generator (all 5 methods) Yes Status code selection, UDAP response format

Every single method is either overridden or no-op'd. At that point you're
not extending — you're wearing a Duende costume.


Architecture Comparison

Both pipelines already follow the same pattern:

Duende: Endpoint → Context → Validator → Processor → ResponseGenerator
UDAP: Endpoint → Context → Validator → Processor → (inline response)

The architectural alignment is already there. The class hierarchy doesn't need
to match.


UDAP vs RFC 7591: Why They're Fundamentally Different

Aspect RFC 7591 (Duende) UDAP DCR
Authentication API key/secret or OAuth token X.509 certificates (PKI-based)
Software Statement Optional, can contain metadata Mandatory, contains ALL metadata, signed with cert
Metadata Location Top-level JSON fields MUST be inside JWT (software_statement)
Issuer Validation Not defined MUST match cert SAN URI
Replay Protection Optional JTI mandatory (when configured)
Token Lifetime Not constrained Max 5 minutes (exp - iat)
Trust Model Single explicit trust Community-based PKI with trust anchors
Secret Storage Long-lived hashed secrets Certificate metadata, auto-expiring
Audience Validation Optional Mandatory, must match endpoint URL
Cancellation Not defined Empty grant_types array
Re-registration Not defined Upsert with same cert
Status Codes 201 create, 400 error 201 create, 200 update/cancel, 400 error
Scopes Static validation Wildcard expansion and aggregation
Community ID N/A Resolved from cert chain, stored as secret
Subject Claim Not required Must exist AND must equal issuer

UDAP DCR is not an extension of RFC 7591 — it's a PKI-based alternative
that happens to share some concepts. The overlap is roughly 20-30%.


When Extending WOULD Make Sense

  • If Duende adds UDAP-specific hooks (unlikely)
  • If UDAP DCR evolves closer to standard RFC 7591 (unlikely — PKI-first is the
    whole point)
  • If Duende adds features UDAP clients would actually use (DPoP, new grant
    types) — but inheriting behavior you don't understand is a liability

Recommendation

Keep the current separate architecture. The refactoring already achieved the
valuable goal: clean separation of validation from client creation, following
the same patterns Duende uses. The codebase is understandable to anyone familiar
with Duende's DCR, without the fragility of subclassing it.

If You Want to Reduce Maintenance Burden

  • Share error constants — Duende's DynamicClientRegistrationErrors uses
    the same RFC 7591 error codes (invalid_client_metadata,
    invalid_software_statement, etc.)
  • Consider IClientConfigurationStore for persistence, if your
    IUdapClientRegistrationStore with upsert/cancel isn't too specialized
  • Keep architectural alignment — same patterns make it easy to reason about
    both systems side-by-side

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions