Class: Safire::Protocols::Smart Private

Inherits:
Object
  • Object
show all
Includes:
Behaviours
Defined in:
lib/safire/protocols/smart.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Note:

For internal use by Client only.

SMART on FHIR OAuth2 implementation for app launch (authorization code, token exchange, refresh) and backend services (client credentials) flows.

This is an internal class used exclusively by Client. Do not instantiate it directly —use Client instead.

Accepts a ClientConfig and a client_type symbol. Reads all configuration attributes directly from the ClientConfig object. Discovery of authorization and token endpoints from the FHIR server’s /.well-known/smart-configuration metadata is performed automatically when those endpoints are not present in the config.

Raises:

Constant Summary collapse

ATTRIBUTES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

%i[
  base_url client_id client_secret redirect_uri scopes issuer
  authorization_endpoint token_endpoint
  private_key kid jwt_algorithm jwks_uri
].freeze
OPTIONAL_ATTRIBUTES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Attributes that are not required during validation

%i[
  redirect_uri authorization_endpoint scopes client_secret private_key kid jwt_algorithm jwks_uri
].freeze
WELL_KNOWN_PATH =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

'/.well-known/smart-configuration'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Behaviours

#register_client

Constructor Details

#initialize(config, client_type: :public) ⇒ Smart

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Smart.



39
40
41
42
43
44
45
46
47
# File 'lib/safire/protocols/smart.rb', line 39

def initialize(config, client_type: :public)
  ATTRIBUTES.each { |attr| instance_variable_set("@#{attr}", config.public_send(attr)) }

  @client_type = client_type.to_sym
  @http_client = Safire::HTTPClient.new
  @issuer ||= base_url

  validate!
end

Instance Attribute Details

#client_typeObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



36
37
38
# File 'lib/safire/protocols/smart.rb', line 36

def client_type
  @client_type
end

Instance Method Details

#authorization_endpointObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



49
50
51
# File 'lib/safire/protocols/smart.rb', line 49

def authorization_endpoint
  @authorization_endpoint ||= .authorization_endpoint
end

#authorization_url(launch: nil, custom_scopes: nil, method: :get) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds the authorization request data for the authorization code flow.

Parameters:

  • launch (String, nil) (defaults to: nil)

    optional launch parameter

  • custom_scopes (Array<String>, nil) (defaults to: nil)

    optional custom scopes to override the configured ones

  • method (Symbol, String) (defaults to: :get)

    authorization request method; :get (default) or :post. Both symbol and string forms are accepted (e.g. +method: :post+ or +method: ‘post’+). * :get — builds a redirect URL with all parameters in the query string (standard flow) * :post — returns the endpoint and parameters separately for POST-based authorization (SMART App Launch 2.2.0 authorize-post capability)

Returns:

  • (Hash)

    containing: * :auth_url [String] authorization URL (GET) or bare endpoint URL (POST) * :state [String] state parameter for CSRF protection; store and verify on callback * :code_verifier [String] PKCE code verifier for the token exchange * :params [Hash] (POST only) authorization parameters to submit as the request body

Raises:

  • (Errors::ConfigurationError)

    if method is invalid, scopes are missing, or redirect_uri / authorization_endpoint are not configured or resolvable via discovery



98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/safire/protocols/smart.rb', line 98

def authorization_url(launch: nil, custom_scopes: nil, method: :get)
  method = method.to_sym
  custom_scopes ||= scopes
  validate_authorization_method(method)
  validate_presence_of_scopes(custom_scopes)
  validate_app_launch_attrs!

  Safire.logger.info("Generating authorization URL for SMART #{client_type} (method: #{method})...")

  code_verifier = PKCE.generate_code_verifier
  params = authorization_params(launch:, custom_scopes:, code_verifier:)

  build_authorization_response(method, params, code_verifier)
end

#refresh_token(refresh_token:, scopes: nil, client_secret: self.client_secret, private_key: self.private_key, kid: self.kid) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Exchanges a refresh token for a new access token.

Parameters:

  • refresh_token (String)

    the refresh token issued by the authorization server (required)

  • scopes (Array<String>, nil) (defaults to: nil)

    optional reduced scope list; if omitted, the same scopes as the original token are requested

  • client_secret (String, nil) (defaults to: self.client_secret)

    optional; used for confidential symmetric clients when not already configured

  • private_key (OpenSSL::PKey, String, nil) (defaults to: self.private_key)

    optional; private key for asymmetric auth (overrides configured)

  • kid (String, nil) (defaults to: self.kid)

    optional; key ID for asymmetric auth (overrides configured)

Returns:

  • (Hash)

    token response parsed from the authorization server. See #request_access_token for token response format.

Raises:



157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/safire/protocols/smart.rb', line 157

def refresh_token(refresh_token:, scopes: nil, client_secret: self.client_secret,
                  private_key: self.private_key, kid: self.kid)
  Safire.logger.info('Refreshing access token...')

  response = @http_client.post(
    token_endpoint,
    body: refresh_token_params(refresh_token:, scopes:, private_key:, kid:),
    headers: oauth2_headers(client_secret)
  )

  parse_token_response(response.body)
rescue Faraday::Error => e
  raise token_error_from(e)
end

#request_access_token(code:, code_verifier:, client_secret: self.client_secret, private_key: self.private_key, kid: self.kid) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Exchanges the authorization code for an access token.

