safire 0.3.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/release-safire/SKILL.md +6 -4
  3. data/.rubocop.yml +11 -1
  4. data/.tool-versions +1 -1
  5. data/CHANGELOG.md +74 -1
  6. data/CONTRIBUTION.md +6 -1
  7. data/Gemfile +1 -2
  8. data/Gemfile.lock +45 -35
  9. data/README.md +59 -5
  10. data/ROADMAP.md +60 -8
  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 +51 -0
  20. data/docs/adr/ADR-010-optional-client-id-dcr-temp-client.md +31 -1
  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 +7 -3
  26. data/docs/configuration/client-setup.md +124 -12
  27. data/docs/configuration/index.md +9 -5
  28. data/docs/index.md +13 -6
  29. data/docs/installation.md +2 -2
  30. data/docs/security.md +44 -5
  31. data/docs/smart-on-fhir/dynamic-client-registration/response.md +11 -2
  32. data/docs/troubleshooting/client-errors.md +103 -2
  33. data/docs/troubleshooting/index.md +60 -4
  34. data/docs/udap/dynamic-client-registration/index.md +198 -0
  35. data/docs/udap/dynamic-client-registration/lifecycle.md +115 -0
  36. data/docs/udap/dynamic-client-registration/registration-metadata.md +133 -0
  37. data/docs/udap/dynamic-client-registration/software-statement.md +115 -0
  38. data/docs/udap.md +186 -64
  39. data/gemfiles/activesupport_71.gemfile +30 -0
  40. data/gemfiles/activesupport_71.gemfile.lock +291 -0
  41. data/lib/safire/client.rb +112 -44
  42. data/lib/safire/client_config.rb +110 -15
  43. data/lib/safire/client_config_builder.rb +18 -0
  44. data/lib/safire/errors.rb +13 -8
  45. data/lib/safire/http_client.rb +7 -2
  46. data/lib/safire/middleware/https_only_redirects.rb +10 -4
  47. data/lib/safire/protocols/behaviours.rb +12 -1
  48. data/lib/safire/protocols/oauth_response_handling.rb +48 -0
  49. data/lib/safire/protocols/smart.rb +7 -28
  50. data/lib/safire/protocols/udap.rb +534 -0
  51. data/lib/safire/protocols/udap_metadata.rb +429 -0
  52. data/lib/safire/protocols/udap_registration_metadata.rb +368 -0
  53. data/lib/safire/protocols/udap_signed_metadata_validator.rb +349 -0
  54. data/lib/safire/protocols/udap_software_statement.rb +348 -0
  55. data/lib/safire/protocols.rb +6 -0
  56. data/lib/safire/uri_validation.rb +75 -7
  57. data/lib/safire/version.rb +1 -1
  58. data/lib/safire.rb +1 -0
  59. data/safire.gemspec +7 -8
  60. metadata +35 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2907e363c56d115aedcc6b5bb0aa55a1780757a7a9fc8dfc3f8853262c4c926
4
- data.tar.gz: 2cf29245a45cb4ad067418a1a23ad2c3bcddac91c3bb672a0b2b6901391af761
3
+ metadata.gz: dc7c5bf60b5d43365009f5073aae66d535f8e5345825f8ca08b638fb26587417
4
+ data.tar.gz: f47a4a2c4a84eefa8427e05a59d9e5e2cfca4adbce2a989f43cd53ead47e42bf
5
5
  SHA512:
6
- metadata.gz: 814cf46717388979bd1a5e7a3666a30fcd1cdc19fd6eb4f71e30a0916d91993104bdf94566736390e3ec39f320eef1ec1bd1621afcacfc8c89b9de376522d038
7
- data.tar.gz: 15dbfb7e5927ebbf91ae9aad6e2658f7f4cc9e42a6e0282267844c063e4693651a6c7348b6e71f24dfc5d75d525b5b0a3a0339e3888d9410c6c15babbbdf8944
6
+ metadata.gz: 625c96ea6424f41590d5396fb9d455ec0d89477474f484cbda236ec55ef13aed76d756ce88e014f76c1556ee8fa8e6e9d92a5a37743af2279e30c59456d74a58
7
+ data.tar.gz: 8c572a8974dfabdad87fd9d672013ca9bf64276a604db1492d11d6ad19e0351b30ecc2485b9262a366e3fce21af8eff656e2d0060077295c1b1127d6315c5ae2
@@ -76,7 +76,7 @@ Stage `CHANGELOG.md` only — no other files.
76
76
 
