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.
- checksums.yaml +4 -4
- data/.claude/skills/release-safire/SKILL.md +153 -0
- data/.rubocop.yml +11 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +107 -1
- data/CONTRIBUTION.md +6 -1
- data/Gemfile +1 -2
- data/Gemfile.lock +47 -37
- data/README.md +60 -5
- data/ROADMAP.md +61 -12
- data/docs/Gemfile.lock +49 -45
- data/docs/_config.yml +2 -2
- data/docs/adr/ADR-001-activesupport-dependency.md +2 -2
- data/docs/adr/ADR-002-facade-and-forwardable.md +21 -6
- data/docs/adr/ADR-003-protocol-vs-client-type.md +13 -6
- data/docs/adr/ADR-004-clientconfig-immutability-and-entity-masking.md +34 -8
- data/docs/adr/ADR-006-lazy-discovery.md +64 -5
- data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +40 -10
- data/docs/adr/ADR-009-oauth-error-hierarchy.md +131 -0
- data/docs/adr/ADR-010-optional-client-id-dcr-temp-client.md +90 -0
- data/docs/adr/ADR-011-udap-stu2-discovery-conformance.md +113 -0
- data/docs/adr/ADR-012-udap-signed-metadata-validation.md +104 -0
- data/docs/adr/ADR-013-udap-registration-request-model.md +106 -0
- data/docs/adr/ADR-014-udap-software-statement-signing.md +122 -0
- data/docs/adr/index.md +9 -3
- data/docs/advanced.md +22 -25
- data/docs/configuration/client-setup.md +126 -10
- data/docs/configuration/index.md +11 -7
- data/docs/index.md +13 -6
- data/docs/installation.md +3 -2
- data/docs/security.md +44 -5
- data/docs/smart-on-fhir/backend-services/index.md +2 -2
- data/docs/smart-on-fhir/backend-services/token-request.md +1 -1
- data/docs/smart-on-fhir/confidential-asymmetric/index.md +2 -2
- data/docs/smart-on-fhir/confidential-symmetric/index.md +1 -1
- data/docs/smart-on-fhir/discovery/capability-checks.md +7 -0
- data/docs/smart-on-fhir/dynamic-client-registration/index.md +103 -0
- data/docs/smart-on-fhir/dynamic-client-registration/registration.md +160 -0
- data/docs/smart-on-fhir/dynamic-client-registration/response.md +161 -0
- data/docs/smart-on-fhir/index.md +2 -1
- data/docs/smart-on-fhir/post-based-authorization.md +1 -1
- data/docs/smart-on-fhir/public-client/index.md +1 -1
- data/docs/troubleshooting/auth-errors.md +20 -0
- data/docs/troubleshooting/client-errors.md +143 -0
- data/docs/troubleshooting/index.md +61 -4
- data/docs/udap/dynamic-client-registration/index.md +198 -0
- data/docs/udap/dynamic-client-registration/lifecycle.md +115 -0
- data/docs/udap/dynamic-client-registration/registration-metadata.md +133 -0
- data/docs/udap/dynamic-client-registration/software-statement.md +115 -0
- data/docs/udap.md +187 -64
- data/gemfiles/activesupport_71.gemfile +30 -0
- data/gemfiles/activesupport_71.gemfile.lock +291 -0
- data/lib/safire/client.rb +141 -44
- data/lib/safire/client_config.rb +114 -35
- data/lib/safire/client_config_builder.rb +18 -0
- data/lib/safire/errors.rb +100 -44
- data/lib/safire/http_client.rb +7 -2
- data/lib/safire/middleware/https_only_redirects.rb +10 -4
- data/lib/safire/protocols/behaviours.rb +12 -1
- data/lib/safire/protocols/oauth_response_handling.rb +48 -0
- data/lib/safire/protocols/smart.rb +108 -36
- data/lib/safire/protocols/smart_metadata.rb +6 -0
- data/lib/safire/protocols/udap.rb +534 -0
- data/lib/safire/protocols/udap_metadata.rb +429 -0
- data/lib/safire/protocols/udap_registration_metadata.rb +368 -0
- data/lib/safire/protocols/udap_signed_metadata_validator.rb +349 -0
- data/lib/safire/protocols/udap_software_statement.rb +348 -0
- data/lib/safire/protocols.rb +6 -0
- data/lib/safire/uri_validation.rb +102 -0
- data/lib/safire/version.rb +1 -1
- data/lib/safire.rb +2 -0
- data/safire.gemspec +7 -8
- metadata +42 -13
data/ROADMAP.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Safire Roadmap
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Latest Published Release — v0.4.0
|
|
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
|
|
|
7
|
+
The `main` branch may include features not yet published to RubyGems; see the
|
|
8
|
+
[Unreleased section of CHANGELOG.md](CHANGELOG.md#unreleased).
|
|
9
|
+
|
|
7
10
|
Feedback, bug reports, and pull requests are welcome via the [issue tracker](https://github.com/vanessuniq/safire/issues).
|
|
8
11
|
|
|
9
12
|
---
|
|
@@ -20,25 +23,71 @@ Feedback, bug reports, and pull requests are welcome via the [issue tracker](htt
|
|
|
20
23
|
- **JWT Assertion Builder** — signed JWT assertions with configurable `kid` and expiry
|
|
21
24
|
- **PKCE** — automatic code verifier and challenge generation
|
|
22
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
|
|
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
|
+
|
|
28
|
+
### UDAP Security (STU2 / v2.0.0)
|
|
29
|
+
|
|
30
|
+
- **UDAP Discovery** — lazy fetch of `/.well-known/udap`; optional
|
|
31
|
+
community-scoped discovery; STU2 metadata parsing plus explicit
|
|
32
|
+
`UdapMetadata#valid?` structural validation
|
|
33
|
+
- **Signed Metadata Validation** — validates `signed_metadata` JWTs using RS256,
|
|
34
|
+
JOSE `x5c`, certificate chain and revocation policy, required claims, and
|
|
35
|
+
signed endpoint claim precedence
|
|
36
|
+
- **Protocol-Aware Client Facade** — `Safire::Client.new(..., protocol: :udap)`
|
|
37
|
+
exposes UDAP discovery while rejecting SMART-only `client_type:` values
|
|
38
|
+
- **UDAP Dynamic Client Registration** — certificate-backed STU2 new
|
|
39
|
+
registration, modification, and cancellation via signed software statements;
|
|
40
|
+
includes metadata validation, signed endpoint precedence, community-scoped
|
|
41
|
+
discovery, certification envelope handling, RFC 7591-shaped registration
|
|
42
|
+
response parsing, and cancellation confirmation through an empty
|
|
43
|
+
`grant_types` response
|
|
44
|
+
|
|
45
|
+
### Demo Application
|
|
46
|
+
|
|
47
|
+
- **Protocol-Aware Workflows** — Sinatra demo supports protocol-aware server
|
|
48
|
+
setup, SMART discovery, registration, authorization, and token workflows,
|
|
49
|
+
plus UDAP signed metadata discovery and the registration/cancellation
|
|
50
|
+
lifecycle
|
|
23
51
|
|
|
24
52
|
---
|
|
25
53
|
|
|
26
54
|
## Planned Features
|
|
27
55
|
|
|
28
|
-
### SMART App Launch
|
|
29
|
-
|
|
30
|
-
- **Dynamic Client Registration** — programmatic client registration per [RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)
|
|
31
|
-
|
|
32
56
|
### UDAP Security
|
|
33
57
|
|
|
34
|
-
- **UDAP Discovery** — `/.well-known/udap` metadata fetch and validation
|
|
35
|
-
- **UDAP Dynamic Client Registration** — signed software statements and client registration
|
|
36
58
|
- **UDAP JWT Client Auth** — B2B and consumer-facing authorization flows
|
|
37
59
|
- **Tiered OAuth** — identity chaining for multi-system access
|
|
38
60
|
|
|
39
61
|
### Quality and Compliance
|
|
40
62
|
|
|
41
|
-
- **Inferno SMART App Launch
|
|
63
|
+
- **Inferno SMART App Launch STU 2.2 Test Suite** — compliance validation using
|
|
64
|
+
[Inferno](https://inferno-framework.github.io/) as a mock EHR authorization server,
|
|
65
|
+
with Safire's demo app acting as the SMART client. Delivered in two phases.
|
|
66
|
+
|
|
67
|
+
**Phase 1 — HTTP-only flows (no browser automation required)**
|
|
68
|
+
- Discovery (local conformance gate): Safire discovers Inferno's
|
|
69
|
+
`/.well-known/smart-configuration` and validates the parsed metadata via
|
|
70
|
+
`SmartMetadata#valid?`; this is a local parsing and conformance check,
|
|
71
|
+
not an Inferno-driven assertion
|
|
72
|
+
- Backend Services (Inferno-driven): JWT assertion construction, `client_credentials`
|
|
73
|
+
token request format, and token response validation against Inferno's mock token
|
|
74
|
+
endpoint
|
|
75
|
+
- Inferno test results published as a GitHub Actions artifact (static HTML report
|
|
76
|
+
generated from Inferno's JSON output)
|
|
77
|
+
|
|
78
|
+
**Phase 2 — Authorization code flows (browser automation via Capybara)**
|
|
79
|
+
- Standalone Patient Launch for all three client types: public (PKCE-only),
|
|
80
|
+
confidential symmetric (`client_secret_basic`), and confidential asymmetric
|
|
81
|
+
(`private_key_jwt`)
|
|
82
|
+
- EHR Launch: Inferno redirects the user agent to the demo app's `GET /launch`
|
|
83
|
+
endpoint with `iss` and `launch` as query parameters, exercising the EHR-initiated
|
|
84
|
+
authorization code flow as a separate Inferno test group
|
|
85
|
+
- Infrastructure: Docker Compose for a local Inferno instance, Capybara and headless
|
|
86
|
+
Chrome for OAuth consent screen automation
|
|
87
|
+
|
|
88
|
+
**Note on Client Registration:** Inferno's reference server documentation states that
|
|
89
|
+
there is currently no registration process and apps must use preconfigured client IDs,
|
|
90
|
+
so DCR is not covered under this Inferno-based plan.
|
|
42
91
|
|
|
43
92
|
---
|
|
44
93
|
|
|
@@ -46,9 +95,9 @@ Feedback, bug reports, and pull requests are welcome via the [issue tracker](htt
|
|
|
46
95
|
|
|
47
96
|
| Component | Version |
|
|
48
97
|
|-----------|---------|
|
|
49
|
-
| Ruby | ≥
|
|
50
|
-
| ActiveSupport |
|
|
51
|
-
| Rails (optional) | 7.
|
|
98
|
+
| Ruby | ≥ 3.2 |
|
|
99
|
+
| ActiveSupport | ≥ 7.1, < 9 |
|
|
100
|
+
| Rails (optional) | ≥ 7.1 |
|
|
52
101
|
| SMART App Launch | 2.2.0 (STU2) |
|
|
53
|
-
| UDAP Security |
|
|
102
|
+
| UDAP Security | 2.0.0 (STU2 discovery and Dynamic Client Registration lifecycle implemented; auth flows planned) |
|
|
54
103
|
| FHIR | R4, R4B |
|
data/docs/Gemfile.lock
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
GEM
|
|
2
2
|
remote: https://rubygems.org/
|
|
3
3
|
specs:
|
|
4
|
-
addressable (2.
|
|
4
|
+
addressable (2.9.0)
|
|
5
5
|
public_suffix (>= 2.0.2, < 8.0)
|
|
6
6
|
base64 (0.3.0)
|
|
7
|
-
bigdecimal (4.1.
|
|
7
|
+
bigdecimal (4.1.2)
|
|
8
8
|
colorator (1.1.0)
|
|
9
9
|
concurrent-ruby (1.3.6)
|
|
10
|
-
csv (3.3.
|
|
10
|
+
csv (3.3.6)
|
|
11
11
|
em-websocket (0.5.3)
|
|
12
12
|
eventmachine (>= 0.12.9)
|
|
13
13
|
http_parser.rb (~> 0)
|
|
14
14
|
eventmachine (1.2.7)
|
|
15
|
-
ffi (1.17.
|
|
16
|
-
ffi (1.17.
|
|
17
|
-
ffi (1.17.
|
|
18
|
-
ffi (1.17.
|
|
15
|
+
ffi (1.17.4)
|
|
16
|
+
ffi (1.17.4-arm-linux-gnu)
|
|
17
|
+
ffi (1.17.4-arm-linux-musl)
|
|
18
|
+
ffi (1.17.4-x86_64-linux-gnu)
|
|
19
19
|
forwardable-extended (2.6.0)
|
|
20
|
-
google-protobuf (4.
|
|
20
|
+
google-protobuf (4.34.1)
|
|
21
21
|
bigdecimal
|
|
22
|
-
rake (
|
|
22
|
+
rake (~> 13.3)
|
|
23
|
+
google-protobuf (4.34.1-x86_64-linux-gnu)
|
|
24
|
+
bigdecimal
|
|
25
|
+
rake (~> 13.3)
|
|
23
26
|
http_parser.rb (0.8.1)
|
|
24
27
|
i18n (1.14.8)
|
|
25
28
|
concurrent-ruby (~> 1.0)
|
|
@@ -48,13 +51,13 @@ GEM
|
|
|
48
51
|
jekyll (>= 3.7, < 5.0)
|
|
49
52
|
jekyll-sass-converter (3.1.0)
|
|
50
53
|
sass-embedded (~> 1.75)
|
|
51
|
-
jekyll-seo-tag (2.
|
|
54
|
+
jekyll-seo-tag (2.9.0)
|
|
52
55
|
jekyll (>= 3.8, < 5.0)
|
|
53
56
|
jekyll-sitemap (1.4.0)
|
|
54
57
|
jekyll (>= 3.7, < 5.0)
|
|
55
58
|
jekyll-watch (2.2.1)
|
|
56
59
|
listen (~> 3.0)
|
|
57
|
-
json (2.
|
|
60
|
+
json (2.19.5)
|
|
58
61
|
just-the-docs (0.12.0)
|
|
59
62
|
jekyll (>= 3.8.5)
|
|
60
63
|
jekyll-include-cache
|
|
@@ -73,34 +76,34 @@ GEM
|
|
|
73
76
|
mercenary (0.4.0)
|
|
74
77
|
pathutil (0.16.2)
|
|
75
78
|
forwardable-extended (~> 2.6)
|
|
76
|
-
public_suffix (7.0.
|
|
77
|
-
rake (13.
|
|
79
|
+
public_suffix (7.0.5)
|
|
80
|
+
rake (13.4.2)
|
|
78
81
|
rb-fsevent (0.11.2)
|
|
79
82
|
rb-inotify (0.11.1)
|
|
80
83
|
ffi (~> 1.0)
|
|
81
84
|
rexml (3.4.4)
|
|
82
85
|
rouge (4.7.0)
|
|
83
86
|
safe_yaml (1.0.5)
|
|
84
|
-
sass-embedded (1.
|
|
87
|
+
sass-embedded (1.99.0)
|
|
85
88
|
google-protobuf (~> 4.31)
|
|
86
89
|
rake (>= 13)
|
|
87
|
-
sass-embedded (1.
|
|
90
|
+
sass-embedded (1.99.0-aarch64-linux-android)
|
|
88
91
|
google-protobuf (~> 4.31)
|
|
89
|
-
sass-embedded (1.
|
|
92
|
+
sass-embedded (1.99.0-arm-linux-androideabi)
|
|
90
93
|
google-protobuf (~> 4.31)
|
|
91
|
-
sass-embedded (1.
|
|
94
|
+
sass-embedded (1.99.0-arm-linux-gnueabihf)
|
|
92
95
|
google-protobuf (~> 4.31)
|
|
93
|
-
sass-embedded (1.
|
|
96
|
+
sass-embedded (1.99.0-arm-linux-musleabihf)
|
|
94
97
|
google-protobuf (~> 4.31)
|
|
95
|
-
sass-embedded (1.
|
|
98
|
+
sass-embedded (1.99.0-riscv64-linux-android)
|
|
96
99
|
google-protobuf (~> 4.31)
|
|
97
|
-
sass-embedded (1.
|
|
100
|
+
sass-embedded (1.99.0-riscv64-linux-gnu)
|
|
98
101
|
google-protobuf (~> 4.31)
|
|
99
|
-
sass-embedded (1.
|
|
102
|
+
sass-embedded (1.99.0-riscv64-linux-musl)
|
|
100
103
|
google-protobuf (~> 4.31)
|
|
101
|
-
sass-embedded (1.
|
|
104
|
+
sass-embedded (1.99.0-x86_64-linux-android)
|
|
102
105
|
google-protobuf (~> 4.31)
|
|
103
|
-
sass-embedded (1.
|
|
106
|
+
sass-embedded (1.99.0-x86_64-linux-gnu)
|
|
104
107
|
google-protobuf (~> 4.31)
|
|
105
108
|
terminal-table (3.0.2)
|
|
106
109
|
unicode-display_width (>= 1.1.1, < 3)
|
|
@@ -138,30 +141,31 @@ DEPENDENCIES
|
|
|
138
141
|
webrick
|
|
139
142
|
|
|
140
143
|
CHECKSUMS
|
|
141
|
-
addressable (2.
|
|
144
|
+
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
|
|
142
145
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
|
143
|
-
bigdecimal (4.1.
|
|
146
|
+
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
|
|
144
147
|
colorator (1.1.0) sha256=e2f85daf57af47d740db2a32191d1bdfb0f6503a0dfbc8327d0c9154d5ddfc38
|
|
145
148
|
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
|
|
146
|
-
csv (3.3.
|
|
149
|
+
csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456
|
|
147
150
|
em-websocket (0.5.3) sha256=f56a92bde4e6cb879256d58ee31f124181f68f8887bd14d53d5d9a292758c6a8
|
|
148
151
|
eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972
|
|
149
|
-
ffi (1.17.
|
|
150
|
-
ffi (1.17.
|
|
151
|
-
ffi (1.17.
|
|
152
|
-
ffi (1.17.
|
|
152
|
+
ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
|
|
153
|
+
ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564
|
|
154
|
+
ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95
|
|
155
|
+
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
|
153
156
|
forwardable-extended (2.6.0) sha256=1bec948c469bbddfadeb3bd90eb8c85f6e627a412a3e852acfd7eaedbac3ec97
|
|
154
|
-
google-protobuf (4.
|
|
157
|
+
google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc
|
|
158
|
+
google-protobuf (4.34.1-x86_64-linux-gnu) sha256=87088c9fd8e47b5b40ca498fc1195add6149e941ff7e81c532a5b0b8876d4cc9
|
|
155
159
|
http_parser.rb (0.8.1) sha256=9ae8df145b39aa5398b2f90090d651c67bd8e2ebfe4507c966579f641e11097a
|
|
156
160
|
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
|
|
157
161
|
jekyll (4.4.1) sha256=4c1144d857a5b2b80d45b8cf5138289579a9f8136aadfa6dd684b31fe2bc18c1
|
|
158
162
|
jekyll-feed (0.17.0) sha256=689aab16c877949bb9e7a5c436de6278318a51ecb974792232fd94d8b3acfcc3
|
|
159
163
|
jekyll-include-cache (0.2.1) sha256=c7d4b9e551732a27442cb2ce853ba36a2f69c66603694b8c1184c99ab1a1a205
|
|
160
164
|
jekyll-sass-converter (3.1.0) sha256=83925d84f1d134410c11d0c6643b0093e82e3a3cf127e90757a85294a3862443
|
|
161
|
-
jekyll-seo-tag (2.
|
|
165
|
+
jekyll-seo-tag (2.9.0) sha256=0260015a8e1df9bf195cdfb0c675b7b2883fd8cbf12556e1c1cbe36a831c6852
|
|
162
166
|
jekyll-sitemap (1.4.0) sha256=0de08c5debc185ea5a8f980e1025c7cd3f8e0c35c8b6ef592f15c46235cf4218
|
|
163
167
|
jekyll-watch (2.2.1) sha256=bc44ed43f5e0a552836245a54dbff3ea7421ecc2856707e8a1ee203a8387a7e1
|
|
164
|
-
json (2.
|
|
168
|
+
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
|
|
165
169
|
just-the-docs (0.12.0) sha256=15f2839ac9082898d60f33b978aa6f8e46fc50ba8fac20ae7a7f0e1fb295523e
|
|
166
170
|
kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa
|
|
167
171
|
kramdown-parser-gfm (1.1.0) sha256=fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729
|
|
@@ -170,23 +174,23 @@ CHECKSUMS
|
|
|
170
174
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
171
175
|
mercenary (0.4.0) sha256=b25a1e4a59adca88665e08e24acf0af30da5b5d859f7d8f38fba52c28f405138
|
|
172
176
|
pathutil (0.16.2) sha256=e43b74365631cab4f6d5e4228f812927efc9cb2c71e62976edcb252ee948d589
|
|
173
|
-
public_suffix (7.0.
|
|
174
|
-
rake (13.
|
|
177
|
+
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
|
|
178
|
+
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
175
179
|
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
|
|
176
180
|
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
|
|
177
181
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
|
178
182
|
rouge (4.7.0) sha256=dba5896715c0325c362e895460a6d350803dbf6427454f49a47500f3193ea739
|
|
179
183
|
safe_yaml (1.0.5) sha256=a6ac2d64b7eb027bdeeca1851fe7e7af0d668e133e8a88066a0c6f7087d9f848
|
|
180
|
-
sass-embedded (1.
|
|
181
|
-
sass-embedded (1.
|
|
182
|
-
sass-embedded (1.
|
|
183
|
-
sass-embedded (1.
|
|
184
|
-
sass-embedded (1.
|
|
185
|
-
sass-embedded (1.
|
|
186
|
-
sass-embedded (1.
|
|
187
|
-
sass-embedded (1.
|
|
188
|
-
sass-embedded (1.
|
|
189
|
-
sass-embedded (1.
|
|
184
|
+
sass-embedded (1.99.0) sha256=61942229dc7da4f335688196d43c1d3d5314060667a43ccf48f4b4c41b5578e1
|
|
185
|
+
sass-embedded (1.99.0-aarch64-linux-android) sha256=3b077c8436424b45eb04edaa46514fc92cc333f17b3fcca45857d124d453cdf1
|
|
186
|
+
sass-embedded (1.99.0-arm-linux-androideabi) sha256=9089a3aca8f16f855a2fc848c0762b09aef23251f91ec9714fea31c28c9094df
|
|
187
|
+
sass-embedded (1.99.0-arm-linux-gnueabihf) sha256=f5f0748934660cda6948917af6dc974caf4d36ea6121e450982153b7d9c49b55
|
|
188
|
+
sass-embedded (1.99.0-arm-linux-musleabihf) sha256=29a4056e76bc136025ba5d03e82d86ecdabfb6c07a473a4fdedcd72fb28dbbc4
|
|
189
|
+
sass-embedded (1.99.0-riscv64-linux-android) sha256=7ac1145c066670d5918a7afae8445338c45b4d8e34ad2968bda7099665ba8d15
|
|
190
|
+
sass-embedded (1.99.0-riscv64-linux-gnu) sha256=b8c421eba2e41fa3c94bffc971611d803aae336586a3baf2933c99e1c7548f88
|
|
191
|
+
sass-embedded (1.99.0-riscv64-linux-musl) sha256=63b924eb97548bd1f31193c36fab89bb100cd1a9bd144789a9a596fc98244a09
|
|
192
|
+
sass-embedded (1.99.0-x86_64-linux-android) sha256=39c51b80c292fdb5157fee9166793374104c4e6639d77dfefc36c339aa5dce12
|
|
193
|
+
sass-embedded (1.99.0-x86_64-linux-gnu) sha256=a4e2ae5e9951815cb8b3ab408cee8b800852491988a57de735f18d259c70d7c4
|
|
190
194
|
terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91
|
|
191
195
|
unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a
|
|
192
196
|
webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
|
data/docs/_config.yml
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
# in the templates via {{ site.myvariable }}.
|
|
20
20
|
|
|
21
21
|
title: Safire Documentation
|
|
22
|
-
tagline: Ruby gem for SMART App Launch and UDAP
|
|
23
|
-
description: SMART App Launch and UDAP
|
|
22
|
+
tagline: Ruby gem for SMART App Launch and UDAP discovery and registration
|
|
23
|
+
description: SMART App Launch and UDAP discovery and registration library for Ruby
|
|
24
24
|
author: Vanessa Fotso
|
|
25
25
|
lang: en-US
|
|
26
26
|
baseurl: /safire
|
|
@@ -31,7 +31,7 @@ Safire is also commonly used alongside Rails applications, where ActiveSupport i
|
|
|
31
31
|
|
|
32
32
|
## Decision
|
|
33
33
|
|
|
34
|
-
Use `require 'active_support/all'` and treat ActiveSupport as a first-class runtime dependency (`spec.add_dependency 'activesupport', '
|
|
34
|
+
Use `require 'active_support/all'` and treat ActiveSupport as a first-class runtime dependency (`spec.add_dependency 'activesupport', '>= 7.1', '< 9'`).
|
|
35
35
|
|
|
36
36
|
ActiveSupport utilities (`present?`, `blank?`, `fetch`, safe navigation, etc.) may be used freely throughout the codebase without needing to track individual requires.
|
|
37
37
|
|
|
@@ -46,5 +46,5 @@ ActiveSupport utilities (`present?`, `blank?`, `fetch`, safe navigation, etc.) m
|
|
|
46
46
|
- Minimal overhead in non-Rails applications due to AS autoloading
|
|
47
47
|
|
|
48
48
|
**Trade-offs:**
|
|
49
|
-
- `activesupport` is
|
|
49
|
+
- `activesupport` is constrained to `>= 7.1, < 9` — non-Rails Ruby applications must accept this dependency; a future major AS version bump (v9) requires a Safire dependency update
|
|
50
50
|
- Developers unfamiliar with AS may not recognise AS methods as external — mitigated by the fact that AS conventions (`present?`, `blank?`) are widely known in the Ruby ecosystem
|
|
@@ -42,22 +42,33 @@ class Client
|
|
|
42
42
|
def_delegators :protocol_client,
|
|
43
43
|
:server_metadata, :authorization_url,
|
|
44
44
|
:request_access_token, :refresh_token,
|
|
45
|
-
:
|
|
45
|
+
:request_backend_token,
|
|
46
|
+
:token_response_valid?, :register_client,
|
|
47
|
+
:cancel_registration
|
|
46
48
|
|
|
47
49
|
private
|
|
48
50
|
|
|
49
51
|
def protocol_client
|
|
50
|
-
@protocol_client ||=
|
|
52
|
+
@protocol_client ||= build_protocol_client
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_protocol_client
|
|
56
|
+
case @protocol
|
|
57
|
+
when :smart then Protocols::Smart.new(config, client_type:)
|
|
58
|
+
when :udap then Protocols::Udap.new(config)
|
|
59
|
+
end
|
|
51
60
|
end
|
|
52
61
|
end
|
|
53
62
|
```
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
`Protocols::Smart` and `Protocols::Udap` include `Protocols::Behaviours` to
|
|
65
|
+
declare the required interface. Unsupported methods inherit the default
|
|
66
|
+
`NotImplementedError`. Adding another protocol requires:
|
|
56
67
|
1. Implementing the `Behaviours` interface in a new class
|
|
57
|
-
2. Adding
|
|
68
|
+
2. Adding a `when` branch to `build_protocol_client` in `Client`
|
|
58
69
|
3. Adding its valid client types to `PROTOCOL_CLIENT_TYPES`
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
The original design aimed for "no changes to `Client` itself" when adding a protocol. That invariant was intentionally relaxed: protocols have different constructor signatures (`Smart` takes `client_type:`, `Udap` does not), and a small explicit `case` in `build_protocol_client` is preferable to forcing every protocol into a uniform constructor it does not need.
|
|
61
72
|
|
|
62
73
|
**Why `Forwardable` over `method_missing`:** `Forwardable` is explicit — the delegated method list is visible in the class body, easy to grep, and YARD-documented. `method_missing` is implicit, difficult to introspect, and catches typos silently.
|
|
63
74
|
|
|
@@ -72,8 +83,12 @@ No changes to `Client` itself.
|
|
|
72
83
|
- Protocol implementations are independently testable
|
|
73
84
|
- `Forwardable` delegation is explicit and greppable
|
|
74
85
|
- The `protocol:` keyword cleanly selects the implementation class without leaking subclass names to callers
|
|
75
|
-
- `client_type=` mutation works naturally — the facade updates
|
|
86
|
+
- SMART `client_type=` mutation works naturally — the facade updates
|
|
87
|
+
`@protocol_client` in place (see [ADR-006]({% link adr/ADR-006-lazy-discovery.md %})
|
|
88
|
+
for why this preserves cached discovery); UDAP rejects `client_type=` because
|
|
89
|
+
SMART client types are not applicable
|
|
76
90
|
|
|
77
91
|
**Trade-offs:**
|
|
78
92
|
- `Client` itself has no runtime behaviour — all logic lives in protocol classes; contributors must know to look in `Protocols::Smart` for SMART logic, not in `Client`
|
|
79
93
|
- `def_delegators` does not forward keyword arguments transparently in all Ruby versions — method signatures in `Behaviours` must be compatible with delegation
|
|
94
|
+
- Adding a new protocol requires a one-line change to `build_protocol_client` in `Client`; this is a small, contained cost that buys protocol-specific constructor freedom
|
|
@@ -31,7 +31,13 @@ Safire::Client.new(config, protocol: :smart, client_type: :confidential_symmetri
|
|
|
31
31
|
Safire::Client.new(config, protocol: :udap) # client_type not applicable
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The key structural difference: SMART has three client authentication methods
|
|
34
|
+
The key structural difference: SMART has three client authentication methods,
|
|
35
|
+
while UDAP does not expose SMART-style client types. The UDAP specification
|
|
36
|
+
defines X.509-backed signed JWT assertions (AnT), although those authentication
|
|
37
|
+
flows are not yet implemented by Safire. Mixing the dimensions into one flat
|
|
38
|
+
enum would create invalid combinations (`:udap_public`,
|
|
39
|
+
`:udap_confidential_symmetric`) and make `client_type=` mutation impossible to
|
|
40
|
+
express cleanly.
|
|
35
41
|
|
|
36
42
|
---
|
|
37
43
|
|
|
@@ -44,12 +50,12 @@ VALID_PROTOCOLS = %i[smart udap].freeze
|
|
|
44
50
|
|
|
45
51
|
PROTOCOL_CLIENT_TYPES = {
|
|
46
52
|
smart: %i[public confidential_symmetric confidential_asymmetric],
|
|
47
|
-
udap: nil #
|
|
53
|
+
udap: nil # SMART client type is not applicable
|
|
48
54
|
}.freeze
|
|
49
55
|
```
|
|
50
56
|
|
|
51
57
|
- `protocol:` is validated against `VALID_PROTOCOLS`; an unknown protocol raises `ConfigurationError`
|
|
52
|
-
- `client_type:`
|
|
58
|
+
- `client_type:` defaults to `nil`. For `:smart`, `nil` resolves to `:public` before validation. For `:udap`, `nil` is the only accepted value — passing any explicit `client_type:` at construction or via `client_type=` raises `ConfigurationError`
|
|
53
59
|
- Changing `client_type=` on a SMART client updates the underlying protocol client in place — already-fetched server metadata is preserved and no re-discovery occurs
|
|
54
60
|
|
|
55
61
|
---
|
|
@@ -57,11 +63,12 @@ PROTOCOL_CLIENT_TYPES = {
|
|
|
57
63
|
## Consequences
|
|
58
64
|
|
|
59
65
|
**Benefits:**
|
|
60
|
-
- No invalid combinations —
|
|
66
|
+
- No invalid combinations — the facade rejects every explicit UDAP
|
|
67
|
+
`client_type:` value at construction and assignment
|
|
61
68
|
- `client_type=` mutation is clean and natural for the "discover first, then select client type" pattern
|
|
62
69
|
- Adding a new SMART client type requires only adding a symbol to `PROTOCOL_CLIENT_TYPES[:smart]`
|
|
63
|
-
- Adding a new protocol requires adding
|
|
70
|
+
- Adding a new protocol requires adding an entry to `PROTOCOL_CLIENT_TYPES` and a branch to `build_protocol_client` (see [ADR-002]({% link adr/ADR-002-facade-and-forwardable.md %}))
|
|
64
71
|
|
|
65
72
|
**Trade-offs:**
|
|
66
73
|
- Two keyword args instead of one — a caller needs to know which dimension belongs to which kwarg; mitigated by clear documentation and validation errors that name the invalid parameter
|
|
67
|
-
- `client_type:` defaults to
|
|
74
|
+
- `client_type:` defaults to `nil` — for SMART callers who previously relied on the `:public` default, behavior is unchanged; for UDAP callers, passing an explicit value now raises rather than silently no-oping, which is stricter but prevents misconfiguration
|
|
@@ -13,7 +13,9 @@ nav_order: 4
|
|
|
13
13
|
|
|
14
14
|
## Context
|
|
15
15
|
|
|
16
|
-
`ClientConfig` holds all credentials and endpoints for a Safire client — including
|
|
16
|
+
`ClientConfig` holds all credentials and endpoints for a Safire client — including
|
|
17
|
+
`client_secret`, `private_key`, and the client certificate chain intended for
|
|
18
|
+
UDAP signing. Two separate concerns need to be addressed:
|
|
17
19
|
|
|
18
20
|
**Concern 1 — Mutability:** should `ClientConfig` allow attributes to be changed after construction?
|
|
19
21
|
|
|
@@ -27,33 +29,57 @@ These two concerns are related: if `ClientConfig` is mutable, masking is harder
|
|
|
27
29
|
|
|
28
30
|
## Decision
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
**The `ClientConfig` configuration surface is immutable after construction.**
|
|
33
|
+
All attributes are `attr_reader` only — no setters — and validation runs once
|
|
34
|
+
at construction. Mutable credential collections that require a stable order,
|
|
35
|
+
such as `certificate_chain`, are defensively stored as described below.
|
|
31
36
|
|
|
32
37
|
**Sensitive attributes are masked at two layers** via the `Entity` base class:
|
|
33
38
|
|
|
34
|
-
**Layer 1 — `#to_hash`:** the `sensitive_attributes` hook (overridden in
|
|
39
|
+
**Layer 1 — `#to_hash`:** the `sensitive_attributes` hook (overridden in
|
|
40
|
+
`ClientConfig` to return `[:client_secret, :private_key, :certificate_chain]`)
|
|
41
|
+
causes those values to appear as `'[FILTERED]'` in any hash serialisation.
|
|
35
42
|
|
|
36
43
|
```ruby
|
|
37
44
|
def to_hash
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
46
|
+
key = var.to_s.delete_prefix('@').to_sym
|
|
47
|
+
value = instance_variable_get(var)
|
|
48
|
+
hash[key] = sensitive_attributes.include?(key) && !value.nil? ? '[FILTERED]' : value
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
```
|
|
44
52
|
|
|
45
53
|
**Layer 2 — `#inspect`:** `ClientConfig` overrides `inspect` directly, emitting `[FILTERED]` for sensitive attributes. This prevents credential leakage in exception backtraces, IRB/pry sessions, and logging middleware that calls `inspect` on objects.
|
|
46
54
|
|
|
55
|
+
Although X.509 certificates contain public material, `certificate_chain` is
|
|
56
|
+
masked because it can be large and identifies the client's operational signing
|
|
57
|
+
identity. The configured chain collection is defensively copied and frozen.
|
|
58
|
+
PEM strings are copied and frozen; certificate objects are stored as immutable
|
|
59
|
+
DER snapshots and materialized as fresh `OpenSSL::X509::Certificate` instances
|
|
60
|
+
whenever the public accessor is called. Mutating either the caller-owned
|
|
61
|
+
certificate or an accessor result therefore cannot alter the configured
|
|
62
|
+
identity. Certificate parsing from PEM, private-key matching, validity checks,
|
|
63
|
+
and URI SAN checks remain the responsibility of the UDAP software-statement
|
|
64
|
+
builder. The leaf-first ordering follows the
|
|
65
|
+
[UDAP Security STU2 JWT header requirements](https://hl7.org/fhir/us/udap-security/STU2/general.html#jwt-headers).
|
|
66
|
+
|
|
47
67
|
---
|
|
48
68
|
|
|
49
69
|
## Consequences
|
|
50
70
|
|
|
51
71
|
**Benefits:**
|
|
52
|
-
-
|
|
72
|
+
- Configuration attributes cannot be reassigned through `ClientConfig`
|
|
73
|
+
- The order and PEM contents of a configured certificate-chain collection
|
|
74
|
+
cannot be changed through the original input array or strings
|
|
53
75
|
- Credentials cannot leak through `inspect`, `to_s`, exception trackers, or log output
|
|
54
|
-
-
|
|
76
|
+
- Configuration shape and URI validation run at construction; flow-specific
|
|
77
|
+
credential and signing checks run when the operation uses them
|
|
55
78
|
- The `sensitive_attributes` hook is extensible — subclasses can add fields without modifying `Entity`
|
|
56
79
|
|
|
57
80
|
**Trade-offs:**
|
|
58
81
|
- Callers cannot modify a `ClientConfig` in place — they must construct a new one; this is intentional and makes state changes explicit
|
|
59
82
|
- `private_key` masking means the key object itself is not serialisable via `to_hash` — callers needing to inspect or store the key must access it directly via `config.private_key`
|
|
83
|
+
- `certificate_chain` masking similarly means callers must access
|
|
84
|
+
`config.certificate_chain` directly when passing the configured identity to a
|
|
85
|
+
signing operation
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
layout: default
|
|
3
|
-
title: "ADR-006: Lazy
|
|
3
|
+
title: "ADR-006: Lazy discovery — no HTTP in constructors"
|
|
4
4
|
parent: Architecture Decision Records
|
|
5
5
|
nav_order: 6
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# ADR-006: Lazy
|
|
8
|
+
# ADR-006: Lazy discovery — no HTTP in constructors
|
|
9
9
|
|
|
10
10
|
**Status:** Accepted
|
|
11
11
|
|
|
@@ -53,7 +53,9 @@ With eager discovery, changing `client_type` must not trigger re-discovery — t
|
|
|
53
53
|
|
|
54
54
|
## Decision
|
|
55
55
|
|
|
56
|
-
Discovery is lazy and memoised at the `Protocols::Smart`
|
|
56
|
+
Discovery is lazy and memoised at the protocol instance level. Both `Protocols::Smart` and `Protocols::Udap` follow this pattern.
|
|
57
|
+
|
|
58
|
+
**SMART** memoises a single metadata object at the instance level:
|
|
57
59
|
|
|
58
60
|
```ruby
|
|
59
61
|
def server_metadata
|
|
@@ -68,15 +70,72 @@ end
|
|
|
68
70
|
|
|
69
71
|
`Safire::Client` memoises the protocol client itself (`@protocol_client ||= ...`), so changing `client_type=` reuses the existing `Protocols::Smart` instance — and thus its already-fetched `@server_metadata` — rather than constructing a new one. This is the mechanism that prevents double-discovery on `client_type=` changes.
|
|
70
72
|
|
|
73
|
+
**UDAP** memoises a Hash keyed by community URI string or `:default`, plus the caller's trust
|
|
74
|
+
policy. The same server can host multiple communities at separate `?community=<uri>` scopes, and
|
|
75
|
+
the same community can be evaluated under different trust anchors, CRLs, or revocation checkers.
|
|
76
|
+
Cache hits revalidate the cached `signed_metadata` before reuse; if validation fails, the cached
|
|
77
|
+
entry is discarded and discovery is fetched again.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
def server_metadata(community: nil, trusted_anchors: [], crls: [], revocation_checker: nil, verify_chain: true)
|
|
81
|
+
community = normalize_community(community)
|
|
82
|
+
trust_policy = {
|
|
83
|
+
trusted_anchors:,
|
|
84
|
+
crls:,
|
|
85
|
+
revocation_checker:,
|
|
86
|
+
verify_chain:,
|
|
87
|
+
allow_insecure_localhost: @allow_insecure_localhost
|
|
88
|
+
}
|
|
89
|
+
cache_key = build_cache_key(community, trusted_anchors, crls, revocation_checker, verify_chain)
|
|
90
|
+
cached_entry = @metadata_cache[cache_key]
|
|
91
|
+
return cached_entry.fetch(:metadata) if cached_entry && cached_entry_valid?(cached_entry, trust_policy)
|
|
92
|
+
|
|
93
|
+
@metadata_cache.delete(cache_key)
|
|
94
|
+
|
|
95
|
+
entry = fetch_metadata(
|
|
96
|
+
community:,
|
|
97
|
+
trust_policy:
|
|
98
|
+
)
|
|
99
|
+
@metadata_cache[cache_key] = entry
|
|
100
|
+
entry.fetch(:metadata)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fetch_metadata(community:, trust_policy:)
|
|
104
|
+
endpoint = well_known_endpoint(community:)
|
|
105
|
+
response = @http_client.get(endpoint)
|
|
106
|
+
check_204!(response, endpoint:, community:)
|
|
107
|
+
raw = parse_discovery_body(response.body, endpoint)
|
|
108
|
+
signed_claims = validate_signed_metadata!(
|
|
109
|
+
raw,
|
|
110
|
+
endpoint:,
|
|
111
|
+
community:,
|
|
112
|
+
trust_policy:
|
|
113
|
+
)
|
|
114
|
+
{
|
|
115
|
+
metadata: UdapMetadata.new(
|
|
116
|
+
raw.merge(signed_claims),
|
|
117
|
+
allow_insecure_localhost: @allow_insecure_localhost
|
|
118
|
+
),
|
|
119
|
+
raw:
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`server_metadata(community:, trusted_anchors:, crls:, revocation_checker:, verify_chain:)` uses UDAP-specific parameters. Calling any of these on a SMART client raises `ArgumentError` from Ruby's own keyword argument checking — this is intentional and correct, since community scoping and UDAP certificate trust policy are UDAP concepts.
|
|
125
|
+
|
|
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
|
+
|
|
71
128
|
---
|
|
72
129
|
|
|
73
130
|
## Consequences
|
|
74
131
|
|
|
75
132
|
**Benefits:**
|
|
76
133
|
- `Safire::Client.new` is instantaneous — no network calls, no stubs required at construction time
|
|
77
|
-
-
|
|
134
|
+
- Construction-time configuration errors are raised before HTTP; requirements
|
|
135
|
+
specific to an operation are validated when that operation runs
|
|
78
136
|
- Callers control when discovery happens — supports application-level caching patterns (see [Advanced Examples]({{ site.baseurl }}/advanced/#metadata-caching))
|
|
79
|
-
- `client_type=` mutation preserves cached metadata — no re-discovery
|
|
137
|
+
- `client_type=` mutation preserves cached SMART metadata — no re-discovery
|
|
138
|
+
- UDAP community-and-trust-policy cache allows a single client instance to serve multiple communities without serving stale signed metadata
|
|
80
139
|
|
|
81
140
|
**Trade-offs:**
|
|
82
141
|
- 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
|