safire 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/release-safire/SKILL.md +153 -0
  3. data/.rubocop.yml +11 -1
  4. data/.tool-versions +1 -1
  5. data/CHANGELOG.md +107 -1
  6. data/CONTRIBUTION.md +6 -1
  7. data/Gemfile +1 -2
  8. data/Gemfile.lock +47 -37
  9. data/README.md +60 -5
  10. data/ROADMAP.md +61 -12
  11. data/docs/Gemfile.lock +49 -45
  12. data/docs/_config.yml +2 -2
  13. data/docs/adr/ADR-001-activesupport-dependency.md +2 -2
  14. data/docs/adr/ADR-002-facade-and-forwardable.md +21 -6
  15. data/docs/adr/ADR-003-protocol-vs-client-type.md +13 -6
  16. data/docs/adr/ADR-004-clientconfig-immutability-and-entity-masking.md +34 -8
  17. data/docs/adr/ADR-006-lazy-discovery.md +64 -5
  18. data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +40 -10
  19. data/docs/adr/ADR-009-oauth-error-hierarchy.md +131 -0
  20. data/docs/adr/ADR-010-optional-client-id-dcr-temp-client.md +90 -0
  21. data/docs/adr/ADR-011-udap-stu2-discovery-conformance.md +113 -0
  22. data/docs/adr/ADR-012-udap-signed-metadata-validation.md +104 -0
  23. data/docs/adr/ADR-013-udap-registration-request-model.md +106 -0
  24. data/docs/adr/ADR-014-udap-software-statement-signing.md +122 -0
  25. data/docs/adr/index.md +9 -3
  26. data/docs/advanced.md +22 -25
  27. data/docs/configuration/client-setup.md +126 -10
  28. data/docs/configuration/index.md +11 -7
  29. data/docs/index.md +13 -6
  30. data/docs/installation.md +3 -2
  31. data/docs/security.md +44 -5
  32. data/docs/smart-on-fhir/backend-services/index.md +2 -2
  33. data/docs/smart-on-fhir/backend-services/token-request.md +1 -1
  34. data/docs/smart-on-fhir/confidential-asymmetric/index.md +2 -2
  35. data/docs/smart-on-fhir/confidential-symmetric/index.md +1 -1
  36. data/docs/smart-on-fhir/discovery/capability-checks.md +7 -0
  37. data/docs/smart-on-fhir/dynamic-client-registration/index.md +103 -0
  38. data/docs/smart-on-fhir/dynamic-client-registration/registration.md +160 -0
  39. data/docs/smart-on-fhir/dynamic-client-registration/response.md +161 -0
  40. data/docs/smart-on-fhir/index.md +2 -1
  41. data/docs/smart-on-fhir/post-based-authorization.md +1 -1
  42. data/docs/smart-on-fhir/public-client/index.md +1 -1
  43. data/docs/troubleshooting/auth-errors.md +20 -0
  44. data/docs/troubleshooting/client-errors.md +143 -0
  45. data/docs/troubleshooting/index.md +61 -4
  46. data/docs/udap/dynamic-client-registration/index.md +198 -0
  47. data/docs/udap/dynamic-client-registration/lifecycle.md +115 -0
  48. data/docs/udap/dynamic-client-registration/registration-metadata.md +133 -0
  49. data/docs/udap/dynamic-client-registration/software-statement.md +115 -0
  50. data/docs/udap.md +187 -64
  51. data/gemfiles/activesupport_71.gemfile +30 -0
  52. data/gemfiles/activesupport_71.gemfile.lock +291 -0
  53. data/lib/safire/client.rb +141 -44
  54. data/lib/safire/client_config.rb +114 -35
  55. data/lib/safire/client_config_builder.rb +18 -0
  56. data/lib/safire/errors.rb +100 -44
  57. data/lib/safire/http_client.rb +7 -2
  58. data/lib/safire/middleware/https_only_redirects.rb +10 -4
  59. data/lib/safire/protocols/behaviours.rb +12 -1
  60. data/lib/safire/protocols/oauth_response_handling.rb +48 -0
  61. data/lib/safire/protocols/smart.rb +108 -36
  62. data/lib/safire/protocols/smart_metadata.rb +6 -0
  63. data/lib/safire/protocols/udap.rb +534 -0
  64. data/lib/safire/protocols/udap_metadata.rb +429 -0
  65. data/lib/safire/protocols/udap_registration_metadata.rb +368 -0
  66. data/lib/safire/protocols/udap_signed_metadata_validator.rb +349 -0
  67. data/lib/safire/protocols/udap_software_statement.rb +348 -0
  68. data/lib/safire/protocols.rb +6 -0
  69. data/lib/safire/uri_validation.rb +102 -0
  70. data/lib/safire/version.rb +1 -1
  71. data/lib/safire.rb +2 -0
  72. data/safire.gemspec +7 -8
  73. metadata +42 -13
