Skip to content

vanessuniq/safire

Safire

Gem Version CI Coverage Documentation

Safire is a Ruby gem for healthcare client applications that implements SMART App Launch 2.2.0 and UDAP Security STU2 / v2.0.0 server metadata discovery. It handles SMART OAuth 2.0 authorization against HL7 FHIR servers, covering PKCE, private key JWT assertions, and the Backend Services system-to-system flow, so you can focus on your application rather than protocol plumbing.


Features

SMART App Launch (v2.2.0)

  • Dynamic Client Registration (RFC 7591): obtain a client_id at runtime by POSTing client metadata to the server's registration endpoint
  • Discovery (/.well-known/smart-configuration)
  • Public Client (PKCE)
  • Confidential Symmetric Client (client_secret + HTTP Basic Auth)
  • Confidential Asymmetric Client (private_key_jwt with RS384/ES384)
  • POST-Based Authorization
  • Backend Services (client_credentials grant, JWT assertion, no user interaction or PKCE; scope defaults to system/*.rs)

UDAP Security (STU2)

Server metadata discovery is implemented. Pass protocol: :udap to fetch /.well-known/udap:

client = Safire::Client.new(
  { base_url: 'https://fhir.example.com' },
  protocol: :udap
)

metadata = client.server_metadata(verify_chain: false) # development/test only
# => #<Safire::Protocols::UdapMetadata ...>

# Community-scoped discovery
metadata = client.server_metadata(community: 'https://udap.example.org/community1', verify_chain: false)

Production UDAP discovery requires trust anchors plus an explicit certificate revocation policy (crls: or revocation_checker:). Use verify_chain: false only for development or tests.

Auth flows (DCR, JWT assertion, Tiered OAuth) are planned. See ROADMAP.md for details.


Installation

Requires Ruby ≥ 4.0.4.

gem 'safire'
bundle install

Quick Start

require 'safire'

# Step 1 — Create a client (Hash config or Safire::ClientConfig.new)
client = Safire::Client.new(
  {
    base_url:     'https://launch.smarthealthit.org/v/r4/sim/eyJoIjoiMSJ9/fhir',
    client_id:    'my_client_id',
    redirect_uri: 'https://myapp.example.com/callback',
    scopes:       ['openid', 'profile', 'patient/*.read']
  }
)

# Step 2 — Discover SMART metadata (lazy — only called when needed)
metadata = client.server_metadata
puts metadata.authorization_endpoint
puts metadata.capabilities.join(', ')

# Step 3 — Build the authorization URL (Safire generates state + PKCE automatically)
auth_data = client.authorization_url
# auth_data => { auth_url:, state:, code_verifier: }
# Store state and code_verifier server-side, redirect the user to auth_data[:auth_url]

# Step 4 — Exchange the authorization code for tokens (on callback)
token_data = client.request_access_token(
  code:          params[:code],
  code_verifier: session[:code_verifier]
)
# token_data => { "access_token" => "...", "token_type" => "Bearer", ... }

# Step 5 — Refresh when the access token expires
new_tokens = client.refresh_token(refresh_token: token_data['refresh_token'])

Supported SMART Client Types

client_type: Authentication When to use
:public (default) PKCE only Browser/mobile apps that cannot store a secret
:confidential_symmetric HTTP Basic Auth (client_secret) Server-side apps with a securely stored secret
:confidential_asymmetric JWT assertion (private_key_jwt, RS384/ES384) Server-side apps using a registered key pair

For a confidential asymmetric client, provide a private key and key ID:

client = Safire::Client.new(
  {
    base_url:    'https://fhir.example.com',
    client_id:   'my_client_id',
    redirect_uri: 'https://myapp.example.com/callback',
    scopes:      ['openid', 'profile', 'patient/*.read'],
    private_key: OpenSSL::PKey::RSA.new(File.read('private_key.pem')),
    kid:         'my-key-id-123'
  },
  client_type: :confidential_asymmetric
)
# Authorization and token exchange are identical — Safire builds the JWT assertion automatically

Backend Services (system-to-system)

No user interaction, redirect URI, or PKCE required — the client authenticates entirely via a signed JWT assertion:

client = Safire::Client.new(
  {
    base_url:    'https://fhir.example.com',
    client_id:   'my_backend_client',
    private_key: OpenSSL::PKey::RSA.new(File.read('private_key.pem')),
    kid:         'my-key-id-123',
    scopes:      ['system/Patient.rs', 'system/Observation.rs']
  }
)

token_data = client.request_backend_token
# token_data => { "access_token" => "...", "token_type" => "Bearer", "expires_in" => 300, ... }

# Override scope or credentials per call
token_data = client.request_backend_token(
  scopes:      ['system/Patient.rs'],
  private_key: OpenSSL::PKey::RSA.new(File.read('new_key.pem')),
  kid:         'new-key-id'
)

# Validate the token response (flow: :backend_services also checks expires_in)
client.token_response_valid?(token_data, flow: :backend_services)

Configuration

Safire.configure do |config|
  config.logger   = Rails.logger   # Default: $stdout
  config.log_http = true           # Log HTTP requests (sensitive headers always filtered)
end

See the Configuration Guide for all options including user_agent, log_level, and SSL settings.


Demo Application

A Sinatra-based demo is included in examples/sinatra_app/:

bin/demo
# Visit http://localhost:4567

Demonstrates Dynamic Client Registration, SMART discovery, UDAP discovery with signed_metadata trust validation, all authorization flows, token refresh, and backend services token requests. See examples/sinatra_app/README.md for details.


Development

bin/setup            # Install dependencies
bundle exec rspec    # Run tests
bin/console          # Interactive prompt

To serve the docs locally:

bin/docs
cd docs && bundle install && bundle exec jekyll serve
# Visit http://localhost:4000/safire/

Contributing

Bug reports and pull requests are welcome. Please read CONTRIBUTION.md before opening a PR — it covers branch naming, commit message style, and the sign-off requirement.


License

Available as open source under the Apache 2.0 License.


Parts of this project were developed with AI assistance (Claude Code) and reviewed by maintainers.