Skip to content

vanessuniq/safire

Repository files navigation

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 discovery plus certificate-backed Dynamic Client Registration. 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.

Dynamic Client Registration is implemented for certificate-backed UDAP clients:

udap_client = Safire::Client.new(
  {
    base_url: 'https://fhir.example.com',
    private_key: File.read('client-key.pem'),
    certificate_chain: [File.read('client-cert.pem')]
  },
  protocol: :udap
)

registration = udap_client.register_client(
  {
    client_name: 'Example Backend Service',
    contacts: ['mailto:security@example.com'],
    grant_types: ['client_credentials'],
    scope: 'system/Patient.rs'
  },
  client_uri:      'https://client.example.com',
  trusted_anchors: [ca_cert],
  crls:            [ca_crl]
)

cancellation = udap_client.cancel_registration(
  {
    client_name: 'Example Backend Service',
    contacts: ['mailto:security@example.com'],
    scope: 'system/Patient.rs'
  },
  client_uri:      'https://client.example.com',
  trusted_anchors: [ca_cert],
  crls:            [ca_crl]
)

UDAP JWT client authentication and Tiered OAuth are planned. See ROADMAP.md for details.


Installation

Requires Ruby ≥ 3.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)
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.

About

SMART App Launch and UDAP client library for Ruby

Topics

Resources

License

Code of conduct

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors