Skip to content

Commit bcb0c46

Browse files
authored
Add support for Dynamic Client Registration (#346)
Implements full OpenID Connect Dynamic Client Registration 1.0 + RFC 7591/7592 support for v7, validated against the OpenID conformance suite. ### Features **Registration endpoint (RFC 7591 / OIDC DCR 1.0)** - Client Registration Endpoint (`POST`) — register clients dynamically with the OP assigning `client_id`, `client_secret`, and a Registration Access Token (RAT). - Configurable access control: open registration or Initial Access Token mode. - Optional "register, then approve" — config option to create dynamically registered clients **disabled** until an administrator reviews and enables them. - Configurable default scope set assigned when a registration omits `scope`. **Client Configuration Endpoint (RFC 7592)** - Read (`GET`), Update (`PUT`, full-replace semantics) and Delete (`DELETE`) of a registered client, authenticated by the Registration Access Token. - RAT is stored only as a hash; the plaintext is returned once and **rotated** on each read/update (RFC 7592 §3 requires `registration_access_token` + `registration_client_uri` in the response). - Uniform `401` on every auth failure so the endpoint never reveals whether a client exists. **Metadata validation & enforcement** - Strict validation of client-supplied metadata: `redirect_uris`, informational URIs (`logo_uri`/`client_uri`/`policy_uri`/`tos_uri`), `contacts`, `application_type`, `request_uris`, `subject_type`, `default_max_age`, `require_auth_time`, `default_acr_values`, `initiate_login_uri`, `software_id`/`software_version`. - `redirect_uris` constrained by `application_type` (native → custom-scheme/loopback only; web+implicit → https, non-localhost) and rejected if they contain a fragment (incl. empty trailing `#`). - `grant_types` / `response_types` / `token_endpoint_auth_method` rejected when outside the OP's supported sets (single source of truth shared by discovery, validator, and admin form). - `response_type` ↔ `grant_type` correspondence enforced (auto-normalized on registration, live-adjusted in the admin UI). - `default_acr_values` restricted to the OP's advertised `acr_values_supported`. - Unsupported features (encrypted/signed UserInfo & ID Tokens, pairwise `sector_identifier_uri`, front-channel logout, etc.) are rejected rather than silently ignored. - Impersonation protection (OIDC DCR §9.1) — protected informational URIs must share a host with a registered redirect URI. - Admin-only properties (e.g. `authproc`) are scrubbed from registration input and preserved across DCR updates. - `Content-Type: application/json` enforced on register/update requests. **Client model & admin UI** - Per-client metadata is presence-based and stored as the single source of truth (getters return the stored value; spec defaults applied at registration time). - `is_confidential` kept in lockstep with `token_endpoint_auth_method` / `application_type`, on both registration and admin save. - Admin form gains all client metadata fields (logo/client/policy/tos URIs, contacts, application type), a multiselect `default_acr_values` bound to supported values, and live JS for response/grant-type correspondence and public/confidential client type. **Discovery** - OP metadata now advertises the supported `response_types`, `grant_types`, and `token_endpoint_auth_methods` (including `none`). **Docs** - Supported-specs list, configuration reference, and the v6→7 upgrade guide updated, including new config options, client properties, and transition guidance for existing/pre-DCR clients.
1 parent 2244d7f commit bcb0c46

59 files changed

Lines changed: 6366 additions & 89 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,13 @@ jobs:
266266
- name: Run form_post implicit tests
267267
run: |
268268
./conformance-suite/scripts/run-test-plan.py "oidcc-formpost-implicit-certification-test-plan[server_metadata=discovery][client_registration=static_client]" ./main/conformance-tests/conformance-implicit-ci.json
269+
- name: Run Dynamic registration conformance tests
270+
# The only remaining non-passing tests are two OP-wide gaps (signed UserInfo
271+
# and signing-key rotation), which are recorded as expected failures in
272+
# conformance-tests/dynamic-warnings.json, so this step is a blocking gate.
273+
# See docs/5-oidc-conformance.md for the inventory.
274+
run: |
275+
./conformance-suite/scripts/run-test-plan.py --expected-failures-file ./main/conformance-tests/dynamic-warnings.json --expected-skips-file ./main/conformance-tests/dynamic-skips.json "oidcc-dynamic-certification-test-plan[response_type=code]" ./main/conformance-tests/conformance-dynamic-ci.json
269276
- name: Stop SSP
270277
working-directory: ./main
271278
run: |

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"psr/container": "^2.0",
3232
"psr/log": "^3",
3333
"simplesamlphp/composer-module-installer": "^1.3",
34-
"simplesamlphp/openid": "~v0.3.5",
34+
"simplesamlphp/openid": "~0.3.8",
3535
"spomky-labs/base64url": "^2.0",
3636
"symfony/expression-language": "^7.4",
3737
"symfony/psr-http-message-bridge": "^7.4",

config/module_oidc.php.dist

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,24 @@ $config = [
448448
*/
449449
ModuleConfig::OPTION_PROTOCOL_DISCOVERY_SHOW_CLAIMS_SUPPORTED => false,
450450

451+
/**
452+
* Guzzle HTTP client options for the protocol-layer outbound requests made by the underlying `openid`
453+
* library, such as fetching a client's 'jwks_uri' or a 'request_uri'. The array is passed through verbatim
454+
* to the Guzzle client; see https://docs.guzzlephp.org/en/stable/request-options.html for the full list.
455+
*
456+
* Leave empty (the default) to use the library's secure defaults (TLS verification enabled). The typical
457+
* use for a non-empty value is testing against endpoints that present self-signed certificates (for
458+
* example, the OpenID conformance suite), by disabling TLS verification:
459+
*
460+
* ModuleConfig::OPTION_PROTOCOL_HTTP_CLIENT_OPTIONS => [
461+
* 'verify' => false,
462+
* ],
463+
*
464+
* SECURITY WARNING: disabling TLS verification ('verify' => false) exposes these fetches to
465+
* man-in-the-middle attacks. Only use it in development/testing, NEVER in production.
466+
*/
467+
ModuleConfig::OPTION_PROTOCOL_HTTP_CLIENT_OPTIONS => [],
468+
451469
/**
452470
* Settings regarding Authentication Processing Filters.
453471
* Note: An OIDC authN state array will not contain all the keys which are
@@ -570,6 +588,136 @@ $config = [
570588
*/
571589
ModuleConfig::OPTION_ADMIN_UI_PAGINATION_ITEMS_PER_PAGE => 20,
572590

591+
/***************************************************************************
592+
* (optional) OpenID Connect Dynamic Client Registration (DCR) related
593+
* options. If not enabled (the default), Dynamic Client Registration
594+
* capabilities will be disabled.
595+
**************************************************************************/
596+
597+
/**
598+
* Enable or disable OpenID Connect Dynamic Client Registration (DCR), as
599+
* described in the OpenID Connect Dynamic Client Registration 1.0
600+
* specification (which is also compatible with RFC 7591). Default is
601+
* disabled (false).
602+
*
603+
* When enabled, the module serves:
604+
* - a Client Registration Endpoint (HTTP POST to .../oidc/register) which
605+
* creates a new client from the supplied client metadata and returns its
606+
* client_id, client_secret (for confidential clients), a
607+
* registration_access_token and a registration_client_uri; and
608+
* - a Client Configuration Endpoint (HTTP GET to
609+
* .../oidc/register?client_id=...) which returns the current client
610+
* registration when called with the registration_access_token as an HTTP
611+
* Bearer token.
612+
*
613+
* When enabled, the registration endpoint is also advertised as the
614+
* 'registration_endpoint' claim in the OP discovery metadata.
615+
*
616+
* Note that dynamically registered clients are stored like any other client
617+
* and are visible / manageable in the admin UI.
618+
*/
619+
ModuleConfig::OPTION_DCR_ENABLED => false,
620+
621+
/**
622+
* Access-control mode for the registration (create) endpoint. Only relevant
623+
* if Dynamic Client Registration is enabled. Possible values:
624+
*
625+
* - DcrRegistrationAuthEnum::Open (the default): open registration, meaning
626+
* anyone may register a client without authenticating. In this mode you
627+
* should protect the endpoint from abuse using rate limiting at the
628+
* web-server level.
629+
* - DcrRegistrationAuthEnum::InitialAccessToken: callers must present a
630+
* valid Initial Access Token (provisioned out-of-band) as an HTTP Bearer
631+
* token to register. The accepted tokens are configured using
632+
* the OPTION_DCR_INITIAL_ACCESS_TOKENS option below.
633+
*/
634+
ModuleConfig::OPTION_DCR_REGISTRATION_AUTH =>
635+
\SimpleSAML\Module\oidc\Codebooks\DcrRegistrationAuthEnum::Open->value,
636+
637+
/**
638+
* Allowlist of Initial Access Tokens (opaque, randomly generated strings)
639+
* accepted by the registration endpoint. This option is only consulted when
640+
* the access mode (OPTION_DCR_REGISTRATION_AUTH) is set to
641+
* DcrRegistrationAuthEnum::InitialAccessToken; in 'open' mode it is ignored.
642+
*
643+
* A registration request must then carry one of these tokens as an HTTP
644+
* Bearer token. Use long, high-entropy values and treat them as secrets.
645+
*
646+
* Format: string[] (array of strings)
647+
*/
648+
ModuleConfig::OPTION_DCR_INITIAL_ACCESS_TOKENS => [
649+
// 'a-long-random-secret-token',
650+
],
651+
652+
/**
653+
* Enable or disable impersonation protection for Dynamic Client
654+
* Registration, as recommended by Section 9.1 of the OpenID Connect Dynamic
655+
* Client Registration 1.0 specification. Default is enabled (true).
656+
*
657+
* When enabled, the host component of the logo_uri, policy_uri and tos_uri
658+
* client metadata values (if provided) must match the host of one of the
659+
* registered redirect_uris. Otherwise, the registration is rejected with an
660+
* 'invalid_client_metadata' error. This mitigates a rogue client trying to
661+
* impersonate a legitimate one by reusing its branding (logo) or links.
662+
*
663+
* You may want to disable this (set to false) if your clients legitimately
664+
* host these resources on a different domain than their redirect URIs (for
665+
* example, on a shared CDN or marketing domain). Note that the client_uri
666+
* (the client home page) is intentionally NOT subject to this check.
667+
*/
668+
ModuleConfig::OPTION_DCR_IMPERSONATION_PROTECTION_ENABLED => true,
669+
670+
/**
671+
* Default scopes assigned to a Dynamic Client Registration (DCR) client that
672+
* registers WITHOUT an explicit 'scope'. The OpenID Connect Dynamic Client
673+
* Registration 1.0 specification makes 'scope' OPTIONAL and lets the OP
674+
* assign a default set; this option controls that set.
675+
*
676+
* If this option is omitted (commented out), it defaults to ALL scopes this
677+
* OP supports, so a scope-less dynamic client may request any supported scope
678+
* (including 'offline_access', i.e. refresh tokens). To restrict what a
679+
* scope-less dynamic client receives, set an explicit list below; only values
680+
* that are actually supported by this OP are kept.
681+
*
682+
* This applies ONLY to Dynamic registrations. Manual (admin) and OpenID
683+
* Federation automatic registrations are NOT affected: a federated client
684+
* with no 'scope' in its metadata still defaults to 'openid' only.
685+
*
686+
* Note: an explicit but unsupported 'scope' in a registration request is NOT
687+
* treated as "not specified" - the unsupported values are dropped and the
688+
* client ends up with 'openid' only (it does not receive this default set).
689+
*
690+
* Format: string[] (array of scope names)
691+
*/
692+
// ModuleConfig::OPTION_DCR_DEFAULT_SCOPES => [
693+
// 'openid',
694+
// 'offline_access',
695+
// ],
696+
697+
/**
698+
* Whether a client registered through Dynamic Client Registration (DCR) is
699+
* created ENABLED and therefore immediately usable.
700+
*
701+
* When true (default), a dynamically registered client can be used right
702+
* away. Set to false to create such clients DISABLED, so an administrator
703+
* must review and enable them in the admin UI before they can complete
704+
* authorization / token flows ("register, then approve"). While disabled, the
705+
* client can still read and manage its own registration (RFC 7592) using the
706+
* registration access token it received - it simply cannot obtain tokens until
707+
* an administrator enables it.
708+
*
709+
* Note: there is no standard way to signal "pending approval" back to the
710+
* client in the registration response (it receives a normal success response),
711+
* so you may need to communicate the review step out-of-band.
712+
*
713+
* This applies ONLY to Dynamic registrations. OpenID Federation automatic
714+
* registrations are always created enabled (they are vouched for by their
715+
* trust chain).
716+
*
717+
* Format: bool (default: true)
718+
*/
719+
ModuleConfig::OPTION_DCR_REGISTERED_CLIENTS_ENABLED => true,
720+
573721
/***************************************************************************
574722
* (optional) OpenID Federation-related options. If these are not set,
575723
* OpenID Federation capabilities will be disabled.

0 commit comments

Comments
 (0)