Security Guide

This guide covers security requirements and best practices for every Safire integration, regardless of client type. Apply these rules in all production deployments.

Table of contents

  1. Security Guide
    1. HTTPS and Redirect URI Rules
    2. Credential Protection
      1. Client Secrets (Confidential Symmetric)
      2. Private Keys (Confidential Asymmetric)
    3. Token and Session Security
      1. Token Storage
      2. CSRF State Parameter
      3. PKCE Code Verifier
    4. Key Rotation and Scope Minimization
      1. Symmetric Secret Rotation
      2. Asymmetric Key Rotation
      3. Scope Minimization

HTTPS and Redirect URI Rules

All production FHIR integrations must use HTTPS. Safire enforces this at configuration time — HTTP redirect URIs are rejected in non-localhost environments.

# config/environments/production.rb
config.force_ssl = true
# ✅ Always use HTTPS in production
config = Safire::ClientConfig.new(
  redirect_uri: 'https://myapp.example.com/auth/callback',
  # ...
)

# ❌ Raises Safire::Errors::ConfigurationError
config = Safire::ClientConfig.new(
  redirect_uri: 'http://myapp.example.com/auth/callback',
  # ...
)

Localhost is permitted during development:

# ✅ Allowed for local development only
redirect_uri: 'http://localhost:3000/auth/callback'

Credential Protection

Never expose client secrets or private keys in logs, responses, or version control.

Client Secrets (Confidential Symmetric)

# ❌ NEVER: log the secret
Rails.logger.info("Using secret: #{client_secret}")

# ❌ NEVER: render in a response
render json: { client_secret: ENV['SMART_CLIENT_SECRET'] }

# ❌ NEVER: commit .env to version control
# Add to .gitignore: .env

Load secrets from a secure source:

# Environment variable
config = Safire::ClientConfig.new(
  client_secret: ENV.fetch('SMART_CLIENT_SECRET'),
  # ...
)

# Rails credentials
config = Safire::ClientConfig.new(
  client_secret: Rails.application.credentials.smart[:client_secret],
  # ...
)

# AWS Secrets Manager
require 'aws-sdk-secretsmanager'

def fetch_client_secret
  client = Aws::SecretsManager::Client.new
  secret = client.get_secret_value(secret_id: 'smart/credentials')
  JSON.parse(secret.secret_string)['client_secret']
end

Private Keys (Confidential Asymmetric)

# ❌ NEVER: render or log the key
render json: { private_key: @private_key.to_pem }

# Add to .gitignore: *.pem, *.key

Load private keys securely:

# From a file path
private_key = OpenSSL::PKey::RSA.new(File.read(ENV['SMART_PRIVATE_KEY_PATH']))

# From a PEM string in an env var
private_key = OpenSSL::PKey::RSA.new(ENV['SMART_PRIVATE_KEY_PEM'])

# From Rails credentials
private_key = OpenSSL::PKey::RSA.new(
  Rails.application.credentials.smart[:private_key_pem]
)

# From AWS Secrets Manager
def fetch_private_key
  client = Aws::SecretsManager::Client.new
  secret = client.get_secret_value(secret_id: 'smart/private-key')
  OpenSSL::PKey::RSA.new(secret.secret_string)
end

Use strong keys:

# RSA: minimum 2048-bit, 4096-bit recommended
key = OpenSSL::PKey::RSA.generate(4096)

# EC: must use P-384 curve (required by SMART spec for ES384)
key = OpenSSL::PKey::EC.generate('secp384r1')

Safire automatically masks client_secret and private_key in inspect output and error messages, so they will not appear in Rails logs even if a ClientConfig object is accidentally logged.


Token and Session Security

Token Storage

Always store tokens server-side. Never expose them to client-side code.

# ✅ DO: Server-side session
session[:access_token] = tokens['access_token']

# ✅ DO: Encrypted database column
user.update(encrypted_access_token: cipher.encrypt(tokens['access_token']))

# ❌ DON'T: Plain cookie
cookies[:access_token] = tokens['access_token']

# ❌ DON'T: JSON response to the browser
render json: { access_token: tokens['access_token'] }

CSRF State Parameter

Safire generates a 32-character hex state value (128 bits of entropy) automatically. Always verify it on callback and delete it immediately after:

def callback
  unless params[:state] == session[:oauth_state]
    render plain: 'Invalid state', status: :unauthorized
    return
  end

  # ... exchange code for tokens ...

  session.delete(:oauth_state)   # ✅ Delete after validation
  session.delete(:code_verifier) # ✅ Delete after token exchange
end

PKCE Code Verifier

Safire generates the code verifier automatically. Store it server-side only and discard it immediately after the token exchange — never send it to the client or include it in a URL.

# Store on launch
session[:code_verifier] = auth_data[:code_verifier]

# Delete immediately after exchange
session.delete(:code_verifier)

Key Rotation and Scope Minimization

Symmetric Secret Rotation

Support two secrets during rotation to allow a zero-downtime rollover:

module SmartSecretRotation
  def build_smart_client
    create_client(primary_secret)
  rescue Safire::Errors::TokenError => e
    raise unless e.error_code == 'invalid_client'
    create_client(secondary_secret) # Fall back during rotation
  end

  def primary_secret   = ENV['SMART_CLIENT_SECRET']
  def secondary_secret = ENV['SMART_CLIENT_SECRET_PREVIOUS']
end

Asymmetric Key Rotation

Publish both old and new public keys simultaneously in your JWKS endpoint during rotation:

{
  "keys": [
    { "kid": "key-v1", "kty": "RSA", "use": "sig", ... },
    { "kid": "key-v2", "kty": "RSA", "use": "sig", ... }
  ]
}

Rotation steps:

  1. Generate a new key pair with a new kid
  2. Add the new public key to your JWKS endpoint
  3. Update your application to use the new private key
  4. Remove the old public key from JWKS after a grace period (allow in-flight tokens to expire)

Scope Minimization

Request only the scopes your application needs. Broad wildcard scopes increase the impact of a compromised token.

# ✅ Request specific resource types
scopes: ['patient/Patient.read', 'patient/Observation.read']

# ❌ Avoid unless truly necessary
scopes: ['patient/*.*']

You can also reduce scopes at refresh time:

client.refresh_token(
  refresh_token: session[:refresh_token],
  scopes: ['patient/Patient.read'] # Must be a subset of the original grant
)

See also: Configuration Guide for ssl_options and log_http settings that affect security behaviour.


Back to Top ↑

This site uses Just the Docs, a documentation theme for Jekyll.