Diagrama de arquitectura de seguridad SMART on FHIR con flujo OAuth 2.0 PKCE, tokens JWT y scopes clínicos granulares para hospitales
Salud Digital

Seguridad en SMART on FHIR y OAuth 2.0: Arquitectura de Autorización Clínica para Hospitales y Clínicas

Guía técnica integral sobre la implementación de SMART on FHIR v2 y OAuth 2.0 en infraestructuras hospitalarias. Análisis de flujos PKCE, validación de tokens JWT y scopes clínicos granulares.

La apertura de los sistemas de historia clínica electrónica (EHR/EMR) a través de APIs REST bajo el estándar HL7 FHIR R4 representa el mayor salto en interoperabilidad sanitaria de la última década. En Colombia, los mandatos de la Ley 2015 de 2020 (Historia Clínica Electrónica Interoperable) y las resoluciones de MinSalud exigen que hospitales, clínicas (IPS), aseguradoras (EPS) y plataformas Healthtech intercambien datos clínicos sin fricciones.

Sin embargo, abrir un endpoint FHIR sin un modelo de control de acceso riguroso equivale a exponer los datos más sensibles y confidenciales de los ciudadanos a vulnerabilidades catastróficas. Los registros médicos electrónicos (PHI - Protected Health Information) no son simples registros transaccionales: su filtración no solo acarrea sanciones regulatorias severas por violación del Habeas Data (Ley 1581 de 2012 en Colombia y HIPAA en EE. UU.), sino que compromete la intimidad y la vida de los pacientes.

Para resolver este desafío nace SMART on FHIR (Substitutable Medical Applications, Reusable Technologies). SMART define el perfil de seguridad y autorización estándar sobre HL7 FHIR utilizando OAuth 2.0, OpenID Connect (OIDC) y JSON Web Tokens (JWT) con scopes clínicos granulares.

En este artículo técnico para arquitectos de software, CISOs e ingenieros de salud digital, profundizaremos en la arquitectura de seguridad de SMART on FHIR v2, sus flujos de autenticación y cómo implementar validación criptográfica en producción.


1. El Desafío del Control de Acceso Clínico

A diferencia de un sistema comercial tradicional donde un usuario tiene un rol genérico (como Admin o User), en un entorno hospitalario el acceso a los datos de salud depende de contextos dinámicos y relaciones clínicas activas:

  1. Principio de Privilegio Mínimo (Need-to-Know): Un cardiólogo de turno necesita acceso inmediato a los electrocardiogramas y medicamentos del paciente que acaba de ingresar a urgencias, pero no debe tener acceso a las notas psicoterapéuticas de pacientes de otra sala.
  2. Aplicaciones de Terceros (Ecosistema de Apps Médicas): Una aplicación móvil para pacientes con diabetes solo debe leer observaciones de glucosa (Observation) y prescripciones de insulina (MedicationRequest), sin capacidad de modificar diagnósticos (Condition) ni consultar datos de otros pacientes.
  3. Múltiples Contextos de Lanzamiento: La aplicación puede abrirse integrada dentro del software de historia clínica del médico (EHR Launch) o de manera independiente por el paciente en su teléfono móvil (Standalone Launch).

SMART on FHIR estandariza cómo un servidor FHIR delega la autenticación y la autorización a un Servidor de Autorización OAuth 2.0, permitiendo desacoplar la lógica de seguridad del motor de base de datos clínica.


2. Flujos de SMART on FHIR: EHR Launch vs. Standalone Launch

El marco SMART define dos mecanismos principales de inicio de sesión:

┌────────────────────────────────────────────────────────────────────────┐
│                        SMART EHR Launch Flow                           │
└────────────────────────────────────────────────────────────────────────┘

 [Médico en EHR] ──► Selecciona Paciente ──► Clic en "Abrir App SMART"

         ▼ (1) Redirección con launch context
   [Navegador / App] ──► Servidor Autorización (/authorize?launch=xyz&...)

         ▼ (2) Autenticación SSO y Consentimiento
   [Servidor Auth] ──► Emite Código de Autorización Temporal (Code)

         ▼ (3) Intercambio de Código por Tokens (PKCE + Secret)
   [Navegador / App] ──► POST /token

         ▼ (4) Respuesta con Access Token + Contexto del Paciente:
   {
     "access_token": "eyJhbGciOiJSUzI1NiIs...",
     "token_type": "Bearer",
     "expires_in": 3600,
     "scope": "patient/Observation.read patient/MedicationRequest.read",
     "patient": "778942-col",
     "encounter": "enc-9921"
   }

         ▼ (5) Consulta al Servidor FHIR
   [App] ──► GET /Observation?patient=778942-col
             Header: Authorization: Bearer eyJhbG...

