safire 0.4.0 → 0.4.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/Gemfile.lock +5 -5
  4. data/README.md +14 -1
  5. data/ROADMAP.md +26 -5
  6. data/docs/adr/ADR-006-lazy-discovery.md +11 -0
  7. data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +4 -4
  8. data/docs/adr/ADR-008-warn-return-false-for-compliance-validation.md +17 -4
  9. data/docs/adr/ADR-009-oauth-error-hierarchy.md +13 -4
  10. data/docs/adr/ADR-011-udap-stu2-discovery-conformance.md +9 -7
  11. data/docs/adr/ADR-013-udap-registration-request-model.md +22 -1
  12. data/docs/adr/ADR-014-udap-software-statement-signing.md +8 -4
  13. data/docs/adr/ADR-015-client-role-and-runtime-readiness.md +207 -0
  14. data/docs/adr/index.md +1 -0
  15. data/docs/configuration/client-setup.md +2 -0
  16. data/docs/configuration/index.md +1 -1
  17. data/docs/smart-on-fhir/backend-services/index.md +19 -4
  18. data/docs/smart-on-fhir/backend-services/token-request.md +20 -2
  19. data/docs/smart-on-fhir/discovery/capability-checks.md +8 -1
  20. data/docs/smart-on-fhir/discovery/index.md +18 -2
  21. data/docs/smart-on-fhir/discovery/metadata.md +16 -2
  22. data/docs/troubleshooting/auth-errors.md +19 -3
  23. data/docs/troubleshooting/client-errors.md +58 -9
  24. data/docs/troubleshooting/index.md +17 -2
  25. data/docs/udap/dynamic-client-registration/index.md +20 -11
  26. data/docs/udap/dynamic-client-registration/lifecycle.md +26 -7
  27. data/docs/udap/dynamic-client-registration/registration-metadata.md +14 -3
  28. data/docs/udap/dynamic-client-registration/software-statement.md +7 -4
  29. data/docs/udap.md +14 -4
  30. data/lib/safire/client.rb +3 -2
  31. data/lib/safire/client_config.rb +9 -3
  32. data/lib/safire/protocols/json_response_parsing.rb +104 -0
  33. data/lib/safire/protocols/oauth_response_handling.rb +2 -5
  34. data/lib/safire/protocols/smart.rb +80 -17
  35. data/lib/safire/protocols/smart_metadata.rb +61 -15
  36. data/lib/safire/protocols/udap.rb +135 -55
  37. data/lib/safire/protocols/udap_metadata.rb +9 -3
  38. data/lib/safire/protocols/udap_registration_metadata.rb +31 -15
  39. data/lib/safire/protocols/udap_scope_coverage.rb +122 -0
  40. data/lib/safire/protocols/udap_signed_metadata_validator.rb +2 -2
  41. data/lib/safire/uri_validation.rb +9 -5
  42. data/lib/safire/version.rb +1 -1
  43. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc7c5bf60b5d43365009f5073aae66d535f8e5345825f8ca08b638fb26587417
4
- data.tar.gz: f47a4a2c4a84eefa8427e05a59d9e5e2cfca4adbce2a989f43cd53ead47e42bf
3
+ metadata.gz: d9019d0d248f2e4e723a7be2051feea39d3be739d6b7118b31c1d67be16aad1a
4
+ data.tar.gz: ed3c092bc91718d766e779b361e10275014b0d750d3f59f22fa60504b9cee5a9
5
5
  SHA512:
6
- metadata.gz: 625c96ea6424f41590d5396fb9d455ec0d89477474f484cbda236ec55ef13aed76d756ce88e014f76c1556ee8fa8e6e9d92a5a37743af2279e30c59456d74a58
7
- data.tar.gz: 8c572a8974dfabdad87fd9d672013ca9bf64276a604db1492d11d6ad19e0351b30ecc2485b9262a366e3fce21af8eff656e2d0060077295c1b1127d6315c5ae2
6
+ metadata.gz: cd779a03d653c1108aee4085d90a14f806b1cf0e337892a3a9ca08bbe41dd2f266211e5173b265b5ca597b82b60cf4a0d4509705181369deb9b7affb79eb8888
7
+ data.tar.gz: 69939d1190e064f56a38481d069f27d313fdfacb2efd096316add8c2db4e5baa6c7b572420f949fe952edfa844f2bb35d63ecbdf18833208fb5f95896c2d7b72
data/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2026-08-29
11
+
12
+ ### Changed
13
+
14
+ - SMART and UDAP discovery now accept usable raw JSON-object response bodies
15
+ when incorrect or missing response content types leave them undecoded by the
16
+ HTTP adapter. Shared discovery, registration, and OAuth error handling treats
17
+ malformed or non-object JSON plus ambiguous, recursive, or non-JSON-compatible
18
+ adapter-provided Hashes as unusable instead of risking silent normalization
19
+ or unexpected type errors.
20
+ - SMART metadata diagnostics now validate array-valued field shapes. Capability
21
+ and signing-algorithm helpers treat malformed list advertisements as
22
+ unsupported instead of applying scalar substring semantics or raising an
23
+ unexpected type error. Authorization and token operations now also apply the
24
+ configured HTTPS/localhost policy to endpoints obtained through SMART
25
+ discovery before using them. OAuth authorization, token, registration, and
26
+ redirect endpoint URIs are rejected when they contain fragment components.
27
+ - UDAP Dynamic Client Registration now validates and snapshots caller input
28
+ before discovery, then gates only on trusted DCR profile, endpoint, algorithm,
29
+ certification, and scope values needed by the request. Missing RS256
30
+ advertisement and unconfirmed scope support produce value-free warnings when
31
+ an otherwise usable request can proceed; unrelated metadata conformance
32
+ defects remain available through `UdapMetadata#valid?`. In v0.4.1,
33
+ unadvertised wildcards still proceed after warning; registration and
34
+ modification require exact advertisement in v0.5.0, while wildcard scope
35
+ compatibility remains warning-only during cancellation.
36
+ - UDAP authorization-code registration now rejects only locally provable
37
+ `logo_uri` defects. Usable HTTPS URLs whose PNG, JPG/JPEG, or GIF format
38
+ cannot be inferred from the path are accepted with a value-free warning;
39
+ Safire does not dereference caller-supplied logo URLs during validation.
40
+ - UDAP registration returns metadata only for completed `200`/`201` outcomes;
41
+ `202 Accepted` is reported as pending rather than treated as completed.
42
+ Cancellation error messages now more clearly distinguish an unconfirmed
43
+ outcome from server rejection. Safire never retries registration lifecycle
44
+ requests automatically when the authorization server may already have
45
+ committed them.
46
+
47
+ ### Deprecated
48
+
49
+ - SMART Backend Services requests without usable configured or per-call scopes
50
+ still use `system/*.rs` for v0.4.x compatibility, but now emit a deprecation warning.
51
+ Configure or pass scopes explicitly before v0.6.0, when missing scopes will
52
+ raise `ConfigurationError`.
53
+
10
54
  ## [0.4.0] - 2026-08-07
