Safire

Gem Version CI Coverage Documentation

Safire is a Ruby gem implementing the SMART App Launch 2.2.0 specification and the UDAP Security protocol for healthcare client applications. It handles 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

Planned. See ROADMAP.md for details.


Installation

Requires Ruby ≥ 4.0.2.

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)
 = client.
puts .authorization_endpoint
puts .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, 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.