@@ -2,7 +2,7 @@
2
2
  layout: default
3
3
  title: Confidential Symmetric Client Workflow
4
4
  parent: SMART
5
- nav_order: 3
5
+ nav_order: 4
6
6
  has_children: true
7
7
  permalink: /smart-on-fhir/confidential-symmetric/
8
8
  ---
@@ -53,6 +53,10 @@ metadata.supports_openid_connect?
53
53
  metadata.supports_post_based_authorization?
54
54
  # => true if capabilities include "authorize-post"
55
55
 
56
+ # Dynamic Client Registration (RFC 7591)
57
+ metadata.supports_dynamic_registration?
58
+ # => true if registration_endpoint is present in the server's SMART metadata
59
+
56
60
  # Backend Services (client_credentials grant, no user interaction)
57
61
  metadata.supports_backend_services?
58
62
  # => true if:
@@ -63,6 +67,9 @@ metadata.supports_backend_services?
63
67
  {: .note }
64
68
  > `supports_backend_services?` checks `grant_types_supported` rather than a capability flag — it combines a grant type check with `supports_asymmetric_auth?` because the SMART Backend Services flow always authenticates via JWT assertion (`private_key_jwt`).
65
69
 
70
+ {: .note }
71
+ > `supports_dynamic_registration?` checks only for the presence of `registration_endpoint` in the server's SMART metadata. SMART App Launch 2.2.0 defines no dedicated capability string for DCR, so the endpoint field is the sole signal.
72
+
66
73
  ---
67
74
 
68
75
  ## Flag-Only Checks (`*_capability?`)