Diferencias Clave entre Flujos

AspectoSMART EHR LaunchSMART Standalone Launch
Punto de PartidaDentro de la interfaz clínica del EHR hospitalarioNavegador web o app móvil nativa iniciada por el usuario
Contexto InicialEl EHR envía un parámetro launch opaco con el paciente y encuentro actualNo hay contexto previo; el usuario debe autenticarse y seleccionar el paciente si tiene permisos
Casos de UsoCalculadoras de riesgo cardiovascular integradas, visualizadores DICOMPortales de pacientes, apps de monitoreo remoto en el hogar
Seguridad de ClienteConfidencial (Servidor a Servidor) o Público con PKCETípicamente Cliente Público (Single Page App o Mobile) con PKCE obligatorio

3. Scopes Clínicos Granulares en SMART v2

El estándar SMART v2 (alineado con FHIR R4) introdujo un lenguaje de scopes expresivo y formalizado que permite a los sistemas hospitalarios restringir el acceso con precisión quirúrgica. La sintaxis sigue el patrón:

[actor]/[tipo-recurso].[permiso]?[filtro-opcional]

1. Actores (Actor)

  • patient/: Limita el alcance estrictamente a los recursos clínicos del paciente que ha iniciado sesión o que fue seleccionado en el contexto.
  • user/: Concede acceso a los recursos a los que el usuario clínico actual (médico, enfermera) tiene permiso para ver en todo el hospital.
  • system/: Utilizado en comunicaciones backend-to-backend sin intervención de un usuario interactivo (ej. sincronización nocturna de RIPS o facturación electrónica).

2. Tipos de Recurso FHIR

  • Cualquier recurso FHIR válido: Patient, Observation, Condition, Encounter, MedicationRequest, DiagnosticReport, etc.
  • Comodín global: * (aplica a todos los tipos de recursos del servidor).

3. Niveles de Permiso

  • read: Permite métodos HTTP GET y operaciones de búsqueda _search.
  • write: Permite creación (POST), actualización (PUT) y eliminación (DELETE).
  • cruds: Notación granular de SMART v2 (c: create, r: read, u: update, d: delete, s: search).

Ejemplos Reales de Scopes para Producción:

  • patient/Observation.rs: Permite leer y buscar observaciones clínicas (ej. signos vitales, laboratorios) únicamente del paciente en sesión.
  • patient/*.read: Permite lectura completa de todo el expediente clínico del paciente activo.
  • system/Patient.c: Permite a un sistema externo registrar nuevos pacientes sin concederle permisos de lectura masiva de la base de datos hospitalaria.

4. Implementación de Middleware de Seguridad en Node.js / TypeScript

A continuación, implementamos un middleware de producción para Express/Node.js que valida tokens de acceso JWT emitidos por un servidor de identidad SMART on FHIR (Keycloak, Auth0 o SMART Identity Server) utilizando el conjunto de claves públicas JWKS (.well-known/jwks.json).

import { Request, Response, NextFunction } from 'express';
import { createRemoteJWKSet, jwtVerify, JWTVerifyResult } from 'jose';

// Interfaz para el payload de un token SMART on FHIR
interface SmartJwtPayload {
  iss: string; // Issuer del servidor OAuth2
  sub: string; // Subject (ID del usuario o aplicación)
  aud: string; // Audience (URL del servidor FHIR)
  exp: number; // Timestamp de expiración
  scope: string; // Scopes concedidos separados por espacio
  patient?: string; // ID del paciente en contexto
  encounter?: string; // ID del encuentro clínico en contexto
  fhirUser?: string; // URL del recurso Practitioner o Patient del usuario
}

// Extensión de la Request de Express
export interface AuthenticatedFhirRequest extends Request {
  authContext?: {
    payload: SmartJwtPayload;
    allowedScopes: string[];
    patientId?: string;
  };
}

// Configuración del validador JWKS remoto
const AUTH_SERVER_ISSUER = process.env.FHIR_AUTH_ISSUER || 'https://auth.hospital.com/oauth2';
const FHIR_SERVER_URL = process.env.FHIR_BASE_URL || 'https://fhir.hospital.com/r4';
const JWKS_URI = new URL(`${AUTH_SERVER_ISSUER}/.well-known/jwks.json`);

const JWKS = createRemoteJWKSet(JWKS_URI, {
  cacheMaxAge: 600000, // Caché de claves públicas por 10 minutos
  cooldownDuration: 30000,
});

/**
 * Middleware para validar el Bearer Token de SMART on FHIR
 */