11
55
 
12
56
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- safire (0.4.0)
4
+ safire (0.4.1)
5
5
  activesupport (>= 7.1, < 9)
6
6
  addressable (~> 2.8)
7
7
  faraday (~> 2.14)
@@ -127,8 +127,8 @@ GEM
127
127
  diff-lcs (>= 1.2.0, < 2.0)
128
128
  rspec-support (~> 3.13.0)
129
129
  rspec-support (3.13.6)
130
- rubocop (1.89.0)
131
- json (~> 2.3)
130
+ rubocop (1.90.0)
131
+ json (>= 2.3)
132
132
  language_server-protocol (~> 3.17.0.2)
133
133
  lint_roller (~> 1.1.0)
134
134
  parallel (>= 1.10)
@@ -146,7 +146,7 @@ GEM
146
146
  rubocop (~> 1.81)
147
147
  ruby-progressbar (1.13.0)
148
148
  securerandom (0.4.1)
149
- simplecov (1.0.3)
149
+ simplecov (1.1.1)
150
150
  simplecov-cobertura (4.0.0)
151
151
  rexml
152
152
  simplecov (~> 1.0)
@@ -167,7 +167,7 @@ GEM
167
167
  unicode-emoji (~> 4.1)
168
168
  unicode-emoji (4.2.0)
169
169
  uri (1.1.1)
170
- webmock (3.26.2)
170
+ webmock (3.26.4)
171
171
  addressable (>= 2.8.0)
172
172
  crack (>= 0.3.2)
173
173
  hashdiff (>= 0.4.0, < 2.0.0)
