safire 0.1.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 (72) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +62 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +35 -0
  6. data/CODE_OF_CONDUCT.md +17 -0
  7. data/CONTRIBUTION.md +283 -0
  8. data/Gemfile +26 -0
  9. data/Gemfile.lock +186 -0
  10. data/LICENSE +201 -0
  11. data/README.md +159 -0
  12. data/ROADMAP.md +54 -0
  13. data/Rakefile +26 -0
  14. data/docs/.gitignore +5 -0
  15. data/docs/404.html +25 -0
  16. data/docs/Gemfile +37 -0
  17. data/docs/Gemfile.lock +195 -0
  18. data/docs/_config.yml +103 -0
  19. data/docs/_includes/footer_custom.html +6 -0
  20. data/docs/_includes/head_custom.html +14 -0
  21. data/docs/_sass/custom/custom.scss +108 -0
  22. data/docs/adr/ADR-001-activesupport-dependency.md +50 -0
  23. data/docs/adr/ADR-002-facade-and-forwardable.md +79 -0
  24. data/docs/adr/ADR-003-protocol-vs-client-type.md +67 -0
  25. data/docs/adr/ADR-004-clientconfig-immutability-and-entity-masking.md +59 -0
  26. data/docs/adr/ADR-005-per-client-http-ownership.md +58 -0
  27. data/docs/adr/ADR-006-lazy-discovery.md +83 -0
  28. data/docs/adr/ADR-007-https-only-redirects-and-localhost-exception.md +59 -0
  29. data/docs/adr/ADR-008-warn-return-false-for-compliance-validation.md +74 -0
  30. data/docs/adr/index.md +22 -0
  31. data/docs/advanced.md +284 -0
  32. data/docs/configuration/client-setup.md +158 -0
  33. data/docs/configuration/index.md +60 -0
  34. data/docs/configuration/logging.md +86 -0
  35. data/docs/index.md +64 -0
  36. data/docs/installation.md +96 -0
  37. data/docs/security.md +256 -0
  38. data/docs/smart-on-fhir/confidential-asymmetric/authorization.md +72 -0
  39. data/docs/smart-on-fhir/confidential-asymmetric/index.md +162 -0
  40. data/docs/smart-on-fhir/confidential-asymmetric/token-exchange.md +250 -0
  41. data/docs/smart-on-fhir/confidential-symmetric/authorization.md +75 -0
  42. data/docs/smart-on-fhir/confidential-symmetric/index.md +69 -0
  43. data/docs/smart-on-fhir/confidential-symmetric/token-exchange.md +215 -0
  44. data/docs/smart-on-fhir/discovery/capability-checks.md +142 -0
  45. data/docs/smart-on-fhir/discovery/index.md +96 -0
  46. data/docs/smart-on-fhir/discovery/metadata.md +147 -0
  47. data/docs/smart-on-fhir/index.md +72 -0
  48. data/docs/smart-on-fhir/post-based-authorization.md +190 -0
  49. data/docs/smart-on-fhir/public-client/authorization.md +112 -0
  50. data/docs/smart-on-fhir/public-client/index.md +80 -0
  51. data/docs/smart-on-fhir/public-client/token-exchange.md +249 -0
  52. data/docs/troubleshooting/auth-errors.md +124 -0
  53. data/docs/troubleshooting/client-errors.md +130 -0
  54. data/docs/troubleshooting/index.md +99 -0
  55. data/docs/troubleshooting/token-errors.md +99 -0
  56. data/docs/udap.md +78 -0
  57. data/lib/safire/client.rb +195 -0
  58. data/lib/safire/client_config.rb +169 -0
  59. data/lib/safire/client_config_builder.rb +72 -0
  60. data/lib/safire/entity.rb +26 -0
  61. data/lib/safire/errors.rb +247 -0
  62. data/lib/safire/http_client.rb +87 -0
  63. data/lib/safire/jwt_assertion.rb +237 -0
  64. data/lib/safire/middleware/https_only_redirects.rb +39 -0
  65. data/lib/safire/pkce.rb +39 -0
  66. data/lib/safire/protocols/behaviours.rb +54 -0
  67. data/lib/safire/protocols/smart.rb +378 -0
  68. data/lib/safire/protocols/smart_metadata.rb +231 -0
  69. data/lib/safire/version.rb +4 -0
  70. data/lib/safire.rb +54 -0
  71. data/safire.gemspec +36 -0
  72. metadata +184 -0
@@ -0,0 +1,142 @@
1
+ ---
2
+ layout: default
3
+ title: Capability Checks and Client Selection
4
+ parent: SMART Discovery
5
+ grand_parent: SMART on FHIR
6
+ nav_order: 2
7
+ ---
8
+
9
+ # Capability Checks and Client Selection
10
+
11
+ {: .no_toc }
12
+
13
+ ## Table of contents
14
+ {: .no_toc .text-delta }
15
+
16
+ 1. TOC
17
+ {:toc}
18
+
19
+ ---
20
+
21
+ ## Full Checks (`supports_*?`)
22
+
23
+ These methods verify both the capability flag **and** any required associated fields. Use them to confirm the server is fully ready for a given mode.
24
+
25
+ ```ruby
26
+ # Launch modes — requires capability flag AND authorization_endpoint present
27
+ metadata.supports_ehr_launch? # launch-ehr + authorization_endpoint
28
+ metadata.supports_standalone_launch? # launch-standalone + authorization_endpoint
29
+
30
+ # Public clients
31
+ metadata.supports_public_auth?
32
+ # => true if capabilities include "client-public"
33
+
34
+ # Confidential symmetric — checks capability AND auth method compatibility
35
+ metadata.supports_symmetric_auth?
36
+ # => true if:
37
+ # capabilities include "client-confidential-symmetric"
38
+ # AND (token_endpoint_auth_methods_supported is blank
39
+ # OR includes "client_secret_basic")
40
+
41
+ # Confidential asymmetric — checks capability, auth method, AND algorithm support
42
+ metadata.supports_asymmetric_auth?
43
+ # => true if:
44
+ # capabilities include "client-confidential-asymmetric"
45
+ # AND (token_endpoint_auth_methods_supported is blank
46
+ # OR includes "private_key_jwt")
47
+ # AND asymmetric_signing_algorithms_supported.any?
48
+
49
+ # OpenID Connect — requires capability flag, issuer, AND jwks_uri present
50
+ metadata.supports_openid_connect?
51
+
52
+ # POST-based authorization
53
+ metadata.supports_post_based_authorization?
54
+ # => true if capabilities include "authorize-post"
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Flag-Only Checks (`*_capability?`)
60
+
61
+ These check only the capability string — they do **not** verify that required fields are present. Use them for lightweight checks or when you plan to validate fields separately.
62
+
63
+ ```ruby
64
+ metadata.ehr_launch_capability? # capabilities include "launch-ehr"
65
+ metadata.standalone_launch_capability? # capabilities include "launch-standalone"
66
+ metadata.openid_connect_capability? # capabilities include "sso-openid-connect"
67
+ ```
68
+
69
+ {: .important }
70
+ > **`supports_*?` vs `*_capability?`** — `supports_ehr_launch?` returns `false` if `authorization_endpoint` is missing even when the capability flag is set. `ehr_launch_capability?` returns `true` based on the flag alone. Prefer `supports_*?` unless you have a specific reason to check the flag independently.
71
+
72
+ ---
73
+
74
+ ## Asymmetric Signing Algorithms
75
+
76
+ ```ruby
77
+ metadata.asymmetric_signing_algorithms_supported
78
+ # Returns the intersection of server-advertised signing algorithms and
79
+ # Safire's supported set [RS384, ES384].
80
+ # If the server does not advertise algorithms, both RS384 and ES384 are assumed.
81
+ # => ["RS384", "ES384"]
82
+ ```
83
+
84
+ This method powers `supports_asymmetric_auth?` internally and is also useful when selecting a signing algorithm explicitly for a confidential asymmetric client.
85
+
86
+ ---
87
+
88
+ ## Client Selection Based on Discovery
89
+
90
+ **Manual client type switch** — discover first, then update the client type. Already-fetched metadata is preserved; no re-discovery occurs.
91
+
92
+ ```ruby
93
+ client = Safire::Client.new(config) # defaults to :public
94
+ metadata = client.server_metadata
95
+
96
+ if metadata.supports_symmetric_auth?
97
+ client.client_type = :confidential_symmetric
98
+ end
99
+
100
+ tokens = client.request_access_token(code: code, code_verifier: verifier)
101
+ ```
102
+
103
+ **Automatic selection** based on server capabilities:
104
+
105
+ ```ruby
106
+ def configure_client_type(client)
107
+ metadata = client.server_metadata
108
+
109
+ if metadata.supports_asymmetric_auth?
110
+ client.client_type = :confidential_asymmetric
111
+ elsif metadata.supports_symmetric_auth?
112
+ client.client_type = :confidential_symmetric
113
+ elsif metadata.supports_public_auth?
114
+ client.client_type = :public
115
+ else
116
+ raise 'Server does not support any known client types'
117
+ end
118
+ end
119
+ ```
120
+
121
+ ---
122
+
123
+ ## SMART Capabilities Reference
124
+
125
+ | Capability | Description |
126
+ |------------|-------------|
127
+ | `launch-ehr` | Supports EHR-initiated launch |
128
+ | `launch-standalone` | Supports standalone launch |
129
+ | `authorize-post` | Supports POST-based authorization |
130
+ | `client-public` | Supports public clients |
131
+ | `client-confidential-symmetric` | Supports `client_secret_basic` |
132
+ | `client-confidential-asymmetric` | Supports `private_key_jwt` |
133
+ | `sso-openid-connect` | Supports OpenID Connect |
134
+ | `context-ehr-patient` | EHR launch provides patient context |
135
+ | `context-ehr-encounter` | EHR launch provides encounter context |
136
+ | `context-standalone-patient` | Standalone launch can request patient context |
137
+ | `context-standalone-encounter` | Standalone launch can request encounter context |
138
+ | `permission-offline` | Supports `offline_access` scope |
139
+ | `permission-patient` | Supports patient-level scopes |
140
+ | `permission-user` | Supports user-level scopes |
141
+ | `permission-v1` | Supports SMART v1 scopes |
142
+ | `permission-v2` | Supports SMART v2 scopes |
@@ -0,0 +1,96 @@
1
+ ---
2
+ layout: default
3
+ title: SMART Discovery
4
+ parent: SMART on FHIR
5
+ nav_order: 1
6
+ has_children: true
7
+ permalink: /smart-on-fhir/discovery/
8
+ ---
9
+
10
+ # SMART Discovery
11
+
12
+ {: .no_toc }
13
+
14
+ <div class="code-example" markdown="1">
15
+ SMART on FHIR discovery allows clients to dynamically learn about a FHIR server's authorization capabilities before initiating the OAuth flow.
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## Overview
21
+
22
+ Safire fetches server metadata from `/.well-known/smart-configuration`, appended to your `base_url`:
23
+
24
+ ```ruby
25
+ base_url = 'https://fhir.example.com/r4'
26
+ # Fetches: https://fhir.example.com/r4/.well-known/smart-configuration
27
+ ```
28
+
29
+ Trailing slashes are handled automatically. The metadata is fetched lazily on first use and cached within the client instance.
30
+
31
+ ```ruby
32
+ config = Safire::ClientConfig.new(
33
+ base_url: 'https://fhir.example.com',
34
+ client_id: 'my_client',
35
+ redirect_uri: 'https://myapp.com/callback',
36
+ scopes: ['openid', 'profile']
37
+ )
38
+
39
+ # :public is the default client_type — appropriate for discovery
40
+ client = Safire::Client.new(config)
41
+ metadata = client.server_metadata
42
+ # => #<Safire::Protocols::SmartMetadata ...>
43
+ ```
44
+
45
+ `server_metadata` returns a `Safire::Protocols::SmartMetadata` object with typed accessors for all fields. See [Metadata Fields and Validation]({% link smart-on-fhir/discovery/metadata.md %}) for the full field reference and validation rules.
46
+
47
+ ---
48
+
49
+ ## Error Handling
50
+
51
+ ```ruby
52
+ begin
53
+ metadata = client.server_metadata
54
+ rescue Safire::Errors::DiscoveryError => e
55
+ case e.message
56
+ when /404/
57
+ puts 'FHIR server does not support SMART on FHIR'
58
+ when /timeout/i
59
+ puts 'Discovery request timed out'
60
+ when /expected JSON object/
61
+ puts 'Server returned invalid SMART configuration'
62
+ else
63
+ puts "Discovery failed: #{e.message}"
64
+ end
65
+ end
66
+ ```
67
+
68
+ **Graceful fallback** — fall back to known endpoints when discovery is unavailable:
69
+
70
+ ```ruby
71
+ def discover_with_fallback(client)
72
+ metadata = client.server_metadata
73
+ {
74
+ authorization_endpoint: metadata.authorization_endpoint,
75
+ token_endpoint: metadata.token_endpoint,
76
+ source: :discovery
77
+ }
78
+ rescue Safire::Errors::DiscoveryError => e
79
+ Rails.logger.warn("Discovery failed, using fallback: #{e.message}")
80
+ {
81
+ authorization_endpoint: ENV['FALLBACK_AUTH_ENDPOINT'],
82
+ token_endpoint: ENV['FALLBACK_TOKEN_ENDPOINT'],
83
+ source: :fallback
84
+ }
85
+ end
86
+ ```
87
+
88
+ {: .note }
89
+ > Application-level metadata caching (e.g. `Rails.cache`) and multi-server registry patterns are covered in the [Advanced Examples]({{ site.baseurl }}/advanced/) guide.
90
+
91
+ ---
92
+
93
+ ## What's Next
94
+
95
+ - [Metadata Fields and Validation]({% link smart-on-fhir/discovery/metadata.md %}) — field reference, validation rules, PKCE checks
96
+ - [Capability Checks and Client Selection]({% link smart-on-fhir/discovery/capability-checks.md %}) — all capability methods and dynamic client type selection
@@ -0,0 +1,147 @@
1
+ ---
2
+ layout: default
3
+ title: Metadata Fields and Validation
4
+ parent: SMART Discovery
5
+ grand_parent: SMART on FHIR
6
+ nav_order: 1
7
+ ---
8
+
9
+ # Metadata Fields and Validation
10
+
11
+ {: .no_toc }
12
+
13
+ ## Table of contents
14
+ {: .no_toc .text-delta }
15
+
16
+ 1. TOC
17
+ {:toc}
18
+
19
+ ---
20
+
21
+ ## Field Reference
22
+
23
+ `server_metadata` returns a `Safire::Protocols::SmartMetadata` object. All fields are accessible as typed readers.
24
+
25
+ ### Always Required
26
+
27
+ | Field | Type | Description |
28
+ |-------|------|-------------|
29
+ | `token_endpoint` | String | OAuth2 token endpoint URL |
30
+ | `grant_types_supported` | Array | Supported OAuth2 grant types |
31
+ | `capabilities` | Array | SMART capabilities list |
32
+ | `code_challenge_methods_supported` | Array | Must include `"S256"`, must not include `"plain"` |
33
+
34
+ ### Conditionally Required
35
+
36
+ | Field | Type | Required when |
37
+ |-------|------|--------------|
38
+ | `authorization_endpoint` | String | capabilities include `launch-ehr` or `launch-standalone` |
39
+ | `issuer` | String | capabilities include `sso-openid-connect` |
40
+ | `jwks_uri` | String | capabilities include `sso-openid-connect` |
41
+
42
+ ### Optional
43
+
44
+ | Field | Type | Description |
45
+ |-------|------|-------------|
46
+ | `registration_endpoint` | String | Dynamic client registration URL |
47
+ | `scopes_supported` | Array | Available OAuth2 scopes |
48
+ | `response_types_supported` | Array | Supported response types |
49
+ | `token_endpoint_auth_methods_supported` | Array | Token endpoint auth methods (e.g. `client_secret_basic`, `private_key_jwt`) |
50
+ | `token_endpoint_auth_signing_alg_values_supported` | Array | JWT signing algorithms (e.g. `RS384`, `ES384`) — used by `asymmetric_signing_algorithms_supported` |
51
+ | `introspection_endpoint` | String | Token introspection URL |
52
+ | `revocation_endpoint` | String | Token revocation URL |
53
+ | `management_endpoint` | String | User access management URL |
54
+ | `associated_endpoints` | Array | Endpoints sharing this auth server |
55
+ | `user_access_brand_bundle` | String | Brand bundle URL for user-facing apps |
56
+ | `user_access_brand_identifier` | String | Primary brand identifier |
57
+
58
+ ### Example Usage
59
+
60
+ ```ruby
61
+ metadata.token_endpoint # => "https://fhir.example.com/token"
62
+ metadata.grant_types_supported # => ["authorization_code", "refresh_token"]
63
+ metadata.capabilities # => ["launch-ehr", "client-public", ...]
64
+ metadata.code_challenge_methods_supported # => ["S256"]
65
+
66
+ # Conditionally present
67
+ metadata.authorization_endpoint # => "https://fhir.example.com/authorize"
68
+ metadata.issuer # => "https://fhir.example.com"
69
+ metadata.jwks_uri # => "https://fhir.example.com/.well-known/jwks.json"
70
+
71
+ # Optional
72
+ metadata.registration_endpoint # => "https://fhir.example.com/register"
73
+ metadata.scopes_supported # => ["openid", "profile", "patient/*.read", ...]
74
+ metadata.token_endpoint_auth_methods_supported # => ["client_secret_basic", "private_key_jwt"]
75
+ metadata.token_endpoint_auth_signing_alg_values_supported # => ["RS384", "ES384"]
76
+ metadata.introspection_endpoint # => "https://fhir.example.com/introspect"
77
+ metadata.revocation_endpoint # => "https://fhir.example.com/revoke"
78
+ metadata.management_endpoint # => "https://fhir.example.com/manage"
79
+ metadata.associated_endpoints # => [{"url" => "...", "capabilities" => [...]}]
80
+ metadata.user_access_brand_bundle # => "https://fhir.example.com/brands"
81
+ metadata.user_access_brand_identifier # => "example-brand"
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Validation
87
+
88
+ `valid?` checks conformance with SMART App Launch 2.2.0 and logs a warning for each violation. It never raises — deciding whether to block on non-compliant metadata is the caller's responsibility.
89
+
90
+ ```ruby
91
+ if metadata.valid?
92
+ # All required fields present, PKCE compliant
93
+ else
94
+ # Safire has already logged warnings for each violation
95
+ raise 'Server metadata does not meet SMART App Launch 2.2.0 requirements'
96
+ end
97
+ ```
98
+
99
+ **What `valid?` checks:**
100
+ - All always-required fields are present
101
+ - Conditional fields present when their capability is advertised
102
+ - `code_challenge_methods_supported` includes `'S256'` (SHALL per SMART 2.2.0)
103
+ - `code_challenge_methods_supported` does not include `'plain'` (SHALL NOT per SMART 2.2.0)
104
+
105
+ Example warning output:
106
+
107
+ ```
108
+ WARN: SMART metadata non-compliance: required field 'authorization_endpoint' is missing
109
+ WARN: SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported (SMART App Launch 2.2.0 requires S256)
110
+ WARN: SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported (SMART App Launch 2.2.0 prohibits plain)
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Example Server Response (SMART App Launch 2.2.0)
116
+
117
+ ```json
118
+ {
119
+ "issuer": "https://fhir.example.com",
120
+ "authorization_endpoint": "https://fhir.example.com/authorize",
121
+ "token_endpoint": "https://fhir.example.com/token",
122
+ "jwks_uri": "https://fhir.example.com/.well-known/jwks.json",
123
+ "grant_types_supported": ["authorization_code", "refresh_token"],
124
+ "scopes_supported": ["openid", "profile", "launch", "patient/*.read", "offline_access"],
125
+ "response_types_supported": ["code"],
126
+ "token_endpoint_auth_methods_supported": [
127
+ "client_secret_basic",
128
+ "private_key_jwt"
129
+ ],
130
+ "token_endpoint_auth_signing_alg_values_supported": ["RS384", "ES384"],
131
+ "code_challenge_methods_supported": ["S256"],
132
+ "capabilities": [
133
+ "launch-ehr",
134
+ "launch-standalone",
135
+ "client-public",
136
+ "client-confidential-symmetric",
137
+ "client-confidential-asymmetric",
138
+ "sso-openid-connect",
139
+ "context-ehr-patient",
140
+ "context-ehr-encounter",
141
+ "context-standalone-patient",
142
+ "permission-offline",
143
+ "permission-patient",
144
+ "permission-user"
145
+ ]
146
+ }
147
+ ```
@@ -0,0 +1,72 @@
1
+ ---
2
+ layout: default
3
+ title: SMART on FHIR
4
+ nav_order: 4
5
+ has_children: true
6
+ permalink: /smart-on-fhir/
7
+ ---
8
+
9
+ # SMART on FHIR
10
+
11
+ This section provides step-by-step guides for implementing SMART on FHIR authorization flows with Safire.
12
+
13
+ ## Available Workflows
14
+
15
+ | Workflow | Description |
16
+ |----------|-------------|
17
+ | [SMART Discovery]({% link smart-on-fhir/discovery/index.md %}) | Fetching and using SMART configuration metadata |
18
+ | [Public Client]({% link smart-on-fhir/public-client/index.md %}) | Authorization flow for browser-based and mobile applications |
19
+ | [Confidential Symmetric Client]({% link smart-on-fhir/confidential-symmetric/index.md %}) | Authorization flow for server-side applications with client secrets |
20
+ | [Confidential Asymmetric Client]({% link smart-on-fhir/confidential-asymmetric/index.md %}) | Authorization flow using private_key_jwt (RSA/EC key pair) |
21
+ | [POST-Based Authorization]({% link smart-on-fhir/post-based-authorization.md %}) | Sending the authorization request as a form POST (`authorize-post` capability) |
22
+
23
+ ## Choosing a Client Type
24
+
25
+ ```
26
+ Is your application a server-side web application
27
+ that can securely store credentials?
28
+
29
+ ├── YES → Can you use asymmetric key pairs (RSA/EC)?
30
+ │ │
31
+ │ ├── YES → Confidential Asymmetric Client
32
+ │ │ (Uses private_key_jwt with signed JWT assertions)
33
+ │ │
34
+ │ └── NO → Confidential Symmetric Client
35
+ │ (Uses client_secret with HTTP Basic auth)
36
+
37
+ └── NO → Public Client
38
+ (Uses PKCE only, no client secret)
39
+ ```
40
+
41
+ ## Common Flow
42
+
43
+ All SMART authorization flows follow this general pattern. The key differences between client types are in how they authenticate during token exchange and refresh.
44
+
45
+ ```mermaid
46
+ sequenceDiagram
47
+ participant App
48
+ participant Safire
49
+ participant FHIR as FHIR Server
50
+
51
+ App->>Safire: Client.new(config)
52
+ Note over Safire: No network call yet
53
+
54
+ App->>Safire: authorization_url()
55
+ Safire->>FHIR: GET /.well-known/smart-configuration
56
+ FHIR-->>Safire: SmartMetadata (endpoints, capabilities)
57
+ Safire-->>App: { auth_url, state, code_verifier }
58
+
59
+ App->>FHIR: Redirect user to auth_url
60
+ FHIR-->>App: Callback with ?code=...&state=...
61
+
62
+ App->>Safire: request_access_token(code:, code_verifier:)
63
+ Note over Safire: Auth method varies by client_type:<br/>public → client_id in body<br/>confidential_symmetric → Basic auth header<br/>confidential_asymmetric → JWT assertion in body
64
+ Safire->>FHIR: POST /token
65
+ FHIR-->>Safire: { access_token, refresh_token, expires_in, ... }
66
+ Safire-->>App: token response Hash
67
+
68
+ App->>Safire: refresh_token(refresh_token:)
69
+ Safire->>FHIR: POST /token (grant_type=refresh_token)
70
+ FHIR-->>Safire: { access_token, ... }
71
+ Safire-->>App: new token response Hash
72
+ ```
@@ -0,0 +1,190 @@
1
+ ---
2
+ layout: default
3
+ title: POST-Based Authorization
4
+ parent: SMART on FHIR
5
+ nav_order: 5
6
+ has_toc: true
7
+ ---
8
+
9
+ # POST-Based Authorization
10
+
11
+ {: .no_toc }
12
+
13
+ <div class="code-example" markdown="1">
14
+ SMART App Launch 2.2.0 introduces the `authorize-post` capability, allowing servers to accept the authorization request as an HTTP form POST instead of a GET redirect. This page explains when and how to use it with Safire.
15
+ </div>
16
+
17
+ ## Table of contents
18
+ {: .no_toc .text-delta }
19
+
20
+ 1. TOC
21
+ {:toc}
22
+
23
+ ---
24
+
25
+ ## Overview
26
+
27
+ By default, SMART authorization uses a GET redirect — the user's browser is sent to the authorization server with all parameters in the query string. Servers that advertise the `authorize-post` capability also accept the authorization request as an HTTP POST with parameters in the request body.
28
+
29
+ POST-based authorization can be useful when:
30
+ - Authorization parameters are too large for a URL (e.g. rich `state` or `launch` values)
31
+ - The client prefers to avoid sensitive parameters appearing in browser history or server logs
32
+
33
+ ---
34
+
35
+ ## Detecting Server Support
36
+
37
+ Check whether the authorization server advertises the `authorize-post` capability before using POST mode:
38
+
39
+ ```ruby
40
+ client = Safire::Client.new(config, client_type: :public)
41
+ metadata = client.server_metadata
42
+
43
+ if metadata.supports_post_based_authorization?
44
+ puts "Server supports POST-based authorization"
45
+ end
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Generating a POST Authorization Request
51
+
52
+ Pass `method: :post` (or `method: 'post'`) to `authorization_url`:
53
+
54
+ ```ruby
55
+ auth_data = client.authorization_url(method: :post)
56
+
57
+ auth_data[:auth_url] # The bare authorization endpoint URL
58
+ auth_data[:params] # Hash of parameters to POST as the request body
59
+ auth_data[:state] # Store in session for CSRF verification
60
+ auth_data[:code_verifier] # Store in session for token exchange
61
+ ```
62
+
63
+ The `:get` method returns `auth_url` as a fully-formed URL with query parameters. The `:post` method returns the endpoint separately from the parameters so your application can submit a form POST.
64
+
65
+ ---
66
+
67
+ ## Rails Example
68
+
69
+ ```ruby
70
+ # app/controllers/smart_auth_controller.rb
71
+ def launch
72
+ # Check server support (optional but recommended)
73
+ metadata = @client.server_metadata
74
+ use_post = metadata.supports_post_based_authorization?
75
+
76
+ auth_data = @client.authorization_url(method: use_post ? :post : :get)
77
+
78
+ session[:oauth_state] = auth_data[:state]
79
+ session[:code_verifier] = auth_data[:code_verifier]
80
+
81
+ if use_post
82
+ # Render a form that auto-submits to the authorization endpoint
83
+ @auth_url = auth_data[:auth_url]
84
+ @auth_params = auth_data[:params]
85
+ render :authorize_post
86
+ else
87
+ redirect_to auth_data[:auth_url], allow_other_host: true
88
+ end
89
+ end
90
+ ```
91
+
92
+ ```erb
93
+ <%# app/views/smart_auth/authorize_post.html.erb %>
94
+ <form id="auth-form" method="POST" action="<%= @auth_url %>">
95
+ <% @auth_params.each do |key, value| %>
96
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
97
+ <% end %>
98
+ </form>
99
+
100
+ <script>
101
+ // Auto-submit after the page loads
102
+ document.getElementById('auth-form').submit();
103
+ </script>
104
+ ```
105
+
106
+ ### Response Hash Comparison
107
+
108
+ | Key | GET (`method: :get`) | POST (`method: :post`) |
109
+ |---------------|-----------------------------------------------|--------------------------------|
110
+ | `:auth_url` | Full URL with query string parameters | Bare authorization endpoint URL |
111
+ | `:state` | State value (also embedded in query string) | State value |
112
+ | `:code_verifier` | PKCE code verifier | PKCE code verifier |
113
+ | `:params` | Not present | Hash of all authorization parameters |
114
+
115
+ The callback handling (token exchange) is identical for both methods — the authorization server always redirects back to your `redirect_uri` with an authorization code.
116
+
117
+ ---
118
+
119
+ ## Sinatra Example
120
+
121
+ ```ruby
122
+ get '/launch' do
123
+ auth_data = @client.authorization_url(method: :post)
124
+
125
+ session[:state] = auth_data[:state]
126
+ session[:code_verifier] = auth_data[:code_verifier]
127
+
128
+ @auth_url = auth_data[:auth_url]
129
+ @auth_params = auth_data[:params]
130
+ erb :authorize_post
131
+ end
132
+ ```
133
+
134
+ ```erb
135
+ <%# views/authorize_post.erb %>
136
+ <form id="auth-form" method="POST" action="<%= @auth_url %>">
137
+ <% @auth_params.each do |key, value| %>
138
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
139
+ <% end %>
140
+ </form>
141
+ <script>document.getElementById('auth-form').submit();</script>
142
+ ```
143
+
144
+ ---
145
+
146
+ ## String and Symbol Forms
147
+
148
+ Both string and symbol values are accepted:
149
+
150
+ ```ruby
151
+ client.authorization_url(method: :post) # symbol
152
+ client.authorization_url(method: 'post') # string — also accepted
153
+ client.authorization_url(method: :get) # default
154
+ client.authorization_url(method: 'get') # also accepted
155
+ ```
156
+
157
+ Passing any other value raises a `Safire::Errors::ConfigurationError`:
158
+
159
+ ```ruby
160
+ client.authorization_url(method: :put)
161
+ # => Safire::Errors::ConfigurationError:
162
+ # Invalid authorization method: :put. Supported methods are :get and :post
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Authorization Parameters
168
+
169
+ The parameters included in `:params` (POST) are identical to those embedded in the query string for GET:
170
+
171
+ | Parameter | Description |
172
+ |---|---|
173
+ | `response_type` | `"code"` — OAuth 2.0 authorization code flow |
174
+ | `client_id` | Your registered client identifier |
175
+ | `redirect_uri` | Callback URL for your application |
176
+ | `scope` | Requested permissions (space-separated) |
177
+ | `state` | CSRF protection token (32 hex chars, randomly generated) |
178
+ | `aud` | FHIR server being accessed |
179
+ | `code_challenge_method` | `"S256"` — PKCE using SHA-256 |
180
+ | `code_challenge` | SHA-256 hash of the code verifier |
181
+ | `launch` | EHR launch token (if provided via `launch:` argument) |
182
+
183
+ ---
184
+
185
+ ## Next Steps
186
+
187
+ - [Public Client Workflow]({% link smart-on-fhir/public-client/index.md %})
188
+ - [Confidential Symmetric Client Workflow]({% link smart-on-fhir/confidential-symmetric/index.md %})
189
+ - [Confidential Asymmetric Client Workflow]({% link smart-on-fhir/confidential-asymmetric/index.md %})
190
+ - [SMART Discovery Details]({% link smart-on-fhir/discovery/index.md %})