Skip to content

Commit 0e1bc95

Browse files
committed
feat(verifier): protect dynamic client registration with static/JWT auth
- Add verifier OIDC dynamic_registration_auth config - Support modes: open, static(file bearer token), jwt(JWKS validation) - Wire auth middleware to POST /register endpoint - Keep introspection as reserved/not implemented - Add middleware tests for auth modes and bearer handling - Add config examples in config.yaml and README - Regenerate docs/CONFIGURATION.md
1 parent 988c54e commit 0e1bc95

File tree

7 files changed

+635
-19
lines changed

7 files changed

+635
-19
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,42 @@ determined by `auth_method` in the credential configuration.
207207
- OpenTelemetry distributed tracing
208208
- Static Linux/amd64 binaries for containerized deployment
209209

210+
## Verifier Dynamic Client Registration Authorization
211+
212+
The verifier exposes OAuth 2.0 Dynamic Client Registration at `POST /register`.
213+
You can protect this endpoint with an initial access token policy.
214+
215+
Supported modes:
216+
217+
- `open` (default): registration is open (rate-limited)
218+
- `static`: requires a fixed bearer token loaded from local file
219+
- `jwt`: requires a signed JWT bearer token validated with configured `issuer`, `audience`, and `jwks_uri`
220+
221+
Example (`config.yaml`):
222+
223+
```yaml
224+
verifier:
225+
oidc:
226+
dynamic_registration_auth:
227+
mode: "static"
228+
static_bearer_token_file: "/run/secrets/verifier_dcr_initial_access_token"
229+
```
230+
231+
```yaml
232+
verifier:
233+
oidc:
234+
dynamic_registration_auth:
235+
mode: "jwt"
236+
jwt:
237+
jwks_uri: "https://auth.example.com/.well-known/jwks.json"
238+
issuer: "https://auth.example.com"
239+
audience: "vc-verifier-register"
240+
allowed_signing_algs: ["RS256", "ES256"]
241+
clock_skew_seconds: 60
242+
```
243+
244+
Note: `introspection` is reserved for future implementation and is not enabled yet.
245+
210246
## Docker release version
211247

212248
`latest` tracks the latest tag available and is built from branch `main`.

config.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,28 @@ verifier:
191191
# response_types: # Optional, default: ["code"]
192192
# - "code"
193193
# client_name: "Example Static Client" # Optional
194+
195+
# Dynamic Client Registration authorization policy for POST /register
196+
# Modes:
197+
# - open : no additional authorization (default behavior)
198+
# - static : require a fixed bearer token from local file
199+
# - jwt : require signed JWT bearer token validated via JWKS
200+
# - introspection: reserved for future implementation (not yet supported)
201+
#
202+
# Example 1: static token from file
203+
# dynamic_registration_auth:
204+
# mode: "static"
205+
# static_bearer_token_file: "/run/secrets/verifier_dcr_initial_access_token"
206+
#
207+
# Example 2: JWT bearer token validation
208+
# dynamic_registration_auth:
209+
# mode: "jwt"
210+
# jwt:
211+
# jwks_uri: "https://auth.example.com/.well-known/jwks.json"
212+
# issuer: "https://auth.example.com"
213+
# audience: "vc-verifier-register"
214+
# allowed_signing_algs: ["RS256", "ES256"]
215+
# clock_skew_seconds: 60
194216
openid4vp:
195217
presentation_timeout: 300
196218
# Template-based presentation requests (optional)

docs/CONFIGURATION.md

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Configuration Reference
22

3-
**Generated:** 2026-02-26
3+
**Generated:** 2026-03-06
44

55
Complete reference for all configuration parameters in the VC system.
66

@@ -611,16 +611,58 @@ This configures how the verifier issues ID tokens and access tokens to relying p
611611
Note: This is NOT related to verifiable credential issuance (see IssuerConfig for VC issuance).
612612
The signing key is shared from the parent Verifier.KeyConfig.
613613