@@ -0,0 +1,103 @@
1
+ ---
2
+ layout: default
3
+ title: Dynamic Client Registration
4
+ parent: SMART
5
+ nav_order: 2
6
+ has_children: true
7
+ permalink: /smart-on-fhir/dynamic-client-registration/
8
+ description: "Register your Ruby application with a SMART authorization server at runtime using the OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591). Safire handles endpoint discovery, metadata submission, and returns the assigned client_id and credentials."
9
+ ---
10
+
11
+ # Dynamic Client Registration
12
+ {: .no_toc }
13
+
14
+ <div class="code-example" markdown="1">
15
+ Obtain a `client_id` by registering your application with a SMART authorization server at runtime using the OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591). Safire handles endpoint discovery and metadata submission through `Client#register_client`.
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## Overview
21
+
22
+ Before your application can run any SMART authorization flow, it needs a `client_id`. This is a one-time, out-of-band setup step: the client presents its metadata to the authorization server, and the server issues credentials in return.
23
+
24
+ The response always includes a `client_id`. For confidential symmetric clients, the server also issues a `client_secret`. For clients using asymmetric authentication (`private_key_jwt`), no secret is issued because the client authenticates with a signed JWT, but the client must include a `jwks_uri` or inline JWKS in its registration metadata so that the server can obtain the client's public keys for verifying those assertions.
25
+
26
+ There are two ways to accomplish registration:
27
+
28
+ - **Manual registration** — you register through the server's developer portal, email exchange, or administrative interface, and hard-code the assigned credentials into your configuration.
29
+ - **Dynamic Client Registration (RFC 7591)** — your application POSTs its metadata to a registration endpoint at runtime and receives credentials in the response. No portal visit or manual coordination is required.
30
+
31
+ SMART App Launch 2.2.0 does not mandate DCR. The specification states that "SMART does not specify a standards-based registration process, but we encourage EHR implementers to consider the OAuth 2.0 Dynamic Client Registration Protocol for an out-of-the-box solution." In practice, many servers support manual registration only, while others advertise a `registration_endpoint` in their SMART metadata and accept RFC 7591 requests. Safire implements DCR through [`Client#register_client`]({{ site.baseurl }}/api/Safire/Client.html).
32
+
33
+ ---
34
+
35
+ ## Where Registration Fits in the SMART Flow
36
+
37
+ Client registration is always Step 1 in the SMART App Launch sequence. It is a one-time prerequisite that happens separately from the runtime authorization interaction.
38
+
39
+ ```
40
+ 1. Registration — obtain credentials out-of-band (DCR or manual)
41
+ 2. Discovery — fetch /.well-known/smart-configuration (lazy: on first auth call)
42
+ 3. Authorization — run the appropriate SMART App Launch or Backend Services flow
43
+ ```
44
+
45
+ If you already hold a `client_id` from a prior registration, skip to the [Discovery]({% link smart-on-fhir/discovery/index.md %}) guide or directly to the authorization guide that matches your client type.
46
+
47
+ ---
48
+
49
+ ## When to Use DCR vs. Manual Registration
50
+
51
+ | Situation | Recommended path |
52
+ |-----------|-----------------|
53
+ | Server advertises `registration_endpoint` in SMART metadata | DCR |
54
+ | Automated deployment or multi-tenant provisioning | DCR |
55
+ | Server requires administrator approval before issuing credentials | Manual |
56
+ | No `registration_endpoint` in SMART metadata | Manual (or pass the endpoint explicitly to `register_client`) |
57
+ | You already have a `client_id` | Skip registration entirely |
58
+
59
+ ---
60
+
61
+ ## How DCR Works
62
+
63
+ ```mermaid
64
+ sequenceDiagram
65
+ participant App
66
+ participant Safire
67
+ participant AS as Authorization Server
68
+
69
+ Note over App,AS: One-time setup (out-of-band)
70
+
71
+ App->>Safire: Client.new(base_url:)
72
+
73
+ App->>Safire: register_client(metadata)
74
+ Safire->>AS: GET /.well-known/smart-configuration
75
+ AS-->>Safire: SmartMetadata (includes registration_endpoint)
76
+ Safire->>AS: POST /register<br/>Content-Type: application/json<br/>{ client_name, redirect_uris, grant_types,<br/> token_endpoint_auth_method, jwks_uri, ... }
77
+ AS-->>Safire: { client_id: "dyn_abc123",<br/> client_secret: "..." (symmetric only), ... }
78
+ Safire-->>App: registration response Hash
79
+
80
+ Note over App: Store credentials durably
81
+
82
+ Note over App,AS: Runtime (per user session or scheduled job)
83
+
84
+ App->>Safire: Client.new(base_url:, client_id: "dyn_abc123", ...)
85
+ Note over App: Proceed with the appropriate authorization flow
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Prerequisites
91
+
92
+ - **Registration endpoint** — The authorization server must expose a `registration_endpoint` in its SMART configuration, or you must know the URL and supply it explicitly through the `registration_endpoint:` keyword argument.
93
+ - **Initial access token** — Some servers protect the registration endpoint with a bearer token (RFC 7591 §3.1). If required, obtain this out-of-band from the server operator and pass it through the `authorization:` keyword argument.
94
+ - **JWKS or JWKS URI** — If your client will use `private_key_jwt` authentication, you must include either a `jwks_uri` pointing to your public key endpoint or an inline `jwks` object in the registration metadata. The server uses these keys to verify your JWT assertions during token requests.
95
+ - **Accurate redirect URIs** — The `redirect_uris` field must contain the exact URIs the server will accept during authorization callbacks.
96
+
97
+ ---
98
+
99
+ ## What's Next
100
+
101
+ [Calling register_client]({% link smart-on-fhir/dynamic-client-registration/registration.md %}) covers assembling client metadata, choosing grant types and authentication methods, endpoint discovery, and passing an initial access token.
102
+
103
+ [Registration Response]({% link smart-on-fhir/dynamic-client-registration/response.md %}) covers what the server returns, how to persist credentials, how to build a runtime `Safire::Client` from the response, and error handling.
@@ -0,0 +1,160 @@
1
+ ---
2
+ layout: default
3
+ title: Calling register_client
4
+ parent: Dynamic Client Registration
5
+ grand_parent: SMART
6
+ nav_order: 1
7
+ permalink: /smart-on-fhir/dynamic-client-registration/registration/
8
+ description: "How to call Client#register_client: assembling client metadata, choosing grant types and authentication methods, endpoint discovery, and passing an initial access token."
9
+ ---
10
+
11
+ # Calling register_client
12
+ {: .no_toc }
13
+
14
+ <div class="code-example" markdown="1">
15
+ Call `Client#register_client` to POST your application's metadata to the authorization server's registration endpoint and receive a `client_id` in response.
16
+ </div>
17
+
18
+ ## Table of contents
19
+ {: .no_toc .text-delta }
20
+
21
+ 1. TOC
22
+ {:toc}
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ Create a `Safire::Client` without a `client_id`, call `register_client` with your application's metadata, and then build a fully configured client using the credentials the server returns.
29
+
30
+ ```ruby
31
+ # Step 1 — temporary client for registration only (no client_id needed)
32
+ temp_client = Safire::Client.new({ base_url: 'https://fhir.example.com' })
33
+
34
+ # Step 2 — register and receive credentials
35
+ registration = temp_client.register_client(
36
+ {
37
+ client_name: 'My FHIR App',
38
+ redirect_uris: ['https://myapp.example.com/callback'],
39
+ grant_types: ['authorization_code'],
40
+ token_endpoint_auth_method: 'none',
41
+ scope: 'openid profile patient/*.read'
42
+ }
43
+ )
44
+ # => { "client_id" => "dyn_abc123", "client_name" => "My FHIR App", ... }
45
+
46
+ # Step 3 — persist credentials durably (database, secrets manager, etc.)
47
+ client_id = registration['client_id'] # always present
48
+ secret = registration['client_secret'] # present for confidential_symmetric only
49
+
50
+ # Step 4 — build a properly configured client for authorization flows
51
+ client = Safire::Client.new(
52
+ {
53
+ base_url: 'https://fhir.example.com',
54
+ client_id: client_id,
55
+ redirect_uri: 'https://myapp.example.com/callback',
56
+ scopes: ['openid', 'profile', 'patient/*.read']
57
+ }
58
+ )
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Client Metadata
64
+
65
+ The `metadata` argument is a Hash of RFC 7591 §2 client metadata fields. Keys may be symbols or strings. Any field the server does not recognize is silently ignored; any field the server requires but you omit will produce a `RegistrationError`.
66
+
67
+ ### Common fields
68
+
69
+ | Field | Type | When required | Notes |
70
+ |-------|------|--------------|-------|
71
+ | `client_name` | String | Recommended | Human-readable application name displayed in authorization prompts |
72
+ | `redirect_uris` | Array\<String\> | Required for `authorization_code` | Exact URIs the server will redirect to after authorization |
73
+ | `grant_types` | Array\<String\> | Recommended | See [Grant types](#grant-types) below |
74
+ | `token_endpoint_auth_method` | String | Recommended | See [Authentication methods](#authentication-methods) below |
75
+ | `scope` | String | Optional | Space-separated default scopes; may be narrowed per-request at runtime |
76
+ | `jwks_uri` | String | Required for `private_key_jwt` | URL of the client's JWKS endpoint; the server fetches public keys for JWT verification |
77
+ | `jwks` | Hash | Alternative to `jwks_uri` | Inline JWKS containing the client's public key(s) |
78
+ | `client_uri` | String | Optional | URL of the client's home page |
79
+
80
+ ### Grant types
81
+
82
+ Pass an array containing each grant type the client intends to use.
83
+
84
+ | Value | Use |
85
+ |-------|-----|
86
+ | `authorization_code` | SMART App Launch — user-facing authorization with redirect and PKCE |
87
+ | `client_credentials` | Backend Services — system-to-system access with no user interaction |
88
+ | `refresh_token` | Token refresh; include alongside `authorization_code` when the server should issue refresh tokens |
89
+
90
+ ### Authentication methods
91
+
92
+ The `token_endpoint_auth_method` value tells the server how the client will authenticate at the token endpoint. Safire supports the following methods.
93
+
94
+ | Value | Safire `client_type` | How it works |
95
+ |-------|----------------------|-------------|
96
+ | `none` | `:public` | No client authentication; `client_id` sent in the POST body |
97
+ | `client_secret_basic` | `:confidential_symmetric` | HTTP Basic auth using the `client_secret` the server issues |
98
+ | `private_key_jwt` | `:confidential_asymmetric` | Signed JWT assertion; requires `jwks_uri` or inline `jwks` in metadata |
99
+
100
+ {: .note }
101
+ If your client will use `private_key_jwt`, you must include `jwks_uri` or `jwks` in the registration metadata so the server can obtain your public key for JWT verification.
102
+
103
+ ---
104
+
105
+ ## Registration Endpoint
106
+
107
+ ### Discovery (default)
108
+
109
+ When you do not supply `registration_endpoint:`, Safire fetches the server's SMART configuration from `/.well-known/smart-configuration` and reads the `registration_endpoint` field. Discovery is cached after the first call, so repeated calls on the same `Safire::Client` instance do not issue another network request.
110
+
111
+ ```ruby
112
+ # Safire discovers the registration endpoint automatically
113
+ registration = client.register_client({ client_name: 'My App', ... })
114
+ ```
115
+
116
+ If the server does not advertise a `registration_endpoint`, Safire raises `Safire::Errors::DiscoveryError`. The error message explains that you must supply the endpoint explicitly or that the server may not support DCR.
117
+
118
+ To check before calling `register_client`, use `supports_dynamic_registration?` on the server metadata. This is only relevant when you are relying on discovery — if you already hold the endpoint URL, pass it via `registration_endpoint:` and skip this check.
119
+
120
+ ```ruby
121
+ metadata = client.server_metadata
122
+
123
+ unless metadata.supports_dynamic_registration?
124
+ raise 'Server does not advertise a registration_endpoint — pass registration_endpoint: explicitly or use manual registration'
125
+ end
126
+
127
+ registration = client.register_client({ client_name: 'My App', ... })
128
+ ```
129
+
130
+ ### Explicit endpoint
131
+
132
+ Pass `registration_endpoint:` to bypass discovery entirely. This is useful when the server advertises the endpoint out-of-band, when you want to skip the discovery request, or when the server does not expose `/.well-known/smart-configuration`.
133
+
134
+ ```ruby
135
+ registration = client.register_client(
136
+ { client_name: 'My App', ... },
137
+ registration_endpoint: 'https://auth.example.com/register'
138
+ )
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Initial Access Token
144
+
145
+ Some authorization servers protect their registration endpoint with a bearer token to prevent unauthorized registrations (RFC 7591 §3.1). If the server requires one, obtain it out-of-band from the server operator and pass the full `Authorization` header value to `authorization:`.
146
+
147
+ ```ruby
148
+ registration = client.register_client(
149
+ { client_name: 'My App', ... },
150
+ authorization: 'Bearer eyJhbGciOiJSUzI1NiJ9...'
151
+ )
152
+ ```
153
+
154
+ The `authorization:` parameter accepts any valid header value. Safire passes it verbatim as the `Authorization` header, so the token type prefix (`Bearer`, `MAC`, etc.) must be included.
155
+
156
+ ---
157
+
158
+ ## What's Next
159
+
160
+ [Registration Response]({% link smart-on-fhir/dynamic-client-registration/response.md %}) covers what the server returns, how to persist credentials, how to build a runtime client from the response, and error handling.
@@ -0,0 +1,161 @@
1
+ ---
2
+ layout: default
3
+ title: Registration Response
4
+ parent: Dynamic Client Registration
5
+ grand_parent: SMART
6
+ nav_order: 2
7
+ permalink: /smart-on-fhir/dynamic-client-registration/response/
8
+ description: "What register_client returns: client_id, client_secret, echoed fields, building a runtime Safire::Client from the response, and handling DiscoveryError, RegistrationError, and NetworkError."
9
+ ---
10
+
11
+ # Registration Response
12
+ {: .no_toc }
13
+
14
+ <div class="code-example" markdown="1">
15
+ On success, `register_client` returns the server's response as a Hash. On failure, it raises `DiscoveryError`, `RegistrationError`, or `NetworkError`.
16
+ </div>
17
+
18
+ ## Table of contents
19
+ {: .no_toc .text-delta }
20
+
21
+ 1. TOC
22
+ {:toc}
23
+
24
+ ---
25
+
26
+ ## Response Fields
27
+
28
+ ### `client_id`
29
+
30
+ The `client_id` is always present. It is the permanent identifier the server assigned to this client and is required for every subsequent authorization flow.
31
+
32
+ ```ruby
33
+ client_id = registration['client_id'] # => "dyn_abc123"
34
+ ```
35
+
36
+ ### `client_secret`
37
+
38
+ For clients registered with `token_endpoint_auth_method: 'client_secret_basic'`, the server also issues a `client_secret`. Store it durably in a secrets manager or encrypted storage. The server will not return it again in routine operation; if it is lost, you will need to obtain a replacement through the server's administrative process.
39
+
40
+ ```ruby
41
+ client_secret = registration['client_secret'] # => "s3cr3t..." or nil
42
+ ```
43
+
44
+ ### Additional fields
45
+
46
+ The server typically echoes back some or all of the metadata you submitted, along with server-assigned values such as `client_id_issued_at` or `client_secret_expires_at`. Servers that also implement the OAuth 2.0 Dynamic Client Registration Management Protocol (RFC 7592) may include a `registration_client_uri` and `registration_access_token` for subsequent read, update, and delete operations. Safire does not implement RFC 7592 management operations, but you can store those fields if you need to interact with the management endpoint using another tool.
47
+
48
+ ---
49
+
50
+ ## Building Your Runtime Client
51
+
52
+ After persisting credentials, build a new `Safire::Client` configured for the authorization flows you intend to use. The client type determines how authentication works at the token endpoint.
53
+
54
+ ```ruby
55
+ # Public client — authorization_code grant with PKCE
56
+ client = Safire::Client.new(
57
+ {
58
+ base_url: 'https://fhir.example.com',
59
+ client_id: registration['client_id'],
60
+ redirect_uri: 'https://myapp.example.com/callback',
61
+ scopes: ['openid', 'profile', 'patient/*.read']
62
+ }
63
+ )
64
+
65
+ # Confidential symmetric — client_secret_basic
66
+ client = Safire::Client.new(
67
+ {
68
+ base_url: 'https://fhir.example.com',
69
+ client_id: registration['client_id'],
70
+ client_secret: registration['client_secret'],
71
+ redirect_uri: 'https://myapp.example.com/callback',
72
+ scopes: ['openid', 'profile', 'patient/*.read']
73
+ },
74
+ client_type: :confidential_symmetric
75
+ )
76
+
77
+ # Confidential asymmetric — private_key_jwt
78
+ client = Safire::Client.new(
79
+ {
80
+ base_url: 'https://fhir.example.com',
81
+ client_id: registration['client_id'],
82
+ redirect_uri: 'https://myapp.example.com/callback',
83
+ scopes: ['openid', 'profile', 'patient/*.read'],
84
+ private_key: OpenSSL::PKey::RSA.new(File.read('private.pem')),
85
+ kid: 'my-key-id'
86
+ },
87
+ client_type: :confidential_asymmetric
88
+ )
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Error Handling
94
+
95
+ `register_client` raises one of three error classes. All inherit from `Safire::Errors::Error`, so you can rescue any Safire error with a single clause when needed.
96
+
97
+ ### `Safire::Errors::DiscoveryError`
98
+
99
+ Raised when no `registration_endpoint:` was passed and the server either does not publish `/.well-known/smart-configuration`, returns a non-JSON response, or does not include a `registration_endpoint` field in its SMART metadata.
100
+
101
+ ```ruby
102
+ rescue Safire::Errors::DiscoveryError => e
103
+ puts e.message # "Failed to discover SMART configuration from https://...: server does not advertise a 'registration_endpoint'..."
104
+ puts e.endpoint # "https://fhir.example.com/.well-known/smart-configuration"
105
+ puts e.status # HTTP status code, or nil if the server was unreachable
106
+ ```
107
+
108
+ ### `Safire::Errors::RegistrationError`
109
+
110
+ Raised when the server returns an HTTP error response (4xx or 5xx), or when a
111
+ 2xx response does not contain a non-blank string `client_id`.
112
+
113
+ ```ruby
114
+ rescue Safire::Errors::RegistrationError => e
115
+ # HTTP error path — server returned an RFC 7591 error response
116
+ puts e.status # 400
117
+ puts e.error_code # "invalid_redirect_uri"
118
+ puts e.error_description # "Redirect URI must use HTTPS"
119
+
120
+ # Structural failure path — client_id key is missing
121
+ puts e.received_fields # ["error", "error_description"]
122
+ puts e.message # "Registration response missing client_id; received fields: ..."
123
+
124
+ # Structural failure path — client_id is nil, blank, or not a string
125
+ puts e.error_description # "response client_id must be a non-blank string"
126
+ ```
127
+
128
+ For a missing `client_id` key, `received_fields` contains the field names from
129
+ the response. When the key is present but its value is invalid,
130
+ `error_description` describes the validation failure and `received_fields` is
131
+ `nil`.
132
+
133
+ To rescue any OAuth protocol error in one clause, use `Safire::Errors::OAuthError`, the shared base class for `RegistrationError`, `TokenError`, and `AuthError`.
134
+
135
+ ### `Safire::Errors::NetworkError`
136
+
137
+ Raised when the HTTP request fails at the transport level: connection refused, timeout, or SSL handshake error.
138
+
139
+ ```ruby
140
+ rescue Safire::Errors::NetworkError => e
141
+ puts e.message # "HTTP request failed: ..."
142
+ puts e.error_description # underlying transport error message
143
+ ```
144
+
145
+ ### Full rescue example
146
+
147
+ ```ruby
148
+ begin
149
+ registration = client.register_client(
150
+ { client_name: 'My App', ... },
151
+ registration_endpoint: explicit_endpoint,
152
+ authorization: initial_access_token
153
+ )
154
+ rescue Safire::Errors::DiscoveryError => e
155
+ logger.error("Registration endpoint not found: #{e.message}")
156
+ rescue Safire::Errors::RegistrationError => e
157
+ logger.error("Server rejected registration: #{e.message}")
158
+ rescue Safire::Errors::NetworkError => e
159
+ logger.error("Network failure during registration: #{e.message}")
160
+ end
161
+ ```
@@ -4,7 +4,7 @@ title: SMART
4
4
  nav_order: 4
5
5
  has_children: true
6
6
  permalink: /smart-on-fhir/
7
- description: "Step-by-step guides for SMART App Launch OAuth 2.0 flows in Ruby, covering public client (PKCE), confidential symmetric, confidential asymmetric (private_key_jwt), and Backend Services (system-to-system)."
7
+ description: "Step-by-step guides for SMART App Launch OAuth 2.0 flows in Ruby, covering Dynamic Client Registration (RFC 7591), public client (PKCE), confidential symmetric, confidential asymmetric (private_key_jwt), and Backend Services (system-to-system)."
8
8
  ---
9
9
 
10
10
  # SMART App Launch
@@ -16,6 +16,7 @@ This section provides step-by-step guides for implementing SMART authorization f
16
16
  | Workflow | Description |
17
17
  |----------|-------------|
18
18
  | [SMART Discovery]({% link smart-on-fhir/discovery/index.md %}) | Fetching and using SMART configuration metadata |
19
+ | [Dynamic Client Registration]({% link smart-on-fhir/dynamic-client-registration/index.md %}) | Register a client at runtime using RFC 7591 to obtain a `client_id` |
19
20
  | [Public Client]({% link smart-on-fhir/public-client/index.md %}) | Authorization flow for browser-based and mobile applications |
20
21
  | [Confidential Symmetric Client]({% link smart-on-fhir/confidential-symmetric/index.md %}) | Authorization flow for server-side applications with client secrets |
21
22
  | [Confidential Asymmetric Client]({% link smart-on-fhir/confidential-asymmetric/index.md %}) | Authorization flow using private_key_jwt (RSA/EC key pair) |
@@ -2,7 +2,7 @@
2
2
  layout: default
3
3
  title: POST-Based Authorization
4
4
  parent: SMART
5
- nav_order: 5
5
+ nav_order: 6
6
6
  has_toc: true
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  layout: default
3
3
  title: Public Client Workflow
4
4
  parent: SMART
5
- nav_order: 2
5
+ nav_order: 3
6
6
  has_children: true
7
7
  permalink: /smart-on-fhir/public-client/
8
8
  ---
@@ -75,6 +75,26 @@ The response must be a JSON object (`{...}`) with at least `authorization_endpoi
75
75
 
