---
title: "Seguridad en SMART on FHIR y OAuth 2.0: Arquitectura de Autorización Clínica para Hospitales y Clínicas"
description: "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."
date: 2026-08-30
category: "Salud Digital"
imageUrl: "/assets/images/blog/seguridad-smart-on-fhir-oauth2.webp"
imageAlt: "Diagrama de arquitectura de seguridad SMART on FHIR con flujo OAuth 2.0 PKCE, tokens JWT y scopes clínicos granulares para hospitales"
readTime: "12 min de lectura"
author: "DoneAPI Engineering Team"
tags: ["FHIR", "HL7", "SMART on FHIR", "OAuth2", "Ciberseguridad", "Salud Digital", "MinSalud"]
lang: "es"
translationSlug: "smart-on-fhir-security-oauth2-hospitals-guide"
featured: false
---

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

| Aspecto | SMART EHR Launch | SMART Standalone Launch |
| :--- | :--- | :--- |
| **Punto de Partida** | Dentro de la interfaz clínica del EHR hospitalario | Navegador web o app móvil nativa iniciada por el usuario |
| **Contexto Inicial** | El EHR envía un parámetro `launch` opaco con el paciente y encuentro actual | No hay contexto previo; el usuario debe autenticarse y seleccionar el paciente si tiene permisos |
| **Casos de Uso** | Calculadoras de riesgo cardiovascular integradas, visualizadores DICOM | Portales de pacientes, apps de monitoreo remoto en el hogar |
| **Seguridad de Cliente** | Confidencial (Servidor a Servidor) o Público con PKCE | Tí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`).

```typescript
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:

```json
{
  "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:

```bash
# 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.

<div class="my-8 p-6 bg-slate-900 border border-teal-500/30 rounded-2xl shadow-xl flex flex-col md:flex-row items-center justify-between gap-6">
  <div>
    <h3 class="text-xl font-bold text-white mb-2">Consultoría Especializada en SMART on FHIR y Ciberseguridad Clínica</h3>
    <p class="text-slate-300 text-sm max-w-xl">Protege tus datos sanitarios bajo estándares mundiales y cumple la normativa colombiana de Historia Clínica Interoperable.</p>
  </div>
  <a href="https://wa.me/573208173939?text=Hola%20DoneAPI,%20quiero%20solicitar%20asesoria%20tecnica%20en%20SMART%20on%20FHIR%20y%20seguridad%20OAuth2%20para%20salud" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 px-6 py-3.5 bg-teal-500 hover:bg-teal-400 text-slate-950 font-bold rounded-xl transition-all shadow-lg hover:shadow-teal-500/25 shrink-0 text-sm">
    <svg class="w-5 h-5 fill-current" viewBox="0 0 24 24"><path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.893 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z"/></svg>
    Agendar Consulta Técnica por WhatsApp
  </a>
</div>

---

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