Class: Safire::Client
- Inherits:
-
Object
- Object
- Safire::Client
- Extended by:
- Forwardable
- Defined in:
- lib/safire/client.rb
Overview
Future kwargs (not yet implemented):
flow: [Symbol] the authorization flow for UDAP clients (protocol: :udap):
:b2b — client_credentials grant, server-to-server
:b2c — authorization_code grant, user-facing
:tiered_oauth — authorization_code + IdP identity delegationUnified facade client for SMART and (future) UDAP authorization flows.
This class is the main entry point for integrating SMART authorization via Safire. It supports discovery of server metadata and provides a unified interface for building authorization URLs, exchanging authorization codes, refreshing tokens, and requesting backend services access tokens (client_credentials grant).
Configuration is provided via ClientConfig or a Hash. Key attributes:
- :base_url [String] FHIR base URL used for SMART discovery
- :client_id [String, nil] OAuth2 client identifier — optional at initialization; required by all authorization flows and validated at call time
- :redirect_uri [String] redirect URI registered with the authorization server; required for app launch, not required for backend services
- :scopes [Array
] default scopes; falls back to ["system/*.rs"]for backend services when not provided - :client_secret [String, optional] required for confidential_symmetric clients
- :private_key [OpenSSL::PKey, String, optional] private key for asymmetric clients and backend services
- :kid [String, optional] key ID matching the registered public key for asymmetric clients and backend services
- :jwt_algorithm [String, optional] JWT signing algorithm (RS384 or ES384). Auto-detected if not provided
- :jwks_uri [String, optional] URL to client's JWKS for jku header in JWT assertions
The protocol: keyword selects the authorization protocol:
- :smart (default) — SMART App Launch 2.2.0
- :udap — UDAP Security (future; not yet implemented)
The client_type: keyword controls how the SMART client authenticates at the token endpoint.
Defaults to nil, which resolves to :public for SMART. For UDAP, client_type: is not
applicable — passing any explicit value raises ConfigurationError.
- :public — no client authentication; client_id sent in request body (SMART default)
- :confidential_symmetric — HTTP Basic auth using client_secret
- :confidential_asymmetric — private_key_jwt assertion (JWT signed with private key)
UDAP clients authenticate via signed JWT assertions (Authentication Token / AnT) with an X.509 certificate chain in the x5c JOSE header; the authentication method is not user-configurable for UDAP. DCR is typically performed once to obtain a client_id, which is then reused as iss/sub in every subsequent AnT. The unregistered client flow (§8.1) allows client_credentials grant without prior DCR when identity can be fully determined from certificate attributes alone.
Constant Summary collapse
- VALID_PROTOCOLS =
%i[smart udap].freeze
- PROTOCOL_CLIENT_TYPES =
Valid client_type values per protocol. nil means client_type is not applicable for that protocol; any explicit value raises ConfigurationError.
{ smart: %i[public confidential_symmetric confidential_asymmetric], udap: nil # UDAP authenticates via signed JWT assertions (AnT) with X.509 certificate chain }.freeze
Instance Attribute Summary collapse
-
#client_type ⇒ Symbol?
The SMART client authentication method (:public, :confidential_symmetric, or :confidential_asymmetric); nil for protocol: :udap where client authentication is not user-configurable.
-
#config ⇒ Safire::ClientConfig
readonly
The resolved client configuration.
-
#protocol ⇒ Symbol
readonly
The selected protocol (:smart or :udap).
Instance Method Summary collapse
-
#initialize(config, protocol: :smart, client_type: nil) ⇒ Client
constructor
A new instance of Client.
Constructor Details
#initialize(config, protocol: :smart, client_type: nil) ⇒ Client
Returns a new instance of Client.
154 155 156 157 158 159 160 161 162 |
# File 'lib/safire/client.rb', line 154 def initialize(config, protocol: :smart, client_type: nil) @protocol = protocol.to_sym @client_type = normalize_client_type(client_type) @config = build_config(config) validate_protocol! resolve_client_type! validate_client_type! end |
Instance Attribute Details
#client_type ⇒ Symbol?
Returns the SMART client authentication method (:public, :confidential_symmetric, or :confidential_asymmetric); nil for protocol: :udap where client authentication is not user-configurable.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/safire/client.rb', line 134 class Client extend Forwardable VALID_PROTOCOLS = %i[smart udap].freeze # Valid client_type values per protocol. # nil means client_type is not applicable for that protocol; any explicit value raises ConfigurationError. PROTOCOL_CLIENT_TYPES = { smart: %i[public confidential_symmetric confidential_asymmetric], udap: nil # UDAP authenticates via signed JWT assertions (AnT) with X.509 certificate chain }.freeze def_delegators :protocol_client, :server_metadata, :authorization_url, :request_access_token, :refresh_token, :request_backend_token, :token_response_valid?, :register_client attr_reader :config, :protocol, :client_type def initialize(config, protocol: :smart, client_type: nil) @protocol = protocol.to_sym @client_type = normalize_client_type(client_type) @config = build_config(config) validate_protocol! resolve_client_type! validate_client_type! end # Changes the client type for this client. # # Updates the underlying protocol client in place — server metadata already # fetched is preserved and no re-discovery occurs. # # @param new_client_type [Symbol, String] the new client type # @return [Symbol] the new client type # @raise [Safire::Errors::ConfigurationError] if the client type is not valid for this protocol # # @example Discover then switch client type # client = Safire::Client.new(config) # defaults to :public # metadata = client.server_metadata # # if metadata.supports_symmetric_auth? # client.client_type = :confidential_symmetric # end def client_type=(new_client_type) raise_client_type_not_applicable!(new_client_type) if PROTOCOL_CLIENT_TYPES[@protocol].nil? @client_type = normalize_client_type(new_client_type) validate_client_type! @protocol_client&.client_type = @client_type end private def protocol_client @protocol_client ||= build_protocol_client end def build_protocol_client case @protocol when :smart then Protocols::Smart.new(config, client_type:) when :udap then raise NotImplementedError, 'UDAP protocol client is not yet implemented' end end def build_config(config) return config if config.is_a?(Safire::ClientConfig) Safire::ClientConfig.new(config) end def validate_protocol! return if VALID_PROTOCOLS.include?(@protocol) raise Errors::ConfigurationError.new( invalid_attribute: :protocol, invalid_value: @protocol, valid_values: VALID_PROTOCOLS ) end def resolve_client_type! @client_type = :public if @protocol == :smart && @client_type.nil? end def validate_client_type! valid_types = PROTOCOL_CLIENT_TYPES[@protocol] if valid_types.nil? return if @client_type.nil? raise_client_type_not_applicable!(@client_type) end return if valid_types.include?(@client_type) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: @client_type, valid_values: valid_types ) end def raise_client_type_not_applicable!(value) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: value, valid_values: ["N/A (client_type is not applicable for protocol :#{@protocol})"] ) end def normalize_client_type(value) return nil if value.nil? return value.to_sym if value.is_a?(Symbol) || value.is_a?(String) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: value, valid_values: PROTOCOL_CLIENT_TYPES[:smart] ) end end |
#config ⇒ Safire::ClientConfig (readonly)
Returns the resolved client configuration.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/safire/client.rb', line 134 class Client extend Forwardable VALID_PROTOCOLS = %i[smart udap].freeze # Valid client_type values per protocol. # nil means client_type is not applicable for that protocol; any explicit value raises ConfigurationError. PROTOCOL_CLIENT_TYPES = { smart: %i[public confidential_symmetric confidential_asymmetric], udap: nil # UDAP authenticates via signed JWT assertions (AnT) with X.509 certificate chain }.freeze def_delegators :protocol_client, :server_metadata, :authorization_url, :request_access_token, :refresh_token, :request_backend_token, :token_response_valid?, :register_client attr_reader :config, :protocol, :client_type def initialize(config, protocol: :smart, client_type: nil) @protocol = protocol.to_sym @client_type = normalize_client_type(client_type) @config = build_config(config) validate_protocol! resolve_client_type! validate_client_type! end # Changes the client type for this client. # # Updates the underlying protocol client in place — server metadata already # fetched is preserved and no re-discovery occurs. # # @param new_client_type [Symbol, String] the new client type # @return [Symbol] the new client type # @raise [Safire::Errors::ConfigurationError] if the client type is not valid for this protocol # # @example Discover then switch client type # client = Safire::Client.new(config) # defaults to :public # metadata = client.server_metadata # # if metadata.supports_symmetric_auth? # client.client_type = :confidential_symmetric # end def client_type=(new_client_type) raise_client_type_not_applicable!(new_client_type) if PROTOCOL_CLIENT_TYPES[@protocol].nil? @client_type = normalize_client_type(new_client_type) validate_client_type! @protocol_client&.client_type = @client_type end private def protocol_client @protocol_client ||= build_protocol_client end def build_protocol_client case @protocol when :smart then Protocols::Smart.new(config, client_type:) when :udap then raise NotImplementedError, 'UDAP protocol client is not yet implemented' end end def build_config(config) return config if config.is_a?(Safire::ClientConfig) Safire::ClientConfig.new(config) end def validate_protocol! return if VALID_PROTOCOLS.include?(@protocol) raise Errors::ConfigurationError.new( invalid_attribute: :protocol, invalid_value: @protocol, valid_values: VALID_PROTOCOLS ) end def resolve_client_type! @client_type = :public if @protocol == :smart && @client_type.nil? end def validate_client_type! valid_types = PROTOCOL_CLIENT_TYPES[@protocol] if valid_types.nil? return if @client_type.nil? raise_client_type_not_applicable!(@client_type) end return if valid_types.include?(@client_type) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: @client_type, valid_values: valid_types ) end def raise_client_type_not_applicable!(value) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: value, valid_values: ["N/A (client_type is not applicable for protocol :#{@protocol})"] ) end def normalize_client_type(value) return nil if value.nil? return value.to_sym if value.is_a?(Symbol) || value.is_a?(String) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: value, valid_values: PROTOCOL_CLIENT_TYPES[:smart] ) end end |
#protocol ⇒ Symbol (readonly)
Returns the selected protocol (:smart or :udap).
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/safire/client.rb', line 134 class Client extend Forwardable VALID_PROTOCOLS = %i[smart udap].freeze # Valid client_type values per protocol. # nil means client_type is not applicable for that protocol; any explicit value raises ConfigurationError. PROTOCOL_CLIENT_TYPES = { smart: %i[public confidential_symmetric confidential_asymmetric], udap: nil # UDAP authenticates via signed JWT assertions (AnT) with X.509 certificate chain }.freeze def_delegators :protocol_client, :server_metadata, :authorization_url, :request_access_token, :refresh_token, :request_backend_token, :token_response_valid?, :register_client attr_reader :config, :protocol, :client_type def initialize(config, protocol: :smart, client_type: nil) @protocol = protocol.to_sym @client_type = normalize_client_type(client_type) @config = build_config(config) validate_protocol! resolve_client_type! validate_client_type! end # Changes the client type for this client. # # Updates the underlying protocol client in place — server metadata already # fetched is preserved and no re-discovery occurs. # # @param new_client_type [Symbol, String] the new client type # @return [Symbol] the new client type # @raise [Safire::Errors::ConfigurationError] if the client type is not valid for this protocol # # @example Discover then switch client type # client = Safire::Client.new(config) # defaults to :public # metadata = client.server_metadata # # if metadata.supports_symmetric_auth? # client.client_type = :confidential_symmetric # end def client_type=(new_client_type) raise_client_type_not_applicable!(new_client_type) if PROTOCOL_CLIENT_TYPES[@protocol].nil? @client_type = normalize_client_type(new_client_type) validate_client_type! @protocol_client&.client_type = @client_type end private def protocol_client @protocol_client ||= build_protocol_client end def build_protocol_client case @protocol when :smart then Protocols::Smart.new(config, client_type:) when :udap then raise NotImplementedError, 'UDAP protocol client is not yet implemented' end end def build_config(config) return config if config.is_a?(Safire::ClientConfig) Safire::ClientConfig.new(config) end def validate_protocol! return if VALID_PROTOCOLS.include?(@protocol) raise Errors::ConfigurationError.new( invalid_attribute: :protocol, invalid_value: @protocol, valid_values: VALID_PROTOCOLS ) end def resolve_client_type! @client_type = :public if @protocol == :smart && @client_type.nil? end def validate_client_type! valid_types = PROTOCOL_CLIENT_TYPES[@protocol] if valid_types.nil? return if @client_type.nil? raise_client_type_not_applicable!(@client_type) end return if valid_types.include?(@client_type) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: @client_type, valid_values: valid_types ) end def raise_client_type_not_applicable!(value) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: value, valid_values: ["N/A (client_type is not applicable for protocol :#{@protocol})"] ) end def normalize_client_type(value) return nil if value.nil? return value.to_sym if value.is_a?(Symbol) || value.is_a?(String) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: value, valid_values: PROTOCOL_CLIENT_TYPES[:smart] ) end end |