export async function smartAuthMiddleware(
  req: AuthenticatedFhirRequest,
  res: Response,
  next: NextFunction
) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      resourceType: 'OperationOutcome',
      issue: [{
        severity: 'error',
        code: 'login',
        diagnostics: 'Missing or malformed Authorization header with Bearer token'
      }]
    });
  }

  const token = authHeader.split(' ')[1];

  try {
    // 1. Verificación criptográfica de firma (RS256/ES256), caducidad, issuer y audience
    const verifyResult: JWTVerifyResult = await jwtVerify(token, JWKS, {
      issuer: AUTH_SERVER_ISSUER,
      audience: FHIR_SERVER_URL,
      clockTolerance: '60s',
    });

    const payload = verifyResult.payload as unknown as SmartJwtPayload;

    if (!payload.scope) {
      return res.status(403).json({
        resourceType: 'OperationOutcome',
        issue: [{
          severity: 'error',
          code: 'forbidden',
          diagnostics: 'Token does not contain SMART on FHIR scopes claim'
        }]
      });
    }

    const scopes = payload.scope.split(' ');

    // 2. Inyectar contexto validado en la petición
    req.authContext = {
      payload,
      allowedScopes: scopes,
      patientId: payload.patient,
    };

    next();
  } catch (error: any) {
    return res.status(401).json({
      resourceType: 'OperationOutcome',
      issue: [{
        severity: 'error',
        code: 'security',
        diagnostics: `Token verification failed: ${error.message}`
      }]
    });
  }
}

/**
 * Guard para verificar permisos sobre un recurso FHIR específico
 * Ejemplo: requireClinicalScope('Observation', 'read')
 */
export function requireClinicalScope(resourceType: string, action: 'read' | 'write') {
  return (req: AuthenticatedFhirRequest, res: Response, next: NextFunction) => {
    const auth = req.authContext;
    if (!auth) {
      return res.status(401).end();
    }

    const requestedPatientId = req.query.patient || req.params.patientId;

    // Validación de alcance: Comprobar scopes compatibles
    const hasValidScope = auth.allowedScopes.some(scope => {
      // Casos de scopes globales (médicos o sistemas)
      if (scope === `system/${resourceType}.${action}` || scope === `system/*.${action}`) return true;
      if (scope === `user/${resourceType}.${action}` || scope === `user/*.${action}`) return true;
      if (scope === `system/${resourceType}.*` || scope === `user/*.read`) return true;

      // Casos de scope orientado a paciente
      if (scope === `patient/${resourceType}.${action}` || scope === `patient/*.${action}` || scope === `patient/${resourceType}.read`) {
        // Obligatorio: Si el scope es de paciente, la petición DEBE estar restringida a su propio ID
        if (auth.patientId && requestedPatientId && auth.patientId === requestedPatientId) {
          return true;
        }
      }

      return false;
    });

    if (!hasValidScope) {
      return res.status(403).json({
        resourceType: 'OperationOutcome',
        issue: [{
          severity: 'error',
          code: 'forbidden',
          diagnostics: `Insufficient privileges for ${action} operation on ${resourceType}`
        }]
      });
    }

    next();
  };
}

5. El Manifiesto de Capacidades: .well-known/smart-configuration

Un servidor compatible con SMART on FHIR debe publicar su documento de descubrimiento en la ruta estándar /.well-known/smart-configuration. Esto permite a cualquier aplicación cliente descubrir dinámicamente los endpoints de autorización y los métodos de autenticación soportados:

{
  "issuer": "https://auth.hospital.com/oauth2",
  "authorization_endpoint": "https://auth.hospital.com/oauth2/authorize",
  "token_endpoint": "https://auth.hospital.com/oauth2/token",
  "jwks_uri": "https://auth.hospital.com/oauth2/.well-known/jwks.json",
  "grant_types_supported": ["authorization_code", "client_credentials"],
  "response_types_supported": ["code"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "private_key_jwt"],
  "scopes_supported": [
    "openid",
    "profile",
    "fhirUser",
    "launch",
    "launch/patient",
    "patient/*.read",
    "patient/Observation.read",
    "patient/MedicationRequest.read",
    "user/*.read"
  ],
  "capabilities": [
    "launch-ehr",
    "launch-standalone",
    "client-public",
    "client-confidential-symmetric",
    "context-passthrough-banner",
    "permission-patient",
    "permission-user"
  ],
  "code_challenge_methods_supported": ["S256"]
}

6. PKCE (RFC 7636) y la Protección contra Intercepción de Código

Para aplicaciones cliente públicas (como aplicaciones móviles desarrolladas en Flutter/React Native o aplicaciones web de una sola página en React/Vue/Angular), almacenar un client_secret en el código fuente es una vulnerabilidad crítica. Cualquier usuario técnico puede descompilar el APK o abrir la consola de desarrollo del navegador para extraer el secreto.