614-
| Field | Type | Description | Example | Default | Required |
615-
| ------------------------ | -------- | -------------------------------------------------------------------------- | ----------------------------- | ------- | -------- |
616-
| `issuer` | `string` | OIDC Provider identifier that appears in ID tokens and discovery metadata. | `"https://verifier.sunet.se"` | - | Yes |
617-
| `session_duration` | `int` | Session duration in seconds | - | `3600` | No |
618-
| `code_duration` | `int` | Authorization code duration in seconds | - | `300` | No |
619-
| `access_token_duration` | `int` | Access token duration in seconds | - | `3600` | No |
620-
| `id_token_duration` | `int` | ID token duration in seconds | - | `3600` | No |
621-
| `refresh_token_duration` | `int` | Refresh token duration in seconds | - | `86400` | No |
622-
| `subject_type` | `string` | Subject type: "public" or "pairwise" | - | - | Yes |
623-
| `subject_salt` | `string` | Salt for pairwise subject generation | - | - | Yes |
614+
| Field | Type | Description | Example | Default | Required |
615+
| --------------------------- | -------- | -------------------------------------------------------------------------- | ----------------------------- | ------- | -------- |
616+
| `issuer` | `string` | OIDC Provider identifier that appears in ID tokens and discovery metadata. | `"https://verifier.sunet.se"` | - | Yes |
617+
| `session_duration` | `int` | Session duration in seconds | - | `3600` | No |
618+
| `code_duration` | `int` | Authorization code duration in seconds | - | `300` | No |
619+
| `access_token_duration` | `int` | Access token duration in seconds | - | `3600` | No |
620+
| `id_token_duration` | `int` | ID token duration in seconds | - | `3600` | No |
621+
| `refresh_token_duration` | `int` | Refresh token duration in seconds | - | `86400` | No |
622+
| `subject_type` | `string` | Subject type: "public" or "pairwise" | - | - | Yes |
623+
| `subject_salt` | `string` | Salt for pairwise subject generation | - | - | Yes |
624+
| `static_clients` | `array` | List of pre-configured OIDC clients | - | - | No |
625+
| `dynamic_registration_auth` | `object` | Authorization for POST /register (RFC 7591). | - | - | No |
626+
627+
### `static_clients` entry
628+
629+
> **Path:** `.verifier.oidc.static_clients[]`
630+
631+
Static clients are configured in YAML and do not require dynamic registration.
632+
These clients are checked in addition to dynamically registered clients stored in the database.
633+
634+
| Field | Type | Description | Example | Default | Required |
635+
| ---------------------------- | ---------- | ------------------------------------------------- | ------- | ------------------------ | -------- |
636+
| `client_id` | `string` | Unique identifier for the client | - | - | Yes |
637+
| `client_secret` | `string` | Client secret for authentication. | - | - | No |
638+
| `redirect_uris` | `[]string` | List of allowed redirect URIs for this client | - | - | Yes |
639+
| `allowed_scopes` | `[]string` | List of scopes this client is allowed to request. | - | - | No |
640+
| `token_endpoint_auth_method` | `string` | Authentication method for the token endpoint. | - | `client_secret_basic` | No |
641+
| `grant_types` | `[]string` | List of allowed grant types. | - | `["authorization_code"]` | No |
642+
| `response_types` | `[]string` | List of allowed response types. | - | `["code"]` | No |
643+
| `client_name` | `string` | Optional human-readable name for the client | - | - | No |
644+
645+
### `dynamic_registration_auth`
646+
647+
> **Path:** `.verifier.oidc.dynamic_registration_auth`
648+
649+
| Field | Type | Description | Example | Default | Required |
650+
| -------------------------- | -------- | ------------------------------------------------------------------------------------------ | ------- | ------- | -------- |
651+
| `mode` | `string` | Registration authorization behavior. | - | `open` | No |
652+
| `static_bearer_token_file` | `string` | StaticBearerTokenFile points to a file containing the expected bearer token (single line). | - | - | No |
653+
| `jwt` | `object` | JWT config for Mode=jwt. | - | - | No |
654+
655+
### `jwt`
656+
657+
> **Path:** `.verifier.oidc.dynamic_registration_auth.jwt`
658+
659+
| Field | Type | Description | Example | Default | Required |
660+
| ---------------------- | ---------- | ------------------------------------------------------------- | ------- | ------------------- | -------- |
661+
| `jwks_uri` | `string` | URL to fetch signing keys from. | - | - | Yes |
662+
| `issuer` | `string` | Required issuer claim (iss). | - | - | Yes |
663+
| `audience` | `string` | Required audience claim (aud). | - | - | Yes |
664+
| `allowed_signing_algs` | `[]string` | AllowedSigningAlgs restricts accepted JWT signing algorithms. | - | `["RS256","ES256"]` | No |
665+
| `clock_skew_seconds` | `int` | Tolerated clock skew for exp/nbf/iat validation. | - | `60` | No |
624666

625667
### `openid4vp`
626668

@@ -687,12 +729,15 @@ The signing key is shared from the parent Verifier.KeyConfig.
687729
688730
This is used for validating W3C VC Data Integrity proofs and other trust-related operations.
689731

