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.
- checksums.yaml +4 -4
- data/.claude/skills/release-safire/SKILL.md +6 -4
- data/.rubocop.yml +11 -1
- data/.tool-versions +1 -1
- data/CHANGELOG.md +74 -1
- data/CONTRIBUTION.md +6 -1
- data/Gemfile +1 -2
- data/Gemfile.lock +45 -35
- data/README.md +59 -5
- data/ROADMAP.md +60 -8
- 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 +51 -0
- data/docs/adr/ADR-010-optional-client-id-dcr-temp-client.md +31 -1
- 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 +7 -3
- data/docs/configuration/client-setup.md +124 -12
- data/docs/configuration/index.md +9 -5
- data/docs/index.md +13 -6
- data/docs/installation.md +2 -2
- data/docs/security.md +44 -5
- data/docs/smart-on-fhir/dynamic-client-registration/response.md +11 -2
- data/docs/troubleshooting/client-errors.md +103 -2
- data/docs/troubleshooting/index.md +60 -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 +186 -64
- data/gemfiles/activesupport_71.gemfile +30 -0
- data/gemfiles/activesupport_71.gemfile.lock +291 -0
- data/lib/safire/client.rb +112 -44
- data/lib/safire/client_config.rb +110 -15
- data/lib/safire/client_config_builder.rb +18 -0
- data/lib/safire/errors.rb +13 -8
- 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 +7 -28
- 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 +75 -7
- data/lib/safire/version.rb +1 -1
- data/lib/safire.rb +1 -0
- data/safire.gemspec +7 -8
- metadata +35 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc7c5bf60b5d43365009f5073aae66d535f8e5345825f8ca08b638fb26587417
|
|
4
|
+
data.tar.gz: f47a4a2c4a84eefa8427e05a59d9e5e2cfca4adbce2a989f43cd53ead47e42bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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 `
|
|
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:
|
|
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.
|
|
1
|
+
ruby 4.0.6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
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
|
|
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.
|
|
5
|
-
activesupport (
|
|
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.
|
|
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.
|
|
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.
|
|
32
|
-
bigdecimal (
|
|
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.
|
|
40
|
-
connection_pool (
|
|
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 (
|
|
53
|
-
faraday (2.14.
|
|
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.
|
|
58
|
+
faraday-net_http (3.4.4)
|
|
60
59
|
net-http (~> 0.5)
|
|
61
60
|
hashdiff (1.2.1)
|
|
62
|
-
i18n (1.14.
|
|
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.
|
|
70
|
-
jwt (2.10.
|
|
68
|
+
json (2.21.2)
|
|
69
|
+
jwt (2.10.3)
|
|
71
70
|
base64
|
|
72
|
-
language_server-protocol (3.17.0.
|
|
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.
|
|
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.
|
|
81
|
-
parser (3.3.
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
141
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
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
|
[](https://codecov.io/gh/vanessuniq/safire)
|
|
6
6
|
[](https://vanessuniq.github.io/safire)
|
|
7
7
|
|
|
8
|
-
Safire is a Ruby gem
|
|
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
|
-
|
|
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 ≥
|
|
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
|
-
##
|
|
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
|
|
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 | ≥
|
|
47
|
-
| ActiveSupport |
|
|
48
|
-
| Rails (optional) | 7.
|
|
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 |
|
|
102
|
+
| UDAP Security | 2.0.0 (STU2 discovery and Dynamic Client Registration lifecycle implemented; auth flows planned) |
|
|
51
103
|
| FHIR | R4, R4B |
|