SMART on FHIR v2 hace obligatorio el uso de PKCE (Proof Key for Code Exchange) con algoritmo S256:

  1. El cliente genera un secreto criptográfico aleatorio de alta entropía llamado code_verifier.
  2. Calcula el hash SHA-256 de dicho verificador y lo codifica en Base64URL: code_challenge = BASE64URL(SHA256(code_verifier)).
  3. Al redirigir al usuario al endpoint /authorize, envía el code_challenge y code_challenge_method=S256.
  4. El servidor de autorización almacena el reto junto con el código emitido.
  5. Al solicitar los tokens en el endpoint /token, el cliente envía el code_verifier original. El servidor aplica la función hash y comprueba que coincida con el reto almacenado.

Con PKCE, incluso si un atacante en un dispositivo comprometido intercepta el código de autorización temporal, no podrá intercambiarlo por el Access Token porque desconoce el code_verifier original.


7. Pruebas de Integración con cURL: Inspección de Tokens

A continuación se muestra cómo un cliente médico intercambia el código por tokens y consulta un recurso clínico de forma segura:

# 1. Intercambio de Authorization Code con PKCE en el Token Endpoint
curl -X POST "https://auth.hospital.com/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=spl_a89f92bdc1" \
  -d "redirect_uri=https://app-medica.com/callback" \
  -d "client_id=clinica-portal-web" \
  -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

# Respuesta del servidor (incluye patient context)
# {"access_token":"eyJhbGci...", "token_type":"Bearer", "patient":"pat-9921", "expires_in":3600}

# 2. Petición autenticada al servidor FHIR para consultar signos vitales
curl -X GET "https://fhir.hospital.com/r4/Observation?patient=pat-9921&category=vital-signs" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  -H "Accept: application/fhir+json"

Si el token enviado no contiene el scope patient/Observation.read o si el token intenta consultar observaciones de un paciente distinto a pat-9921, el servidor responde inmediatamente con un código HTTP 403 Forbidden y un recurso OperationOutcome estructurado.


8. Consultoría de Interoperabilidad FHIR HL7 con DoneAPI

Diseñar y desplegar una arquitectura SMART on FHIR en una institución de salud requiere armonizar tres mundos altamente especializados: estándares clínicos HL7, ciberseguridad con OAuth2/JWT y cumplimiento normativo local (Resolución 866/2275, RIPS y Ley 2015 en Colombia).

En DoneAPI ayudamos a hospitales, clínicas, laboratorios clínicos y empresas Healthtech en América Latina y Norteamérica a:

  • Auditar y Asegurar Endpoints FHIR: Blindaje de APIs clínicas frente al Top 10 de OWASP para APIs.
  • Implementar Servidores SMART on FHIR: Integración de Keycloak u Okta con servidores HAPI FHIR, Microsoft Health Data Services o motores propietarios.
  • Transformación de RIPS JSON a Recursos FHIR: Mapeo automático de atenciones médicas y procedimientos a perfiles oficiales.
  • Certificación de Interoperabilidad ante Entes Reguladores: Asesoría técnica integral para superar auditorías ministeriales.

💬 ¿Tu clínica o empresa de software de salud necesita habilitar intercambio de datos clínicos seguro o certificar su estándar FHIR?
Comunícate directamente con nuestro equipo de ingenieros senior por WhatsApp para coordinar una sesión de diagnóstico técnico.

Consultoría Especializada en SMART on FHIR y Ciberseguridad Clínica

Protege tus datos sanitarios bajo estándares mundiales y cumple la normativa colombiana de Historia Clínica Interoperable.

Agendar Consulta Técnica por WhatsApp

9. Conclusión

La interoperabilidad clínica no puede existir a expensas de la seguridad. SMART on FHIR proporciona un modelo matemático y criptográfico robusto que protege la privacidad de los pacientes sin sacrificar la agilidad en la entrega de datos a los profesionales de la salud.

Al adoptar flujos OAuth 2.0 con PKCE, validar firmas JWT contra endpoints JWKS y exigir scopes granulares basados en el contexto clínico real, las instituciones de salud pueden abrir sus ecosistemas digitales con absoluta certeza técnica y pleno apego a la ley.

Herramientas de Inteligencia Artificial para emprendedores

Desbloquea tu arsenal de automatización.

Regístrate gratis y accede a plantillas para n8n y Make.com, packs de prompts probados para IA, y guías exclusivas diseñadas para escalar tu negocio digital.

Crear cuenta y obtén recursos gratis