690-
| Field | Type | Description | Example | Default | Required |
691-
| ------------------- | ---------- | ----------------------------------------------------------- | ------------------------------ | ------------------------ | -------- |
692-
| `go_trust_url` | `string` | URL of the go-trust PDP (Policy Decision Point) service. | `"https://trust.sunet.se/pdp"` | - | No |
693-
| `local_did_methods` | `[]string` | Which DID methods can be resolved locally without go-trust. | - | `["did:key", "did:jwk"]` | No |
694-
| `trust_policies` | `object` | Per-role trust evaluation policies. | - | - | No |
695-
| `enable` | `bool` | Whether trust evaluation is enabled. | - | `true` | No |
732+
Trust evaluation operates in one of two modes:
733+
- When PDPURL is configured: "default deny" mode - all trust decisions go through the PDP
734+
- When PDPURL is empty: "allow all" mode - keys are resolved but always considered trusted
735+
736+
| Field | Type | Description | Example | Default | Required |
737+
| ------------------- | ---------- | ---------------------------------------------------------------------------- | ------------------------------ | ------------------------ | -------- |
738+
| `pdp_url` | `string` | URL of the AuthZEN PDP (Policy Decision Point) service for trust evaluation. | `"https://trust.sunet.se/pdp"` | - | No |
739+
| `local_did_methods` | `[]string` | Which DID methods can be resolved locally without go-trust. | - | `["did:key", "did:jwk"]` | No |
740+
| `trust_policies` | `object` | Per-role trust evaluation policies. | - | - | No |
696741

697742
### `trust_policies` entry
698743

@@ -737,7 +782,7 @@ Configuration for the Registry service that manages credential status.
737782
738783
| Field | Type | Description | Example | Default | Required |
739784
| ---------- | -------- | -------------- | ------- | ------- | ---------------- |
740-
| `enable` | `bool` | The admin GUI | - | `true` | No |
785+
| `enable` | `bool` | The admin GUI | - | `false` | No |
741786
| `username` | `string` | Admin username | - | `admin` | Yes (if enabled) |
742787
| `password` | `string` | Admin password | - | - | Yes (if enabled) |
743788

internal/verifier/httpserver/service.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Service struct {
3939
tokenLimiter *middleware.RateLimiter
4040
authorizeLimiter *middleware.RateLimiter
4141
registerLimiter *middleware.RateLimiter
42+
registerAuth gin.HandlerFunc
4243
}
4344

4445
// New creates a new httpserver service
@@ -60,6 +61,7 @@ func New(ctx context.Context, cfg *model.Cfg, apiv1 *apiv1.Client, notify *notif
6061
tokenLimiter: middleware.NewRateLimiter(rateLimitConfig.TokenRequestsPerMinute, rateLimitConfig.TokenBurst),
6162
authorizeLimiter: middleware.NewRateLimiter(rateLimitConfig.AuthorizeRequestsPerMinute, rateLimitConfig.AuthorizeBurst),
6263
registerLimiter: middleware.NewRateLimiter(rateLimitConfig.RegisterRequestsPerMinute, rateLimitConfig.RegisterBurst),
64+
registerAuth: func(c *gin.Context) { c.Next() },
6365
sessionsOptions: sessions.Options{
6466
Path: "/",
6567
Domain: "",
@@ -85,6 +87,11 @@ func New(ctx context.Context, cfg *model.Cfg, apiv1 *apiv1.Client, notify *notif
8587
return nil, err
8688
}
8789

90+
s.registerAuth, err = middleware.NewRegistrationAuthMiddleware(s.cfg, s.log.New("registration_auth"))
91+
if err != nil {
92+
return nil, err
93+
}
94+
8895
rgRoot, err := s.httpHelpers.Server.Default(ctx, s.server, s.gin, s.cfg.Verifier.APIServer.Addr)
8996
if err != nil {
9097
return nil, err
@@ -153,7 +160,7 @@ func New(ctx context.Context, cfg *model.Cfg, apiv1 *apiv1.Client, notify *notif
153160
})
154161

155162
// Dynamic Client Registration (RFC 7591/7592) with rate limiting
156-
rgRoot.POST("register", s.registerLimiter.Middleware(), func(c *gin.Context) {
163+
rgRoot.POST("register", s.registerLimiter.Middleware(), s.registerAuth, func(c *gin.Context) {
157164
response, err := s.endpointRegisterClient(ctx, c)
158165
if err != nil {
159166
s.handleOAuthError(c, err)

0 commit comments

Comments
 (0)