77
77
  ---
78
78
 
79
- ## Phase 5: Bump Version and Update Gemfile.lock (release commit)
79
+ ## Phase 5: Bump Version, Update Gemfile.lock and ROADMAP (release commit)
80
80
 
81
81
  Show the user the exact change to `lib/safire/version.rb`:
82
82
  ```ruby
@@ -85,13 +85,15 @@ VERSION = 'X.Y.Z'.freeze
85
85
 
86
86
  Wait for approval, then edit the file and run `bundle install` to regenerate `Gemfile.lock`.
87
87
 
88
+ Update the `Latest Published Release` in the Roadmap.md file
89
+
88
90
  Ask the user to approve this commit:
89
91
  ```
90
- git add lib/safire/version.rb Gemfile.lock
92
+ git add lib/safire/version.rb Gemfile.lock ROADMAP.md
91
93
  git commit -s -m "Bump version to X.Y.Z"
92
94
  ```
93
95
 
94
- Stage `lib/safire/version.rb` and `Gemfile.lock` only — no other files.
96
+ Stage `lib/safire/version.rb`, `Gemfile.lock`, and `ROADMAP.md` only — no other files.
95
97
 
96
98
  ---
97
99
 
@@ -145,7 +147,7 @@ After the PR is created, tell the user the remaining manual steps:
145
147
  ## Rules (never violate)
146
148
 
147
149
  - All commits use `-s`; subjects are one-line only
148
- - Two-commit structure on the release branch: docs commit (CHANGELOG) then release commit (version.rb + Gemfile.lock)
150
+ - Two-commit structure on the release branch: docs commit (CHANGELOG) then release commit (version.rb + Gemfile.lock + ROADMAP.md)
149
151
  - Never commit a `.gem` file
150
152
  - Separate doc changes from code changes into distinct commits
151
153
  - Always get explicit user approval before modifying files or running commands
data/.rubocop.yml CHANGED
@@ -2,7 +2,7 @@ plugins:
2
2
  - rubocop-rspec
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 4.0
5
+ TargetRubyVersion: 3.2
6
6
  NewCops: enable
7
7
  Exclude:
8
8
  - 'vendor/**/*'
@@ -32,6 +32,10 @@ Metrics/BlockLength:
32
32
  - 'spec/**/*'
33
33
  - '*.gemspec'
34
34
 
35
+ Metrics/AbcSize:
36
+ Exclude:
37
+ - 'spec/**/*'
38
+
35
39
  Metrics/ParameterLists:
36
40
  CountKeywordArgs: false
37
41
 
@@ -51,6 +55,12 @@ RSpec/MultipleMemoizedHelpers:
51
55
  RSpec/NestedGroups:
52
56
  Enabled: false
53
57
 
58
+ RSpec/SpecFilePathFormat:
59
+ CustomTransform:
60
+ OAuthResponseHandling: oauth_response_handling
61
+ RuboCop: rubocop
62
+ RSpec: rspec
63
+
54
64
  # Integration tests describe workflows, not classes
55
65
  RSpec/DescribeClass:
56
66
  Exclude:
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 4.0.2
1
+ ruby 4.0.6
data/CHANGELOG.md CHANGED
@@ -1,12 +1,85 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
3
+ All notable changes to the packaged Safire gem will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-08-07
11
+
12
+ ### Added
13
+
14
+ - `Safire::Client#register_client` and `Safire::Client#cancel_registration` now
15
+ support the UDAP Security STU2 Dynamic Client Registration lifecycle when
16
+ initialized with `protocol: :udap`. Safire performs discovery-bound
17
+ registration against authoritative signed endpoints, checks structural DCR
18
+ capability, posts the fixed UDAP envelope with optional certification or
19
+ endorsement JWTs, accepts new-registration `201` and update-style `200`
20
+ responses with a valid `client_id`, and confirms cancellation through a
21
+ successful response containing a valid `client_id` and an empty `grant_types`
22
+ array. OAuth-style failures preserve UDAP error codes such as
23
+ `invalid_software_statement` and `unapproved_software_statement`.
24
+ - `Safire::Protocols::UdapRegistrationMetadata` validates and normalizes
25
+ caller-controlled UDAP Security STU2 registration and cancellation metadata
26
+ before software-statement signing. It enforces exact grant shapes, HTTPS
27
+ redirect and logo URIs, required `mailto:` contact data, protocol-owned
28
+ fields, JSON-compatible extensions, and immutable canonical output. An
29
+ explicit `allow_insecure_localhost: true` option permits development-only
30
+ HTTP loopback URIs without allowing remote HTTP. Registration software
31
+ statements use minimal `alg`/`x5c` headers, exact `iss`/`sub`/`aud` claims, a
32
+ five-minute lifetime, fresh `jti`, key-compatible algorithm negotiation, and
33
+ local certificate/key/SAN checks. `ClientConfig` accepts and masks the
34
+ non-empty, leaf-first `certificate_chain` of PEM strings or
35
+ `OpenSSL::X509::Certificate` instances required for UDAP registration.
36
+ - UDAP Security STU2 discovery is now available with
37
+ `Safire::Client.new(..., protocol: :udap).server_metadata`. Safire fetches
38
+ `/.well-known/udap`, supports community-scoped discovery via `community:`, accepts
39
+ `trusted_anchors:`, `crls:`, `revocation_checker:`, and `verify_chain:` for
40
+ signed metadata trust validation (`verify_chain: false` is for development/test
41
+ only), parses metadata into `Safire::Protocols::UdapMetadata`, and raises
42
+ `DiscoveryError` for HTTP errors, 204 responses, a response body that is not a
43
+ JSON object, or failed signed metadata validation.
44
+ - `Safire::Protocols::UdapMetadata` provides STU2 structural validation and helper
45
+ predicates for advertised UDAP profiles and capabilities.
46
+ - `Safire::Protocols::UdapSignedMetadataValidator` validates the `signed_metadata`
47
+ JWT per UDAP Security STU2, including RS256, `x5c`, JWT signature, certificate
48
+ chain and revocation checks, issuer/subject/time claims, `jti`, and signed
49
+ endpoint claims.
50
+ - UDAP signed endpoint claims are merged over unsigned discovery metadata after
51
+ successful validation. Cached UDAP metadata is revalidated before reuse and
52
+ refetched if the signed JWT, certificate chain, or revocation policy no longer
53
+ validates.
54
+ - `UdapMetadata#signed_metadata_valid?` allows explicit cryptographic re-validation
55
+ against caller-provided trust anchors and revocation material.
56
+ - `Safire::Errors::DiscoveryError` accepts a `label:` keyword argument (default:
57
+ `'SMART configuration'`) and exposes it as a readable attribute so callers can
58
+ identify which protocol's discovery failed.
59
+
60
+ ### Breaking Changes
61
+
62
+ - SMART and shared HTTP URI handling now require an explicit
63
+ `allow_insecure_localhost: true` opt-in before accepting HTTP loopback URIs
64
+ or redirects. This aligns SMART local-development behavior with UDAP DCR
65
+ metadata validation while keeping production defaults HTTPS-only.
66
+
67
+ ### Changed
68
+
69
+ - SMART Dynamic Client Registration now requires successful RFC 7591 responses
70
+ to contain a non-blank string `client_id`. Malformed identifiers that were
71
+ previously accepted now raise `Safire::Errors::RegistrationError`; valid
72
+ registration responses are unchanged.
73
+ - Ruby requirement relaxed from `>= 4.0.4` to `>= 3.2` to support Rails 7.1+ apps still
74
+ running on Ruby 3.x. The minimum is 3.2 because the gem uses anonymous keyword splat
75
+ forwarding (`**` without a name), which was introduced in Ruby 3.2.
76
+ - ActiveSupport requirement relaxed from `~> 8.0.0` to `>= 7.1, < 9`, resolving the
77
+ bundler conflict that prevented the gem from being used in Rails 8.1 apps or any app
78
+ pinning ActiveSupport 8.1.x.
79
+ - `Safire::Client` now raises `ConfigurationError` when `client_type:` is passed explicitly for
80
+ `protocol: :udap`, both at construction and via `client_type=`; previously the value was
81
+ ignored silently.
82
+
10
83
  ## [0.3.0] - 2026-04-15