76
76
  ## Authorization Errors
77
77
 
78
+ ### `ConfigurationError`: Missing `client_id`
79
+
80
+ ```
81
+ Safire::Errors::ConfigurationError: Configuration missing: client_id
82
+ ```
83
+
84
+ `client_id` is optional at construction but required by every authorization method. It is validated at call time, not when the client is created. Ensure `client_id` is set in `ClientConfig` before calling any flow method:
85
+
86
+ ```ruby
87
+ config = Safire::ClientConfig.new(
88
+ base_url: 'https://fhir.example.com',
89
+ client_id: ENV.fetch('SMART_CLIENT_ID'),
90
+ # ...
91
+ )
92
+ ```
93
+
94
+ This error is raised by `authorization_url`, `request_access_token`, `refresh_token`, and `request_backend_token`.
95
+
96
+ ---
97
+
78
98
  ### `ConfigurationError`: Missing scopes
79
99
 
80
100
  ```
@@ -17,6 +17,149 @@ nav_order: 3
17
17
 
18
18
  ---
19
19
 
20
+ ## Dynamic Client Registration Errors
21
+
22
+ ### `DiscoveryError`: Server does not advertise a registration endpoint
23
+
24
+ ```
25
+ Safire::Errors::DiscoveryError: Failed to discover SMART configuration from https://...: server does not advertise a 'registration_endpoint'
26
+ ```
27
+
28
+ The server either does not support Dynamic Client Registration or its registration endpoint is not published in `/.well-known/smart-configuration`. Pass the endpoint explicitly or register manually through the server's developer portal:
29
+
30
+ ```ruby
31
+ registration = client.register_client(
32
+ metadata,
33
+ registration_endpoint: 'https://auth.example.com/register'
34
+ )
35
+ ```
36
+
37
+ For UDAP, the registration endpoint is always discovery-bound. Safire raises
38
+ `DiscoveryError` before POSTing when discovery fails, `signed_metadata` cannot
39
+ be validated, `UdapMetadata#valid?` reports structural non-conformance, the
40
+ server does not advertise `udap_dcr`, or the server does not publish
41
+ mandatory `RS256` registration signing support:
42
+
43
+ ```ruby
44
+ registration = udap_client.register_client(
45
+ metadata,
46
+ client_uri: 'https://client.example.com',
47
+ trusted_anchors: [ca_cert],
48
+ crls: [ca_crl]
49
+ )
50
+ ```
51
+
52
+ ### `ValidationError`: UDAP registration metadata or certifications are invalid
53
+
54
+ ```
55
+ Safire::Errors::ValidationError: Validation failed for certifications: must be nil or an array of compact JWS strings
56
+ ```
57
+
58
+ UDAP registration signs caller-controlled metadata before sending it. Safire
59
+ therefore validates the metadata and certification collection locally and raises
60
+ `ValidationError` before building a software statement when input is malformed.
61
+
62
+ Common causes:
63
+
64
+ - missing required metadata such as `client_name`, `contacts`, `grant_types`, or `scope`
65
+ - unsupported grant combinations
66
+ - non-HTTPS redirect or logo URIs, except HTTP localhost when explicitly enabled for development
67
+ - reserved claims such as `iss`, `sub`, `aud`, `exp`, `jti`, `software_statement`, `certifications`, or `udap`
68
+ - `certifications:` is not `nil` or an array of compact JWS strings
69
+ - discovery declares required certification policy URIs, but `certifications:`
70
+ is omitted or empty; the server still decides whether supplied JWTs satisfy
71
+ those policies
72
+
73
+ ### `CertificateError`: UDAP client signing identity cannot support registration
74
+
75
+ ```
76
+ Safire::Errors::CertificateError: Certificate error — client_uri does not match any URI SAN in the leaf certificate
77
+ ```
78
+
79
+ UDAP registration requires a private key and leaf-first certificate chain. The
80
+ private key must match the leaf certificate, every certificate must be currently
81
+ valid, and the `client_uri:` must exactly match a URI Subject Alternative Name
82
+ in the leaf certificate. Case, port, and trailing slash differences are
83
+ significant.
84
+
85
+ ### `RegistrationError`: Server rejected the registration request
86
+
87
+ ```
88
+ Safire::Errors::RegistrationError: Client registration failed — HTTP 400 — invalid_redirect_uri — Redirect URI must use HTTPS
89
+ ```
90
+
91
+ The server returned an OAuth 2.0 error response. Check `e.error_code` and `e.error_description` for the specific reason, correct the metadata, and retry:
92
+
93
+ ```ruby
94
+ rescue Safire::Errors::RegistrationError => e
95
+ puts e.status # 400
96
+ puts e.error_code # "invalid_redirect_uri"
97
+ puts e.error_description # "Redirect URI must use HTTPS"
98
+ ```
99
+
100
+ UDAP servers may return UDAP-specific error codes such as
101
+ `invalid_software_statement` or `unapproved_software_statement`; Safire
102
+ preserves them in `e.error_code`.
103
+
104
+ ### `RegistrationError`: 2xx response has a missing or invalid `client_id`
105
+
106
+ ```
107
+ Safire::Errors::RegistrationError: Registration response missing client_id; received fields: error, error_description
108
+ ```
109
+
110
+ The server returned a successful HTTP status without a valid `client_id`. A
111
+ successful registration response must contain a non-blank string `client_id`.
112
+
113
+ If the response omitted the key, check `e.received_fields` to see what the
114
+ server returned:
115
+
116
+ ```ruby
117
+ rescue Safire::Errors::RegistrationError => e
118
+ puts e.received_fields # ["error", "error_description"]
119
+ end
120
+ ```
121
+
122
+ If the key was present but its value was `nil`, blank, or not a string,
123
+ `e.error_description` reports the validation failure and `e.received_fields`
124
+ is `nil`:
125
+
126
+ ```ruby
127
+ rescue Safire::Errors::RegistrationError => e
128
+ puts e.error_description # "response client_id must be a non-blank string"
129
+ end
130
+ ```
131
+
132
+ Either response is a server-side protocol error and should not be treated as a
133
+ successful registration.
134
+
135
+ ### `RegistrationError`: UDAP cancellation response does not confirm cancellation
136
+
137
+ ```
138
+ Safire::Errors::RegistrationError: Client registration failed — HTTP 202 — cancellation response must include an empty grant_types array
139
+ ```
140
+
141
+ UDAP registration cancellation is confirmed by the response body, not by a
142
+ specific success status such as `200` or `201`. Safire accepts a cancellation
143
+ response only when the final status is `2xx` and the body contains a non-blank
144
+ string `client_id` plus an empty `grant_types` array:
145
+
146
+ ```ruby
147
+ cancellation = udap_client.cancel_registration(
148
+ metadata,
149
+ client_uri: 'https://client.example.com',
150
+ trusted_anchors: [ca_cert],
151
+ crls: [ca_crl]
152
+ )
153
+
154
+ cancellation['grant_types'] # => []
155
+ ```
156
+
157
+ If the response status is not `2xx`, or if the body omits `grant_types`, returns
158
+ a non-array value, or returns a non-empty array, treat it as a failed
159
+ cancellation and investigate the authorization server response.
160
+
161
+ ---
162
+
20
163
  ## Confidential Symmetric Client Errors
21
164
 
22
165
  ### `ConfigurationError`: Missing `client_secret`