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