11
84
 
12
85
  ### Added
data/CONTRIBUTION.md CHANGED
@@ -20,6 +20,9 @@ This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participatin
20
20
  ## Getting Started
21
21
 
22
22
  1. Fork the repository on GitHub
23
+
24
+ Note: Codex automated review currently runs only for same-repository branches because it requires repository secrets and reviews untrusted PR content. Fork-based PRs are still welcome, but maintainers will review them manually.
25
+
23
26
  2. Clone your fork locally:
24
27
  ```bash
25
28
  git clone https://github.com/YOUR_USERNAME/safire.git
@@ -34,7 +37,7 @@ This project adheres to a [Code of Conduct](CODE_OF_CONDUCT.md). By participatin
34
37
 
35
38
  ### Requirements
36
39
 
37
- - Ruby 4.0.2 or later
40
+ - Ruby 3.2 or later
38
41
  - Bundler
39
42
 
40
43
  ### Setup
@@ -231,6 +234,8 @@ end
231
234
 
232
235
  2. Open a Pull Request against `main`
233
236
 
237
+ Codex automated review only runs for same-repository branches. Pull requests from forks will receive maintainer review instead.
238
+
234
239
  3. Fill out the PR template with:
235
240
  - Summary of changes
236
241
  - Related issues
data/Gemfile CHANGED
@@ -1,7 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- ruby '4.0.2'
4
-
5
3
  gemspec
6
4
 
7
5
  group :development, :test do
@@ -23,5 +21,6 @@ group :test do
23
21
  gem 'dotenv', '~> 3.0'
24
22
  gem 'simplecov', require: false
25
23
  gem 'simplecov-cobertura', require: false
24
+ gem 'sinatra', '~> 4.0'
26
25
  gem 'webmock', '~> 3.18'
27
26
  end
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- safire (0.3.0)
5
- activesupport (~> 8.0.0)
4
+ safire (0.4.0)
5
+ activesupport (>= 7.1, < 9)
6
6
  addressable (~> 2.8)
7
7
  faraday (~> 2.14)
8
8
  faraday-follow_redirects (~> 0.4)
@@ -11,7 +11,7 @@ PATH
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
- activesupport (8.0.2.1)
14
+ activesupport (8.0.5)
15
15
  base64
16
16
  benchmark (>= 0.3)
17
17
  bigdecimal
@@ -24,20 +24,20 @@ GEM
24
24
  securerandom (>= 0.3)
25
25
  tzinfo (~> 2.0, >= 2.0.5)
26
26
  uri (>= 0.13.1)
27
- addressable (2.8.9)
27
+ addressable (2.9.0)
28
28
  public_suffix (>= 2.0.2, < 8.0)
29
29
  ast (2.4.3)
30
30
  base64 (0.3.0)
31
- benchmark (0.4.1)
32
- bigdecimal (3.3.1)
31
+ benchmark (0.5.0)
32
+ bigdecimal (4.1.2)
33
33
  bundler-audit (0.9.3)
34
34
  bundler (>= 1.2.0)
35
35
  thor (~> 1.0)
36
36
  byebug (13.0.0)
37
37
  reline (>= 0.6.0)
38
38
  coderay (1.1.3)
39
- concurrent-ruby (1.3.5)
40
- connection_pool (2.5.4)
39
+ concurrent-ruby (1.3.7)
40
+ connection_pool (3.0.2)
41
41
  crack (1.0.1)
42
42
  bigdecimal
43
43
  rexml
@@ -46,39 +46,40 @@ GEM
46
46
  irb (~> 1.10)
47
47
  reline (>= 0.3.8)
48
48
  diff-lcs (1.6.2)
49
- docile (1.4.1)
50
49
  dotenv (3.2.0)
51
50
  drb (2.2.3)
52
- erb (5.0.3)
53
- faraday (2.14.1)
51
+ erb (6.0.4)
52
+ faraday (2.14.3)
54
53
  faraday-net_http (>= 2.0, < 3.5)
55
54
  json
56
55
  logger
57
56
  faraday-follow_redirects (0.5.0)
58
57
  faraday (>= 1, < 3)
59
- faraday-net_http (3.4.2)
58
+ faraday-net_http (3.4.4)
60
59
  net-http (~> 0.5)
61
60
  hashdiff (1.2.1)
62
- i18n (1.14.7)
61
+ i18n (1.14.8)
63
62
  concurrent-ruby (~> 1.0)
64
63
  io-console (0.8.2)
65
64
  irb (1.15.3)
66
65
  pp (>= 0.6.0)
67
66
  rdoc (>= 4.0.0)
68
67
  reline (>= 0.4.2)
69
- json (2.19.3)
70
- jwt (2.10.2)
68
+ json (2.21.2)
69
+ jwt (2.10.3)
71
70
  base64
72
- language_server-protocol (3.17.0.5)
71
+ language_server-protocol (3.17.0.6)
73
72
  lint_roller (1.1.0)
74
73
  logger (1.7.0)
75
74
  method_source (1.1.0)
76
- minitest (6.0.1)
75
+ minitest (6.0.6)
76
+ drb (~> 2.0)
77
77
  prism (~> 1.5)
78
+ mustermann (3.1.1)
78
79
  net-http (0.9.1)
79
80
  uri (>= 0.11.1)
80
- parallel (1.27.0)
81
- parser (3.3.11.1)
81
+ parallel (1.28.0)
82
+ parser (3.3.12.0)
82
83
  ast (~> 2.4.1)
83
84
  racc
84
85
  pp (0.6.3)
@@ -95,13 +96,21 @@ GEM
95
96
  psych (5.2.6)
96
97
  date
97
98
  stringio
98
- public_suffix (6.0.2)
99
+ public_suffix (7.0.5)
99
100
  racc (1.8.1)
101
+ rack (3.2.6)
102
+ rack-protection (4.2.1)
103
+ base64 (>= 0.1.0)
104
+ logger (>= 1.6.0)
105
+ rack (>= 3.0.0, < 4)
106
+ rack-session (2.1.2)
107
+ base64 (>= 0.1.0)
108
+ rack (>= 3.0.0)
100
109
  rainbow (3.1.1)
101
110
  rdoc (6.14.2)
102
111
  erb
103
112
  psych (>= 4.0.0)
104
- regexp_parser (2.11.3)
113
+ regexp_parser (2.12.0)
105
114
  reline (0.6.3)
106
115
  io-console (~> 0.5)
107
116
  rexml (3.4.4)
@@ -118,7 +127,7 @@ GEM
118
127
  diff-lcs (>= 1.2.0, < 2.0)
119
128
  rspec-support (~> 3.13.0)
120
129
  rspec-support (3.13.6)
121
- rubocop (1.86.1)
130
+ rubocop (1.89.0)
122
131
  json (~> 2.3)
123
132
  language_server-protocol (~> 3.17.0.2)
124
133
  lint_roller (~> 1.1.0)
@@ -129,7 +138,7 @@ GEM
129
138
  rubocop-ast (>= 1.49.0, < 2.0)
130
139
  ruby-progressbar (~> 1.7)
131
140
  unicode-display_width (>= 2.4.0, < 4.0)
132
- rubocop-ast (1.49.1)
141
+ rubocop-ast (1.50.0)
133
142
  parser (>= 3.3.7.2)
134
143
  prism (~> 1.7)
135
144
  rubocop-rspec (3.9.0)
@@ -137,17 +146,20 @@ GEM
137
146
  rubocop (~> 1.81)
138
147
  ruby-progressbar (1.13.0)
139
148
  securerandom (0.4.1)
140
- simplecov (0.22.0)
141
- docile (~> 1.1)
142
- simplecov-html (~> 0.11)
143
- simplecov_json_formatter (~> 0.1)
144
- simplecov-cobertura (3.1.0)
149
+ simplecov (1.0.3)
150
+ simplecov-cobertura (4.0.0)
145
151
  rexml
146
- simplecov (~> 0.19)
147
- simplecov-html (0.13.2)
148
- simplecov_json_formatter (0.1.4)
152
+ simplecov (~> 1.0)
153
+ sinatra (4.2.1)
154
+ logger (>= 1.6.0)
155
+ mustermann (~> 3.0)
156
+ rack (>= 3.0.0, < 4)
157
+ rack-protection (= 4.2.1)
158
+ rack-session (>= 2.0.0, < 3)
159
+ tilt (~> 2.0)
149
160
  stringio (3.1.9)
150
161
  thor (1.4.0)
162
+ tilt (2.7.0)
151
163
  timecop (0.9.11)
152
164
  tzinfo (2.0.6)
153
165
  concurrent-ruby (~> 1.0)
@@ -159,7 +171,7 @@ GEM
159
171
  addressable (>= 2.8.0)
160
172
  crack (>= 0.3.2)
161
173
  hashdiff (>= 0.4.0, < 2.0.0)
162
- yard (0.9.41)
174
+ yard (0.9.45)
163
175
 
164
176
  PLATFORMS
165
177
  ruby
@@ -177,12 +189,10 @@ DEPENDENCIES
177
189
  safire!
178
190
  simplecov
179
191
  simplecov-cobertura
192
+ sinatra (~> 4.0)
180
193
  timecop (~> 0.9)
181
194
  webmock (~> 3.18)
182
195
  yard (~> 0.9)
183
196
 
184
- RUBY VERSION
185
- ruby 4.0.2p0
186
-
187
197
  BUNDLED WITH
188
198
  2.7.1
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Coverage](https://codecov.io/gh/vanessuniq/safire/branch/main/graph/badge.svg)](https://codecov.io/gh/vanessuniq/safire)
6
6
  [![Documentation](https://img.shields.io/badge/docs-yard-blue.svg)](https://vanessuniq.github.io/safire)
7
7
 
8
- Safire is a Ruby gem implementing the [SMART App Launch 2.2.0](https://hl7.org/fhir/smart-app-launch/) specification and the [UDAP Security](https://hl7.org/fhir/us/udap-security/) protocol for healthcare client applications. It handles OAuth 2.0 authorization against HL7 FHIR servers, covering PKCE, private key JWT assertions, and the Backend Services system-to-system flow, so you can focus on your application rather than protocol plumbing.
8
+ Safire is a Ruby gem for healthcare client applications that implements [SMART App Launch 2.2.0](https://hl7.org/fhir/smart-app-launch/) and [UDAP Security STU2 / v2.0.0](https://hl7.org/fhir/us/udap-security/STU2/index.html) discovery plus certificate-backed Dynamic Client Registration. It handles SMART OAuth 2.0 authorization against HL7 FHIR servers, covering PKCE, private key JWT assertions, and the Backend Services system-to-system flow, so you can focus on your application rather than protocol plumbing.
9
9
 
10
10
  ---
11
11
 
@@ -21,15 +21,69 @@ Safire is a Ruby gem implementing the [SMART App Launch 2.2.0](https://hl7.org/f
21
21
  - POST-Based Authorization
22
22
  - Backend Services (`client_credentials` grant, JWT assertion, no user interaction or PKCE; scope defaults to `system/*.rs`)
23
23
 
24
- ### UDAP
24
+ ### UDAP Security (STU2)
25
25
 
26
- > Planned. See [ROADMAP.md](https://github.com/vanessuniq/safire/blob/main/ROADMAP.md) for details.
26
+ Server metadata discovery is implemented. Pass `protocol: :udap` to fetch `/.well-known/udap`:
27
+
28
+ ```ruby
29
+ client = Safire::Client.new(
30
+ { base_url: 'https://fhir.example.com' },
31
+ protocol: :udap
32
+ )
33
+
34
+ metadata = client.server_metadata(verify_chain: false) # development/test only
35
+ # => #<Safire::Protocols::UdapMetadata ...>
36
+
37
+ # Community-scoped discovery
38
+ metadata = client.server_metadata(community: 'https://udap.example.org/community1', verify_chain: false)
39
+ ```
40
+
41
+ Production UDAP discovery requires trust anchors plus an explicit certificate revocation policy
42
+ (`crls:` or `revocation_checker:`). Use `verify_chain: false` only for development or tests.
43
+
44
+ Dynamic Client Registration is implemented for certificate-backed UDAP clients:
45
+
46
+ ```ruby
47
+ udap_client = Safire::Client.new(
48
+ {
49
+ base_url: 'https://fhir.example.com',
50
+ private_key: File.read('client-key.pem'),
51
+ certificate_chain: [File.read('client-cert.pem')]
52
+ },
53
+ protocol: :udap
54
+ )
55
+
56
+ registration = udap_client.register_client(
57
+ {
58
+ client_name: 'Example Backend Service',
59
+ contacts: ['mailto:security@example.com'],
60
+ grant_types: ['client_credentials'],
61
+ scope: 'system/Patient.rs'
62
+ },
63
+ client_uri: 'https://client.example.com',
64
+ trusted_anchors: [ca_cert],
65
+ crls: [ca_crl]
66
+ )
67
+
68
+ cancellation = udap_client.cancel_registration(
69
+ {
70
+ client_name: 'Example Backend Service',
71
+ contacts: ['mailto:security@example.com'],
72
+ scope: 'system/Patient.rs'
73
+ },
74
+ client_uri: 'https://client.example.com',
75
+ trusted_anchors: [ca_cert],
76
+ crls: [ca_crl]
77
+ )
78
+ ```
79
+
80
+ UDAP JWT client authentication and Tiered OAuth are planned. See [ROADMAP.md](https://github.com/vanessuniq/safire/blob/main/ROADMAP.md) for details.
27
81
 
28
82
  ---
29
83
 
30
84
  ## Installation
31
85
 
32
- Requires Ruby ≥ 4.0.2.
86
+ Requires Ruby ≥ 3.2.
33
87
 
34
88
  ```ruby
35
89
  gem 'safire'
@@ -155,7 +209,7 @@ bin/demo
155
209
  # Visit http://localhost:4567
156
210
  ```
157
211
 
158
- Demonstrates Dynamic Client Registration, SMART discovery, all authorization flows, token refresh, and backend services token requests. See [`examples/sinatra_app/README.md`](examples/sinatra_app/README.md) for details.
212
+ Demonstrates SMART Dynamic Client Registration, SMART discovery, UDAP discovery with `signed_metadata` trust validation, UDAP Dynamic Client Registration and cancellation, all authorization flows, token refresh, and backend services token requests. See [`examples/sinatra_app/README.md`](examples/sinatra_app/README.md) for details.
159
213
 
160
214
  ---
161
215
 
data/ROADMAP.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # Safire Roadmap
2
2
 
3
- ## Current Release — v0.2.0
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
  ---
@@ -22,20 +25,69 @@ Feedback, bug reports, and pull requests are welcome via the [issue tracker](htt
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
23
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
24
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
51
+
25
52
  ---
26
53
 
27
54
  ## Planned Features
28
55
 
29
56
  ### UDAP Security
30
57
 
31
- - **UDAP Discovery** — `/.well-known/udap` metadata fetch and validation
32
- - **UDAP Dynamic Client Registration** — signed software statements and client registration
33
58
  - **UDAP JWT Client Auth** — B2B and consumer-facing authorization flows
34
59
  - **Tiered OAuth** — identity chaining for multi-system access
35
60
 
36
61
  ### Quality and Compliance
37
62
 
38
- - **Inferno SMART App Launch STU2.2 Test Suite** — full passing run against the [Inferno Framework](https://inferno-framework.github.io/)
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.
39
91
 
40
92
  ---
41
93
 
@@ -43,9 +95,9 @@ Feedback, bug reports, and pull requests are welcome via the [issue tracker](htt
43
95
 
44
96
  | Component | Version |
45
97
  |-----------|---------|
46
- | Ruby | ≥ 4.0.2 |
47
- | ActiveSupport | ~> 8.0 |
48
- | Rails (optional) | 7.x, 8.x |
98
+ | Ruby | ≥ 3.2 |
99
+ | ActiveSupport | 7.1, < 9 |
100
+ | Rails (optional) | 7.1 |
49
101
  | SMART App Launch | 2.2.0 (STU2) |
50
- | UDAP Security | 1.0 (planned) |
102
+ | UDAP Security | 2.0.0 (STU2 discovery and Dynamic Client Registration lifecycle implemented; auth flows planned) |
51
103
  | FHIR | R4, R4B |