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 delegation
When protocol: :udap is fully implemented, client_type: will default to nil (not applicable) and the flow: kwarg will drive B2B vs B2C selection.
Unified facade client for SMART on FHIR and (future) UDAP authorization flows.
This class is the main entry point for integrating SMART on FHIR 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] OAuth2 client identifier
-
:redirect_uri [String] redirect URI registered with the authorization server; required for app launch, not required for backend services
-
:scopes [Array<String>] 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:
-
:public (default) — no client authentication; client_id sent in request body
-
:confidential_symmetric — HTTP Basic auth using client_secret
-
:confidential_asymmetric — private_key_jwt assertion (JWT signed with private key)
client_type is validated for :smart and ignored for :udap. 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_CLASSES =
{ smart: Protocols::Smart # udap: Protocols::Udap # future }.freeze
- PROTOCOL_CLIENT_TYPES =
Valid client_type values per protocol. nil means the protocol does not use client_type (e.g. UDAP authenticates via signed JWT assertions with an X.509 certificate chain; the authentication method is not user-configurable for UDAP).
{ 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 client authentication method (:public, :confidential_symmetric, or :confidential_asymmetric).
-
#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: :public) ⇒ Client
constructor
A new instance of Client.
Constructor Details
#initialize(config, protocol: :smart, client_type: :public) ⇒ Client
Returns a new instance of Client.
132 133 134 135 136 137 138 139 |
# File 'lib/safire/client.rb', line 132 def initialize(config, protocol: :smart, client_type: :public) @protocol = protocol.to_sym @client_type = client_type.to_sym @config = build_config(config) validate_protocol! validate_client_type! end |
Instance Attribute Details
#client_type ⇒ Symbol
Returns the client authentication method (:public, :confidential_symmetric, or :confidential_asymmetric).
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 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 |
# File 'lib/safire/client.rb', line 105 class Client extend Forwardable VALID_PROTOCOLS = %i[smart udap].freeze PROTOCOL_CLASSES = { smart: Protocols::Smart # udap: Protocols::Udap # future }.freeze # Valid client_type values per protocol. # nil means the protocol does not use client_type (e.g. UDAP authenticates via signed # JWT assertions with an X.509 certificate chain; the authentication method is not # user-configurable for UDAP). 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: :public) @protocol = protocol.to_sym @client_type = client_type.to_sym @config = build_config(config) validate_protocol! 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) if PROTOCOL_CLIENT_TYPES[@protocol].nil? Safire.logger.warn( "client_type is not configurable for protocol: :#{@protocol}; " \ 'UDAP clients authenticate via signed JWT assertions — ignoring' ) return end @client_type = new_client_type.to_sym validate_client_type! @protocol_client&.client_type = @client_type end private def protocol_client @protocol_client ||= PROTOCOL_CLASSES.fetch(@protocol).new(config, client_type:) 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 validate_client_type! valid_types = PROTOCOL_CLIENT_TYPES[@protocol] return if valid_types.nil? || valid_types.include?(@client_type) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: @client_type, valid_values: valid_types ) end end |
#config ⇒ Safire::ClientConfig (readonly)
Returns the resolved client configuration.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 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 |
# File 'lib/safire/client.rb', line 105 class Client extend Forwardable VALID_PROTOCOLS = %i[smart udap].freeze PROTOCOL_CLASSES = { smart: Protocols::Smart # udap: Protocols::Udap # future }.freeze # Valid client_type values per protocol. # nil means the protocol does not use client_type (e.g. UDAP authenticates via signed # JWT assertions with an X.509 certificate chain; the authentication method is not # user-configurable for UDAP). 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: :public) @protocol = protocol.to_sym @client_type = client_type.to_sym @config = build_config(config) validate_protocol! 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) if PROTOCOL_CLIENT_TYPES[@protocol].nil? Safire.logger.warn( "client_type is not configurable for protocol: :#{@protocol}; " \ 'UDAP clients authenticate via signed JWT assertions — ignoring' ) return end @client_type = new_client_type.to_sym validate_client_type! @protocol_client&.client_type = @client_type end private def protocol_client @protocol_client ||= PROTOCOL_CLASSES.fetch(@protocol).new(config, client_type:) 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 validate_client_type! valid_types = PROTOCOL_CLIENT_TYPES[@protocol] return if valid_types.nil? || valid_types.include?(@client_type) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: @client_type, valid_values: valid_types ) end end |
#protocol ⇒ Symbol (readonly)
Returns the selected protocol (:smart or :udap).
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 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 |
# File 'lib/safire/client.rb', line 105 class Client extend Forwardable VALID_PROTOCOLS = %i[smart udap].freeze PROTOCOL_CLASSES = { smart: Protocols::Smart # udap: Protocols::Udap # future }.freeze # Valid client_type values per protocol. # nil means the protocol does not use client_type (e.g. UDAP authenticates via signed # JWT assertions with an X.509 certificate chain; the authentication method is not # user-configurable for UDAP). 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: :public) @protocol = protocol.to_sym @client_type = client_type.to_sym @config = build_config(config) validate_protocol! 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) if PROTOCOL_CLIENT_TYPES[@protocol].nil? Safire.logger.warn( "client_type is not configurable for protocol: :#{@protocol}; " \ 'UDAP clients authenticate via signed JWT assertions — ignoring' ) return end @client_type = new_client_type.to_sym validate_client_type! @protocol_client&.client_type = @client_type end private def protocol_client @protocol_client ||= PROTOCOL_CLASSES.fetch(@protocol).new(config, client_type:) 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 validate_client_type! valid_types = PROTOCOL_CLIENT_TYPES[@protocol] return if valid_types.nil? || valid_types.include?(@client_type) raise Errors::ConfigurationError.new( invalid_attribute: :client_type, invalid_value: @client_type, valid_values: valid_types ) end end |