data/README.md CHANGED
@@ -19,7 +19,7 @@ Safire is a Ruby gem for healthcare client applications that implements [SMART A
19
19
  - Confidential Symmetric Client (`client_secret` + HTTP Basic Auth)
20
20
  - Confidential Asymmetric Client (`private_key_jwt` with RS384/ES384)
21
21
  - POST-Based Authorization
22
- - Backend Services (`client_credentials` grant, JWT assertion, no user interaction or PKCE; scope defaults to `system/*.rs`)
22
+ - Backend Services (`client_credentials` grant, JWT assertion, no user interaction or PKCE; client-selected scopes)
23
23
 
24
24
  ### UDAP Security (STU2)
25
25
 
@@ -77,6 +77,13 @@ cancellation = udap_client.cancel_registration(
77
77
  )
78
78
  ```
79
79
 
80
+ UDAP registration uses the trusted fields needed for that workflow rather than
81
+ turning the full `UdapMetadata#valid?` conformance diagnostic into an automatic
82
+ gate. In v0.4.1, an unadvertised requested wildcard produces a warning and is
83
+ still submitted; v0.5.0 will require exact wildcard advertisement for
84
+ registration and modification while keeping wildcard scope drift warning-only
85
+ during cancellation.
86
+
80
87
  UDAP JWT client authentication and Tiered OAuth are planned. See [ROADMAP.md](https://github.com/vanessuniq/safire/blob/main/ROADMAP.md) for details.
81
88
 
82
89
  ---
@@ -185,6 +192,12 @@ token_data = client.request_backend_token(
185
192
  client.token_response_valid?(token_data, flow: :backend_services)
186
193
  ```
187
194
 
195
+ Configure scopes on the client or pass `scopes:` to each Backend Services
196
+ request. The legacy `system/*.rs` fallback is deprecated in v0.4.x and will be
197
+ removed in v0.6.0. Backend Services normally uses `system/` scopes. Safire
198
+ preserves explicit non-system scopes because SMART permits them when context is
199
+ established out of band.
200
+
188
201
  ---
189
202
 
190
203
  ## Configuration
data/ROADMAP.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Safire Roadmap
2
2
 
3
- ## Latest Published Release — v0.4.0
3
+ ## Latest Published Release — v0.4.1
4
4
 
5
5
  Safire is in early development (pre-release). The API is functional but not yet stable — breaking changes may occur before v1.0.0. Published to [RubyGems](https://rubygems.org/gems/safire).
6
6
 
@@ -22,7 +22,7 @@ Feedback, bug reports, and pull requests are welcome via the [issue tracker](htt
22
22
  - **POST-Based Authorization** — form-encoded authorization requests
23
23
  - **JWT Assertion Builder** — signed JWT assertions with configurable `kid` and expiry
24
24
  - **PKCE** — automatic code verifier and challenge generation
25
- - **Backend Services** — `client_credentials` grant for system-to-system flows; JWT assertion (RS384/ES384); no user interaction, redirect, or PKCE required; scope defaults to `system/*.rs` when not configured
25
+ - **Backend Services** — `client_credentials` grant for system-to-system flows; JWT assertion (RS384/ES384); no user interaction, redirect, or PKCE required; client-selected scopes with a deprecated v0.4.x compatibility fallback
26
26
  - **Dynamic Client Registration** — runtime client registration per [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591); endpoint discovered from SMART metadata or supplied explicitly; supports initial access token
27
27
 
28
28
  ### UDAP Security (STU2 / v2.0.0)
@@ -53,9 +53,30 @@ Feedback, bug reports, and pull requests are welcome via the [issue tracker](htt
53
53
 
54
54
  ## Planned Features
55
55
 
56
- ### UDAP Security
56
+ ### v0.5.0 — UDAP JWT Client Authentication and B2B Authorization
57
+
58
+ - **UDAP Registration Wildcard Enforcement** — require exact
59
+ `scopes_supported` advertisement for requested wildcard scopes during new
60
+ registration and modification after the v0.4.1 warning period; cancellation
61
+ remains warning-only so metadata drift cannot strand an existing registration
62
+ - **UDAP Authentication Tokens** — certificate-backed JWT client authentication
63
+ for token endpoint requests, using authoritative signed discovery metadata
64
+ - **B2B Client Credentials** — headless system-to-system authorization with the
65
+ STU2 `hl7-b2b` authorization extension and discovery-constrained scopes,
66
+ signing algorithms, and extension support
67
+
68
+ ### v0.6.0 — UDAP Authorization Code Flows
69
+
70
+ - **Explicit SMART Backend Scopes** — remove the deprecated `system/*.rs`
71
+ fallback; Backend Services requests without configured or per-call scopes
72
+ raise `ConfigurationError`
73
+ - **Consumer-Facing Authorization** — UDAP authorization code flow with state,
74
+ PKCE, Authentication Tokens, code exchange, and refresh support
75
+ - **Interactive B2B Authorization** — authorization code flow for B2B clients,
76
+ including local-user authorization and UDAP client authentication
77
+
78
+ ### v0.7.0 — UDAP Tiered OAuth
57
79
 
58
- - **UDAP JWT Client Auth** — B2B and consumer-facing authorization flows
59
80
  - **Tiered OAuth** — identity chaining for multi-system access
60
81
 
61
82
  ### Quality and Compliance
@@ -99,5 +120,5 @@ Feedback, bug reports, and pull requests are welcome via the [issue tracker](htt
99
120
  | ActiveSupport | ≥ 7.1, < 9 |
100
121
  | Rails (optional) | ≥ 7.1 |
101
122
  | SMART App Launch | 2.2.0 (STU2) |
102
- | UDAP Security | 2.0.0 (STU2 discovery and Dynamic Client Registration lifecycle implemented; auth flows planned) |
123
+ | UDAP Security | 2.0.0 (STU2 discovery and Dynamic Client Registration lifecycle implemented; JWT client authentication and authorization flows planned) |
103
124
  | FHIR | R4, R4B |
@@ -125,6 +125,15 @@ end
125
125
 
126
126
  A 204 response means the server has no UDAP workflows for that community. `Protocols::Udap` raises `DiscoveryError` before the body is parsed, with a descriptive message that identifies the community when one was requested.
127
127
 
128
+ SMART and UDAP discovery both require a JSON object but do not depend on the
129
+ HTTP adapter having already decoded it. A shared protocol-neutral parser accepts
130
+ an already parsed Hash or a raw JSON-object string, then deeply normalizes keys
131
+ without mutating adapter-owned data. This allows Safire to interoperate when a
132
+ server sends valid JSON with an incorrect or missing content type; the response
133
+ is still non-conformant at the HTTP layer. Malformed JSON, non-object JSON, and
134
+ ambiguous or non-JSON-compatible adapter Hashes raise the protocol's existing
135
+ `DiscoveryError`.
136
+
128
137
  ---
129
138
 
130
139
  ## Consequences
@@ -136,6 +145,8 @@ A 204 response means the server has no UDAP workflows for that community. `Proto
136
145
  - Callers control when discovery happens — supports application-level caching patterns (see [Advanced Examples]({{ site.baseurl }}/advanced/#metadata-caching))
137
146
  - `client_type=` mutation preserves cached SMART metadata — no re-discovery
138
147
  - UDAP community-and-trust-policy cache allows a single client instance to serve multiple communities without serving stale signed metadata
148
+ - Discovery parsing is independent of response content-type middleware while
149
+ still failing closed on unusable object data
139
150
 
140
151
  **Trade-offs:**
141
152
  - Discovery errors surface at first use (e.g. `authorization_url`), not at construction — callers must handle `Errors::DiscoveryError` in their flow logic rather than at the `new` call site
@@ -30,7 +30,7 @@ security boundary and can hide production misconfiguration.
30
30
  For client configuration and HTTP redirects, HTTPS is enforced at **two layers**.
31
31
  Both layers use the same explicit local-development opt-in:
32
32
 
33
- **Layer 1 — `ClientConfig` URI validation:** all URI attributes (`base_url`, `redirect_uri`, `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`) must use `https://`, except when `allow_insecure_localhost: true` is configured and the host is `localhost` or `127.0.0.1`.
33
+ **Layer 1 — `ClientConfig` URI validation:** all URI attributes (`base_url`, `redirect_uri`, `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`) must use `https://`, except when `allow_insecure_localhost: true` is configured and the host is `localhost` or `127.0.0.1`. OAuth redirect, authorization, and token endpoint URIs must not contain a fragment component, as required by RFC 6749 Sections 3.1 and 3.2.
34
34
 
35
35
  **Layer 2 — `HttpsOnlyRedirects` Faraday middleware:** intercepts every 3xx response before `faraday-follow_redirects` follows it. If the redirect target is not HTTPS, and the local-development exception is not enabled for a loopback host, a `Safire::Errors::NetworkError` is raised immediately rather than following the redirect.
36
36
 
@@ -57,9 +57,9 @@ runtime concern.
57
57
  UDAP registration metadata applies the same secure default to its registration
58
58
  URI fields. The UDAP Security STU2 registration profile requires
59
59
  `redirect_uris` and `logo_uri` to use HTTPS.
60
- `URIValidation#strict_https_uri?` provides that default predicate, while
61
- `URIValidation#localhost_http_uri?` identifies the only local HTTP shape that
62
- can be accepted when a caller explicitly opts into development mode.
60
+ `URIValidation#classify_uri` provides the shared HTTPS and fragment policy.
61
+ UDAP redirect URIs reject fragment components; the rule is not applied to the
62
+ presentation-oriented `logo_uri` field.
63
63
 
64
64
  `UdapRegistrationMetadata` uses the same explicit option name for non-TLS local
65
65
  redirect and logo URIs. It accepts only HTTP on `localhost` or `127.0.0.1`,
@@ -15,7 +15,10 @@ nav_order: 8
15
15
 
16
16
  Safire performs two different kinds of checks:
17
17
 
18
- 1. **Configuration checks** — validating that the caller has provided a usable configuration (required attributes present, URIs well-formed and HTTPS). These run at construction time and represent programming errors if they fail.
18
+ 1. **Configuration checks** — validating that the caller has provided usable
19
+ configuration (required attributes present, URIs well-formed and HTTPS).
20
+ General checks run at construction time; flow-specific requirements run at
21
+ the operation boundary. They represent caller-correctable errors if they fail.
19
22
 
20
23
  2. **Compliance checks** — validating that a remote server's response conforms to the SMART App Launch 2.2.0 specification. These run at runtime and represent server behaviour, not caller behaviour.
21
24
 
@@ -31,6 +34,12 @@ Option B lets the caller decide what to do: they can check the return value, obs
31
34
 
32
35
  There is also a clear boundary: **the caller controls the config** (configuration errors should raise — the caller can fix them); **the server controls the response** (compliance violations should warn — the caller cannot fix a remote server).
33
36
 
37
+ This caller/server distinction is a useful default, but it is not sufficient
38
+ for security-sensitive runtime decisions. ADR-015 refines the boundary: remote
39
+ data raises when its defect makes the requested client operation unsafe,
40
+ impossible, or unconfirmed. Complete structural conformance remains an explicit
41
+ warn-and-return-false diagnostic when the operation does not depend on it.
42
+
34
43
  ---
35
44
 
36
45
  ## Decision
@@ -52,12 +61,16 @@ end
52
61
  ```
53
62
 
54
63
  These methods:
64
+
55
65
  - Never raise an exception
56
66
  - Log one warning per violation (not a single combined message) so each issue is individually observable
57
- - Return `true` only when fully compliant; `false` as soon as any violation is found
67
+ - Return `true` only when fully compliant and `false` when any violation is found
58
68
  - Are **user-callable** — they are not invoked automatically by `authorization_url` or `server_metadata`; callers opt in to compliance checking
59
69
 
60
- Configuration validation (`ClientConfig#validate!`, `Smart#validate!`) raises `ConfigurationError` — these are programming errors that must be fixed before the gem can function.
70
+ Configuration validation in `ClientConfig` and protocol workflow-readiness
71
+ checks raise typed Safire errors. These are caller-correctable failures or
72
+ conditions that make the requested operation unsafe, impossible, or
73
+ unconfirmed; ADR-015 owns the detailed boundary.
61
74
 
62
75
  ---
63
76
 
@@ -71,5 +84,5 @@ Configuration validation (`ClientConfig#validate!`, `Smart#validate!`) raises `C
71
84
 
72
85
  **Trade-offs:**
73
86
  - Callers who do not call `token_response_valid?` get no compliance signal at all — non-compliant responses are silently accepted; this is intentional (opt-in, not opt-out)
74
- - The distinction between "warn + return false" and "raise" must be maintained consistently new validation methods should follow the same rule: server behaviour warn; caller configuration raise
87
+ - The distinction between "warn + return false" and "raise" must be maintained consistently. New validation methods follow ADR-015: explicit conformance diagnostics warn, while client obligations and security or operation-readiness failures raise.
75
88
  - `token_response_valid?` accepts a `flow:` keyword argument (`:app_launch` default, `:backend_services`) that adjusts which fields are required and what the warning messages say. For example, `token_type` must be `"Bearer"` (App Launch spec) or `"bearer"` (Backend Services spec), and `expires_in` is RECOMMENDED for App Launch but REQUIRED for Backend Services. Callers opt in to the stricter backend-services validation by passing `flow: :backend_services`
@@ -98,7 +98,9 @@ The module accepts already-received response bodies. It does not send HTTP
98
98
  requests, select endpoints, log response values, or apply protocol-specific
99
99
  policy.
100
100
 
101
- Registration success responses are normalized to string-keyed hashes and must
101
+ Registration success responses delegate JSON-object parsing to the private,
102
+ protocol-neutral `JSONResponseParsing` collaborator. Raw JSON strings and
103
+ already parsed Hashes are normalized to new, deeply string-keyed hashes and must
102
104
  contain a non-blank string `client_id`, as required by RFC 7591. Missing
103
105
  `client_id` responses continue to report only the received field names. A
104
106
  present but blank or non-string `client_id` raises `RegistrationError` with a
@@ -106,9 +108,14 @@ structural error description. This intentionally hardens SMART registration:
106
108
  valid RFC 7591 responses are unchanged, while malformed identifiers that were
107
109
  previously accepted through `present?` now fail closed.
108
110
 
109
- Malformed or non-object OAuth error bodies produce an error containing the HTTP
110
- status only. Parsed JSON objects preserve their protocol error codes, including
111
- UDAP-specific values such as `invalid_software_statement`.
111
+ Malformed, non-object, ambiguous, or non-JSON-compatible OAuth error bodies
112
+ produce an error containing the HTTP status only. Parsed JSON objects preserve
113
+ their protocol error codes, including UDAP-specific values such as
114
+ `invalid_software_statement`. Symbol/string key collisions and the other Ruby
115
+ structure guards apply only to adapter-supplied or directly passed Hashes;
116
+ `JSON.parse` and Faraday's JSON middleware produce string-keyed Hashes. The
117
+ guards prevent local normalization from silently overwriting ambiguous values,
118
+ not collisions in JSON received on the wire.
112
119
 
113
120
  ### Amendment consequences
114
121
 
@@ -118,6 +125,8 @@ UDAP-specific values such as `invalid_software_statement`.
118
125
  construction or endpoint policy
119
126
  - response key normalization makes direct and middleware-parsed hashes behave
120
127
  consistently
128
+ - strict adapter-Hash validation makes response translation total for cyclic or
129
+ non-JSON-compatible Ruby values
121
130
  - malformed successful registrations fail before an invalid identifier reaches
122
131
  application state
123
132
 
@@ -66,10 +66,11 @@ semantics.
66
66
  algorithm (`alg`), required claim presence, and signature are not validated here — these are
67
67
  deferred to the cryptographic validator
68
68
  - endpoint URL fields (`token_endpoint`, `registration_endpoint`, conditionally
69
- `authorization_endpoint`) must be absolute HTTPS URLs; plain HTTP is accepted only when
70
- `allow_insecure_localhost: true` is configured and the host is `localhost` or `127.0.0.1`
71
- to support development without TLS — any other scheme on those hosts (e.g. `ftp://localhost`)
72
- is rejected; this exception does not apply in production
69
+ `authorization_endpoint`) must be absolute HTTPS URLs without fragment components; plain HTTP
70
+ is accepted only when `allow_insecure_localhost: true` is configured and the host is `localhost`
71
+ or `127.0.0.1` to support development without TLS — any other scheme on those hosts (e.g.
72
+ `ftp://localhost`) is rejected; this exception does not apply in production and does not relax
73
+ the fragment prohibition
73
74
  - `authorization_endpoint` is conditionally required when `grant_types_supported` includes
74
75
  `"authorization_code"`
75
76
  - `"udap_authz"` is conditionally required in `udap_profiles_supported` when `grant_types_supported`
@@ -108,6 +109,7 @@ semantics.
108
109
 
109
110
  **Trade-offs:**
110
111
 
111
- - A structurally valid `UdapMetadata` object is not automatically cryptographically validated;
112
- callers that require full STU2 conformance must also perform cryptographic validation of the
113
- `signed_metadata` JWT after structural validation passes
112
+ - A directly constructed `UdapMetadata` object is not automatically
113
+ cryptographically validated. `Safire::Client#server_metadata` validates
114
+ `signed_metadata` before returning the entity; callers that construct an
115
+ entity themselves must establish that trust separately.
@@ -41,6 +41,11 @@ construction. Invalid input raises `Safire::Errors::ValidationError` with the
41
41
  failing attribute and a value-free reason. The class does not inherit from
42
42
  `Entity` and does not use the discovery layer's warn-and-return-false contract.
43
43
 
44
+ Registration orchestration constructs this value object once, before discovery
45
+ or other network activity. Scope diagnostics and software-statement signing
46
+ consume the same immutable instance; Safire does not independently normalize
47
+ the caller's raw Hash at multiple stages.
48
+
44
49
  Top-level string and symbol keys are normalized to strings. Supplying both forms
45
50
  of one key is rejected rather than allowing insertion order to decide which
46
51
  value is signed. The value object and its canonical internal hash are frozen,
@@ -55,7 +60,20 @@ Registration accepts exactly one primary grant:
55
60
 
56
61
  Unknown and duplicate grant values are rejected. `redirect_uris`, `logo_uri`,
57
62
  and generated `response_types: ["code"]` apply only to authorization-code
58
- registration. Redirect and logo URIs require absolute HTTPS by default.
63
+ registration. Redirect and logo URIs require absolute HTTPS by default, and
64
+ redirect URIs must not contain fragment components.
65
+
66
+ The STU2 logo requirement has two different validation boundaries. Safire can
67
+ prove locally that `logo_uri` is an absolute HTTPS URI, so malformed, relative,
68
+ and insecure public values fail before signing. Safire cannot prove that the
69
+ remote representation is PNG, JPG/JPEG, or GIF from a URI path alone. Familiar
70
+ image suffixes remain a quiet heuristic; other usable URI paths produce a
71
+ value-free warning and remain the caller's responsibility.
72
+
73
+ Safire does not dereference `logo_uri` during validation. Fetching an untrusted
74
+ caller-supplied URI would introduce SSRF, latency, redirect, and remote
75
+ availability concerns into construction of an otherwise local value object,
76
+ without reliably proving what the authorization server will later retrieve.
59
77
 
60
78
  For local development without TLS, callers may explicitly pass
61
79
  `allow_insecure_localhost: true`. This permits HTTP only on `localhost` and
@@ -94,10 +112,13 @@ regular expression.
94
112
  ## Consequences
95
113
 
96
114
  - Signing code can consume one canonical, already validated metadata hash.
115
+ - Invalid caller metadata fails before discovery, signing, or POSTing.
97
116
  - Caller mutations cannot change metadata between validation and signing.
98
117
  - Protocol-owned claims cannot be shadowed through caller input.
99
118
  - Plain HTTP remains impossible for remote hosts; local HTTP requires a literal
100
119
  boolean opt-in and produces a warning.
120
+ - Logo media type remains a caller obligation when it cannot be established
121
+ from the URI path; Safire reports that uncertainty without fetching the URI.
101
122
  - Extension metadata remains forward-compatible without permitting arbitrary
102
123
  Ruby objects into a JWT payload.
103
124
  - Metadata validation remains independent of software-statement signing and the
@@ -81,10 +81,14 @@ key, and advertised by the server. Without an explicit algorithm, Safire chooses
81
81
  the first key-compatible advertised algorithm, preferring `RS256` over `RS384`
82
82
  for RSA keys because `RS256` is the STU2 baseline.
83
83
 
84
- Protocol orchestration separately requires every DCR-capable server to advertise
85
- mandatory `RS256` support before Safire signs with any compatible advertised
86
- algorithm. This enforces the server baseline independently from the configured
87
- client key type.
84
+ Protocol orchestration requires the advertised value to be a non-empty array of
85
+ algorithm names. A malformed or empty value makes safe algorithm selection
86
+ impossible and raises `DiscoveryError`. Because STU2 requires every FHIR server
87
+ to support `RS256`, Safire warns when it is absent. The warning diagnoses the
88
+ server baseline without blocking an otherwise interoperable request: the
89
+ builder may still select another advertised, Safire-supported,
90
+ key-compatible algorithm. A valid advertisement with no usable intersection
91
+ raises `ConfigurationError`.
88
92
 
89
93
  ### Validate local signing identity, not server trust
90
94
 
@@ -0,0 +1,207 @@
1
+ ---
2
+ layout: default
3
+ title: "ADR-015: Client role and runtime readiness"
4
+ parent: Architecture Decision Records
5
+ nav_order: 15
6
+ ---
7
+
8
+ # ADR-015: Client role and runtime readiness
9
+
10
+ **Status:** Accepted
11
+
12
+ ---
13
+
14
+ ## Context
15
+
16
+ Safire consumes metadata and protocol responses from remote SMART App Launch
17
+ and UDAP servers. Some defects make a client request unsafe or impossible;
18
+ others are server conformance defects that an interoperable client can report
19
+ without blocking an otherwise valid operation.
20
+
21
+ A blanket rule such as "caller input raises, server input warns" is not
22
+ sufficient at a protocol trust boundary. A server controls discovery and token
23
+ responses, but Safire must still reject an untrusted value when it is needed to
24
+ select a secure endpoint, validate a signed assertion, protect credentials, or
25
+ establish that a lifecycle operation completed. Conversely, complete server
26
+ certification is outside a client library's runtime role.
27
+
28
+ Safire therefore needs a consistent way to distinguish operational readiness
29
+ from optional conformance diagnostics. Comprehensive server certification
30
+ belongs in a conformance harness such as Inferno.
31
+
32
+ ## Decision
33
+
34
+ Safire classifies protocol checks by their effect on the requested client
35
+ operation:
36
+
37
+ 1. **Runtime hard failure:** reject a condition that would violate a client
38
+ obligation, make the operation unsafe or impossible, expose credentials, or
39
+ leave a security-sensitive result unconfirmed. This includes malformed wire
40
+ shapes required by the operation and failed UDAP signed-metadata trust
41
+ validation.
42
+ 2. **Warning or negotiation:** report a server defect or uncertain capability
43
+ when Safire can still send a safe, conformant request and the server remains
44
+ authoritative for acceptance.
45
+ 3. **Explicit diagnostic:** methods such as `SmartMetadata#valid?`,
46
+ `UdapMetadata#valid?`, and `Smart#token_response_valid?` warn and return a
47
+ Boolean when called. Discovery does not automatically promote every failed
48
+ diagnostic into a workflow failure.
49
+ 4. **Server-owned decision:** authorization grants, requested scope approval,
50
+ client registration policy, certifications, and issued credentials are
51
+ decided by the authorization server. Safire validates its request and the
52
+ response shape it must consume, but does not predict the policy result.
53
+
54
+ This decision refines ADR-008. Server-controlled data is warning-only when the
55
+ defect does not cross a security or operation-readiness boundary; its remote
56
+ origin alone does not determine the error behavior.
57
+
58
+ Protocol response bodies that must be JSON objects use the private,
59
+ protocol-neutral `JSONResponseParsing` collaborator. It accepts an already
60
+ parsed `Hash` or a raw JSON string, produces a new deeply string-keyed Hash, and
61
+ rejects malformed or non-object JSON. Adapter-supplied Hashes also fail closed
62
+ when they contain recursive structures, unsupported JSON key or value types,
63
+ non-finite numbers, invalid string encodings, excessive nesting, or keys that
64
+ collide after symbol-to-string normalization. The collaborator returns a parse
65
+ failure and leaves each consumer to choose its protocol-specific error class.
66
+
67
+ Parsing a valid raw JSON object allows interoperability when an HTTP adapter did
68
+ not decode the body because the server sent an incorrect or missing content
69
+ type. Safire's tolerance does not make that server response conformant.
70
+
71
+ Metadata capability helpers type-check list-shaped advertisements before using
72
+ membership semantics. A malformed list cannot assert a SMART capability or
73
+ raise an unexpected type error. The complete structural assessment remains the
74
+ caller's explicit `SmartMetadata#valid?` diagnostic rather than an automatic
75
+ discovery failure.
76
+
77
+ SMART discovery preserves metadata for inspection, but an operation validates
78
+ its discovered authorization or token endpoint against Safire's HTTPS policy
79
+ and rejects fragment components before use. This focused runtime stop protects
80
+ authorization data and client credentials while enforcing the OAuth endpoint
81
+ shape the operation consumes, without promoting unrelated metadata defects into
82
+ discovery failures.
83
+
84
+ UDAP DCR uses focused readiness rather than calling `UdapMetadata#valid?` as an
85
+ operational gate. Safire requires trusted discovery, an advertised `udap_dcr`
86
+ profile, a usable authoritative registration endpoint, usable registration
87
+ algorithm metadata, and type-safe certification requirements. Other structural
88
+ conformance checks remain available through the explicit `valid?` diagnostic.
89
+
90
+ In v0.4.1, requested UDAP registration scopes are diagnostic only. Requested
91
+ tokens containing a literal `*` are classified first and compared by exact
92
+ membership in `scopes_supported`; narrower recognized SMART FHIR scopes may be
93
+ covered by broader advertised scopes. Unrecognized syntax uses exact membership.
94
+ Missing, malformed, or insufficient advertisements produce separate aggregated
95
+ warnings while the server remains authoritative for negotiation. Registration
96
+ and modification move to exact wildcard enforcement in v0.5.0; cancellation
97
+ remains warning-only so an existing registration can still be removed.
98
+
99
+ For warning suppression only, compatible advertised FHIR permission fragments
100
+ may collectively cover a requested permission set. STU2 does not guarantee that
101
+ the server accepts the combined token, so this permissive inference must not be
102
+ reused as a hard-failure decision without a separate normative rule.
103
+
104
+ SMART Backend Services scope intent remains client-owned. In v0.4.x, Safire
105
+ preserves the historical `system/*.rs` fallback only for compatibility and
106
+ logs a deprecation warning whenever it is used. Callers should configure or
107
+ pass scopes explicitly; absent scopes become a runtime `ConfigurationError` in
108
+ v0.6.0. SMART discovery `scopes_supported` is non-exhaustive and is not used as
109
+ an exact allow-list for token requests.
110
+
111
+ `system/` scopes are the normal Backend Services context. The
112
+ [SMART STU2.2 Backend Services scope requirements](https://hl7.org/fhir/smart-app-launch/STU2.2/backend-services.html#scopes)
113
+ also state that `user/` and `patient/` scopes are not prohibited when context
114
+ is established through out-of-band coordination. Safire therefore preserves
115
+ and submits explicit non-system scopes without warning or rejection. The
116
+ library cannot determine whether the caller completed that external
117
+ coordination, and must not make a permitted healthy path noisy based on missing
118
+ local evidence.
119
+
120
+ Scope diagnostics log only the requested scope category and count, never the
121
+ raw token values. SMART v2 scopes may carry FHIR search constraints, and opaque
122
+ custom scopes may encode equally sensitive application context in forms Safire
123
+ cannot safely redact. Count-and-category logging preserves the operational and
124
+ migration signal without copying caller-controlled authorization details into
125
+ application logs.
126
+
127
+ The classifications below were checked against the published
128
+ [SMART App Launch STU2.2 conformance and discovery rules](https://hl7.org/fhir/smart-app-launch/STU2.2/conformance.html),
129
+ [SMART App Launch workflow](https://hl7.org/fhir/smart-app-launch/STU2.2/app-launch.html),
130
+ [SMART Backend Services profile](https://hl7.org/fhir/smart-app-launch/STU2.2/backend-services.html),
131
+ [RFC 7591 Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591),
132
+ [UDAP Security STU2 discovery](https://hl7.org/fhir/us/udap-security/STU2/discovery.html),
133
+ and [UDAP Security STU2 registration](https://hl7.org/fhir/us/udap-security/STU2/registration.html).
134
+
135
+ ## Operation Matrix
136
+
137
+ This matrix covers the public protocol operations currently implemented by the
138
+ `Safire::Client` facade.
139
+
140
+ | Operation | Client obligation | Required capability or input | Runtime hard failure | Runtime warning or negotiation | Explicit diagnostic | Server-owned decision |
141
+ |-----------|-------------------|------------------------------|----------------------|--------------------------------|---------------------|-----------------------|
142
+ | SMART `server_metadata` | Request and consume the well-known JSON object over a protected connection | FHIR base URL | Transport or HTTP failure; unusable JSON-object body | None | Caller may invoke `SmartMetadata#valid?` | Advertised endpoints and capabilities |
143
+ | SMART `authorization_url` | Supply client ID, redirect URI, scopes, and launch context as applicable; Safire adds state and S256 PKCE | Usable authorization endpoint; caller selects GET or POST | Invalid request method; missing client ID, scopes, or redirect URI; missing, malformed, or insecure endpoint | None | Metadata capability helpers and `SmartMetadata#valid?` remain caller-invoked | User authorization, launch context, accepted request method, and granted scopes |
144
+ | SMART `request_access_token` | Supply the authorization code, matching PKCE verifier, redirect URI, and client credentials required by the configured client type | Usable token endpoint | Missing credentials; invalid JWT assertion configuration; missing, malformed, or insecure endpoint; transport, OAuth, unusable response, or missing access-token failure | None | Caller may invoke `token_response_valid?` | Code acceptance and issued token contents |
145
+ | SMART `refresh_token` | Supply the refresh token and authenticate the client; any requested scopes must not exceed the original grant | Usable token endpoint | Missing credentials; invalid JWT assertion configuration; missing, malformed, or insecure endpoint; transport, OAuth, unusable response, or missing access-token failure | None | Caller may invoke `token_response_valid?` | Refresh-token acceptance and issued scope |
146
+ | SMART `request_backend_token` | Supply client ID, a registered signing identity, and explicit scope intent | Usable token endpoint and pre-authorized asymmetric client registration | Missing identity or signing credentials; invalid JWT assertion configuration; missing, malformed, or insecure endpoint; transport, OAuth, unusable response, or missing access-token failure | The v0.4.x legacy scope fallback warns; explicit scopes remain subject to server negotiation | Caller may invoke `token_response_valid?(flow: :backend_services)` | Out-of-band context, client authentication, requested scope, and token issuance |
147
+ | SMART `register_client` | Supply RFC 7591 client metadata and any required initial access token | Explicit or discovered HTTPS registration endpoint | Missing or unsafe endpoint; transport or OAuth failure; unusable response or missing/invalid client ID | None | Discovery capability helpers and `SmartMetadata#valid?` remain caller-invoked | Registration policy, accepted metadata, and issued credentials |
148
+ | SMART `token_response_valid?` | Choose whether to require the optional conformance diagnostic | Token-response Hash and selected flow | None; this method is not an operational gate | Logs each diagnosed response defect and returns `false` | This method is the explicit diagnostic | Caller decides how to handle a failed diagnostic |
149
+ | UDAP `server_metadata` | Supply the intended community and production trust/revocation policy; validate signed metadata before any later workflow | UDAP well-known endpoint and a trusted signed-metadata chain | Transport, HTTP, or 204 outcome; unusable JSON object; failed signature, chain, revocation, issuer, time, or endpoint validation | None | Caller may invoke `UdapMetadata#valid?` or explicitly re-run `signed_metadata_valid?` | Community-specific profiles and capabilities |
150
+ | UDAP `register_client` | Supply conformant metadata, client URI, certifications when required, and a matching private key/certificate chain | Trusted discovery; `udap_dcr`; authoritative registration endpoint; usable algorithm and certification-requirement metadata | Unsafe or unusable readiness data; invalid caller metadata or signing identity; transport or server rejection; pending or malformed completion response | Missing RS256 advertisement and unconfirmed scope support warn in v0.4.1 | Caller may invoke `UdapMetadata#valid?`; it is not a blanket gate | Registration policy, scope and certification acceptance, effective metadata, and issued client ID |
151
+ | UDAP `cancel_registration` | Supply the same stable client URI and trust-community identity; preserve local state until cancellation is confirmed | Registration readiness above and an existing registration to remove | Registration hard failures above; any response that does not provide final 2xx body confirmation with the client ID and empty grants | Scope compatibility and malformed scope advertisements remain warning-only so cleanup can proceed | Caller may invoke `UdapMetadata#valid?`; it is not a blanket gate | Cancellation policy and the confirmation response |
152
+
153
+ SMART cancellation and UDAP authorization, token, refresh, backend-token, and
154
+ token-response operations are not implemented. They raise
155
+ `NotImplementedError` through the shared protocol contract rather than implying
156
+ runtime support.
157
+
158
+ ### Rules for Future Operations
159
+
160
+ Every new public protocol operation must update the matrix and apply these
161
+ questions in order:
162
+
163
+ 1. Which normative actor owns the requirement: client, authorization server,
164
+ resource server, or trust community?
165
+ 2. Does a failure prevent Safire from constructing a conformant request,
166
+ selecting a trusted endpoint, protecting credentials, or confirming the
167
+ operation's result? If so, fail before signing or network activity whenever
168
+ the condition is locally knowable.
169
+ 3. Can Safire still send a safe, conformant request while the server remains
170
+ authoritative for policy or negotiation? If so, preserve the request and use
171
+ a warning only when it gives the caller actionable information.
172
+ 4. Is the check broad server conformance rather than operation readiness? Keep
173
+ it in an explicit diagnostic and do not call it automatically.
174
+ 5. Is untrusted response data consumed by the operation? Validate its type and
175
+ shape before use, and translate failure into the protocol-specific Safire
176
+ error rather than leaking a Ruby implementation exception.
177
+ 6. Does an error prove rejection, or only an unavailable, malformed, pending,
178
+ or otherwise unconfirmed outcome? Use only the strongest description the
179
+ evidence supports.
180
+ 7. Does the implementation invent client intent, retry a possibly committed
181
+ operation, or log caller credentials or authorization details? If so, stop
182
+ and require an explicit, documented policy instead.
183
+
184
+ Shared helpers may encode mechanism, such as JSON-object parsing or
185
+ warning-only UDAP scope coverage, but must not silently promote a diagnostic
186
+ inference into a hard-failure rule for another workflow.
187
+
188
+ ## Consequences
189
+
190
+ **Benefits:**
191
+
192
+ - Runtime failures now have an operation or security rationale rather than
193
+ serving as blanket server certification.
194
+ - Callers retain explicit tools for enforcing stricter server conformance.
195
+ - Shared JSON-object parsing is total for untrusted adapter output and preserves
196
+ protocol-specific error ownership.
197
+ - Valid raw JSON objects remain usable even when response content-type handling
198
+ is imperfect.
199
+
200
+ **Trade-offs:**
201
+
202
+ - The same metadata defect can be diagnostic in one flow and fatal in another
203
+ when the latter needs the affected field to proceed safely.
204
+ - Tolerating a raw JSON object can hide an HTTP content-type defect unless the
205
+ caller separately audits server conformance.
206
+ - The operation matrix must be updated whenever a public protocol operation or
207
+ runtime gate is added or materially changed.