Parameters:

  • code (String)

    authorization code from the authorization server

  • code_verifier (String)

    PKCE code verifier from the authorization step

  • client_secret (String, nil) (defaults to: self.client_secret)

    optional; used for confidential symmetric clients when not already configured

  • private_key (OpenSSL::PKey, String, nil) (defaults to: self.private_key)

    optional; private key for asymmetric auth (overrides configured)

  • kid (String, nil) (defaults to: self.kid)

    optional; key ID for asymmetric auth (overrides configured)

Returns:

  • (Hash)

    token response parsed from the authorization server, including: * “access_token” [String] new access token issued by the authorization server (required) * “token_type” [String] token type, exactly +“Bearer”+ (required, case-sensitive per SMART spec) * “expires_in” [Integer] lifetime of the access token in seconds (RECOMMENDED) * “scope” [String] authorized scopes for this token (required) * “refresh_token” [String] refresh token, if issued (optional) * “authorization_details” [Hash] additional authorization details, if provided (optional) * Context parameters such as “patient” or “encounter” MAY be present, depending on server behavior.

Raises:



130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/safire/protocols/smart.rb', line 130

def request_access_token(code:, code_verifier:, client_secret: self.client_secret,
                         private_key: self.private_key, kid: self.kid)
  Safire.logger.info('Requesting access token using authorization code...')

  response = @http_client.post(
    token_endpoint,
    body: access_token_params(code, code_verifier, private_key:, kid:),
    headers: oauth2_headers(client_secret)
  )

  parse_token_response(response.body)
rescue Faraday::Error => e
  raise token_error_from(e)
end

#request_backend_token(scopes: nil, private_key: self.private_key, kid: self.kid) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Requests an access token using the client credentials grant (SMART Backend Services).

Implements the SMART Backend Services Authorization flow per hl7.org/fhir/smart-app-launch/backend-services.html

No user interaction, redirect, or PKCE is involved. The client authenticates exclusively via a signed JWT assertion (RS384 or ES384).

Parameters:

  • scopes (Array<String>, nil) (defaults to: nil)

    scope override; uses configured scopes if nil, falling back to +[“system/*.rs”]+ when neither is provided

  • private_key (OpenSSL::PKey) (defaults to: self.private_key)

    private key for JWT assertion; uses configured key if not provided. Required — must be present either in configuration or passed here.

  • kid (String) (defaults to: self.kid)

    key ID for JWT assertion header; uses configured kid if not provided. Required — must be present either in configuration or passed here.

Returns:

  • (Hash)

    token response from the authorization server, including: * “access_token” [String] access token (required) * “token_type” [String] token type, value +“Bearer”+ (required) * “expires_in” [Integer] lifetime of the access token in seconds (required per Backend Services spec) * “scope” [String] authorized scopes (required)

Raises:



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/safire/protocols/smart.rb', line 194

def request_backend_token(scopes: nil, private_key: self.private_key, kid: self.kid)
  scopes ||= self.scopes.presence || ['system/*.rs']

  Safire.logger.info('Requesting backend services access token (client_credentials grant)...')

  response = @http_client.post(
    token_endpoint,
    body: backend_services_token_params(scopes:, private_key:, kid:),
    headers: { content_type: 'application/x-www-form-urlencoded' }
  )

  parse_token_response(response.body)
rescue Faraday::Error => e
  raise token_error_from(e)
end

#server_metadataSafire::Protocols::SmartMetadata

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Retrieves and parses SMART on FHIR configuration metadata from the FHIR server.

This method sends a GET request to the server’s /.well-known/smart-configuration endpoint, validates the response format, and builds a Safire::Protocols::SmartMetadata object containing the authorization and token endpoints, among other SMART metadata fields.

The result is cached after the first successful discovery and reused on subsequent calls within the same instance.

Returns:

Raises:



71
72
73
74
75
76
77
78
79
80
# File 'lib/safire/protocols/smart.rb', line 71

def 
  return @server_metadata if @server_metadata

  response = @http_client.get(well_known_endpoint)
  @server_metadata = SmartMetadata.new(parse_discovery_body(response.body))
rescue Faraday::Error => e
  status = e.response&.dig(:status)
  Safire.logger.error("SMART discovery failed for `#{well_known_endpoint}`: HTTP #{status}")
  raise Errors::DiscoveryError.new(endpoint: well_known_endpoint, status: status)
end

#token_endpointObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



53
54
55
# File 'lib/safire/protocols/smart.rb', line 53

def token_endpoint
  @token_endpoint ||= .token_endpoint
end

#token_response_valid?(response, flow: :app_launch) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Validates a token response for SMART compliance.

Checks all required token response fields based on the authorization flow: - access_token must be present (all flows, SHALL) - token_type must be present and +“Bearer”+ or +“bearer”+ (all flows, SHALL) - scope must be present (all flows, SHALL) - expires_in must be present when +flow: :backend_services+ (required per Backend Services spec)

Logs a warning via Safire.logger for each violation found and returns false. Never raises an exception.

Parameters:

  • response (Hash)

    the token response returned by the server

  • flow (Symbol) (defaults to: :app_launch)

    the authorization flow; :app_launch (default) or :backend_services

Returns:

  • (Boolean)

    true if the response is compliant, false otherwise



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/safire/protocols/smart.rb', line 224

def token_response_valid?(response, flow: :app_launch)
  unless response.is_a?(Hash)
    Safire.logger.warn('SMART token response non-compliance: response is not a JSON object')
    return false
  end

  valid = true

  required_fields = %w[access_token scope]
  required_fields << 'expires_in' if flow == :backend_services

  required_fields.each do |field|
    next if response[field].present?

    Safire.logger.warn(
      "SMART token response non-compliance: required field '#{field}' is missing"
    )
    valid = false
  end

  token_type_valid?(response, flow:) && valid
end