Project

safire

0.0
The project is in a healthy, maintained state
A Ruby gem implementing the SMART App Launch 2.2.0 specification and UDAP Security protocol for healthcare client applications. It supports OAuth 2.0 authorization against HL7 FHIR servers, including PKCE, private_key_jwt assertions (RS384 and ES384), confidential client flows, and the Backend Services system-to-system (client_credentials) grant.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

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 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
# => #<Safire::Protocols::UdapMetadata ...>

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

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, 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.