1. SMART on FHIR の概要

###1.1.スマートとは何ですか?
FHIR の SMART (代替医療アプリケーション、再利用可能な技術) は、サードパーティ アプリケーションが FHIR API を通じて医療データに安全にアクセスできるようにするオープン スタンダードです。スマートな定義:
- アプリ起動フレームワーク: アプリが EHR またはスタンドアロンから起動される方法
- 承認スコープ: FHIR リソースに対する詳細な権限
- 起動コンテキスト: アプリに渡される患者、遭遇、位置コンテキスト
- バックエンド サービス: サービス間の承認にはユーザーの操作は必要ありません
###1.2. SMART アプリの起動フロー
EHR 起動フロー:
- ユーザーが EHR で「アプリの起動」をクリックします。
- EHR はコンテキスト (患者 ID、エンカウンター ID) を含む起動リクエストを送信します。
- アプリは認可サーバー (Keycloak) にリダイレクトします
- ユーザー認証 (または SSO)
- KeycloakはSMARTスコープを使用してアクセストークンを発行します
- アプリはトークンを使用して FHIR リソースにアクセスします
スタンドアロンの起動フロー:
- ユーザーがアプリを直接開きます (例: モバイルアプリ)
- アプリは認証のために Keycloak にリダイレクトします
- ユーザーは資格情報 + MFA を使用して認証します。
- アプリは SMART スコープをリクエストします
- 患者のコンテキストが必要な場合 → 患者ピッカー
- Keycloakがアクセストークンを発行する
- アプリが FHIR リソースにアクセスする
バックエンド サービスの承認:
- サービスは署名付き JWT アサーションで認証します
- KeycloakはJWTを検証し、アクセストークンを発行します
- サービスは FHIR リソースにアクセスします → ユーザーの操作は必要ありません。用途: データ同期、分析、レポート作成。
2. SMART スコープ
###2.1. FHIR リソースのスコープ
Format: <context>/<resource>.<permission>
Context:
patient → Access linked to a specific patient
user → Access linked to the current user
system → Full system-level access (backend services)
Resource:
Patient, Observation, MedicationRequest, DiagnosticReport,
Condition, Encounter, AllergyIntolerance, Immunization, etc.
Permission:
read → Search and read resources
write → Create, update, delete resources
* → All permissions
###2.2.ヘルスケアの範囲の例
# Bác sĩ: Truy cập hồ sơ bệnh nhân trong context đang khám
user/Patient.read
user/Observation.read
user/Observation.write # Vital signs, clinical observations
user/Condition.read
user/Condition.write # Diagnoses
user/MedicationRequest.write # Prescriptions
user/DiagnosticReport.read # Lab results
user/AllergyIntolerance.read
# Bệnh nhân: Truy cập hồ sơ của chính mình
patient/Patient.read
patient/Observation.read
patient/MedicationRequest.read
patient/DiagnosticReport.read
patient/AllergyIntolerance.read
patient/Immunization.read
# Backend Service: Sync lab results
system/DiagnosticReport.write
system/Observation.write
system/ServiceRequest.read
# Ứng dụng nghiên cứu (de-identified data)
system/Patient.read?_elements=gender,birthDate,address.state
system/Condition.read
system/Observation.read
###2.3. SMART の Keycloak クライアント スコープ
{
"clientScopes": [
{
"name": "patient/*.read",
"description": "Read all FHIR resources for a specific patient",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"consent.screen.text": "Đọc hồ sơ bệnh án của bạn"
},
"protocolMappers": [
{
"name": "smart-patient-scope",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"config": {
"claim.name": "scope",
"claim.value": "patient/*.read",
"access.token.claim": "true"
}
}
]
},
{
"name": "launch/patient",
"description": "EHR launch with patient context",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true"
},
"protocolMappers": [
{
"name": "patient-launch-context",
"protocol": "openid-connect",
"protocolMapper": "oidc-usersessionmodel-note-mapper",
"config": {
"user.session.note": "patient_id",
"claim.name": "patient",
"access.token.claim": "true"
}
}
]
},
{
"name": "fhirUser",
"description": "FHIR User identity claim",
"protocol": "openid-connect",
"protocolMappers": [
{
"name": "fhir-user-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"config": {
"claim.name": "fhirUser",
"claim.value": "",
"access.token.claim": "true",
"id.token.claim": "true"
}
}
]
}
]
}
3. Quarkus を使用した FHIR での SMART
###3.1. FHIR リソース サーバーの実装
@Path("/fhir/r4")
@Authenticated
@Produces("application/fhir+json")
public class FhirPatientResource {
@Inject
SecurityIdentity identity;
@Inject
PatientRepository patientRepository;
@Inject
SmartScopeValidator scopeValidator;
@GET
@Path("/Patient/{id}")
public Response readPatient(@PathParam("id") String id) {
// Validate SMART scopes
scopeValidator.requireScope(identity, "patient/Patient.read",
"user/Patient.read");
// If patient context → verify patient match
String launchPatient = identity.getAttribute("patient");
if (launchPatient != null && !launchPatient.equals(id)) {
if (scopeValidator.hasOnlyPatientScopes(identity)) {
throw new ForbiddenException(
"Patient scope restricted to patient: " + launchPatient);
}
}
Patient patient = patientRepository.findById(id);
if (patient == null) {
return Response.status(404).entity(
createOperationOutcome("Patient not found")).build();
}
return Response.ok(patient.toFhirJson()).build();
}
@GET
@Path("/Patient")
public Response searchPatients(
@QueryParam("name") String name,
@QueryParam("birthdate") String birthdate,
@QueryParam("identifier") String identifier,
@QueryParam("_count") @DefaultValue("20") int count) {
scopeValidator.requireScope(identity,
"patient/Patient.read", "user/Patient.read", "system/Patient.read");
// Apply patient context restriction
SearchParams params = SearchParams.builder()
.name(name)
.birthdate(birthdate)
.identifier(identifier)
.count(Math.min(count, 100)) // Cap results
.patientContext(identity.getAttribute("patient"))
.build();
Bundle bundle = patientRepository.search(params);
return Response.ok(bundle.toFhirJson()).build();
}
}
###3.2. SMART スコープバリデーター
@ApplicationScoped
public class SmartScopeValidator {
public void requireScope(SecurityIdentity identity, String... anyOfScopes) {
Set<String> grantedScopes = getSmartScopes(identity);
boolean hasScope = Arrays.stream(anyOfScopes)
.anyMatch(scope -> matchScope(grantedScopes, scope));
if (!hasScope) {
throw new ForbiddenException(
"Insufficient SMART scope. Required one of: " +
String.join(", ", anyOfScopes));
}
}
public boolean hasOnlyPatientScopes(SecurityIdentity identity) {
return getSmartScopes(identity).stream()
.allMatch(s -> s.startsWith("patient/") || s.equals("openid") ||
s.equals("fhirUser") || s.startsWith("launch/"));
}
private boolean matchScope(Set<String> granted, String required) {
// Direct match
if (granted.contains(required)) return true;
// Wildcard match: patient/*.read matches patient/Patient.read
String[] parts = required.split("[/.]");
if (parts.length == 3) {
String wildcardScope = parts[0] + "/*." + parts[2];
return granted.contains(wildcardScope);
}
return false;
}
private Set<String> getSmartScopes(SecurityIdentity identity) {
// Extract scopes from JWT claim
JsonArray scopeClaim = identity.getAttribute("scope");
if (scopeClaim != null) {
return scopeClaim.stream()
.map(JsonValue::toString)
.collect(Collectors.toSet());
}
return Set.of();
}
}
###3.3. SMART のよく知られた構成
@Path("/.well-known/smart-configuration")
public class SmartConfigurationResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getSmartConfiguration() {
return Response.ok(Map.of(
"authorization_endpoint",
"https://keycloak.hospital.vn/realms/healthcare/protocol/openid-connect/auth",
"token_endpoint",
"https://keycloak.hospital.vn/realms/healthcare/protocol/openid-connect/token",
"introspection_endpoint",
"https://keycloak.hospital.vn/realms/healthcare/protocol/openid-connect/token/introspect",
"revocation_endpoint",
"https://keycloak.hospital.vn/realms/healthcare/protocol/openid-connect/revoke",
"capabilities", List.of(
"launch-ehr",
"launch-standalone",
"client-public",
"client-confidential-symmetric",
"sso-openid-connect",
"context-passthrough-banner",
"context-passthrough-style",
"context-ehr-patient",
"context-ehr-encounter",
"context-standalone-patient",
"permission-offline",
"permission-patient",
"permission-user",
"authorize-post"
),
"scopes_supported", List.of(
"openid", "fhirUser", "launch", "launch/patient",
"patient/*.read", "patient/*.write",
"user/*.read", "user/*.write",
"system/*.read", "system/*.write"
),
"response_types_supported", List.of("code"),
"code_challenge_methods_supported", List.of("S256"),
"token_endpoint_auth_methods_supported", List.of(
"client_secret_basic", "client_secret_post", "private_key_jwt"
)
)).build();
}
}
4. FHIR 同意リソース
###4.1.患者の同意管理
{
"resourceType": "Consent",
"id": "consent-001",
"status": "active",
"scope": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/consentscope",
"code": "patient-privacy"
}]
},
"category": [{
"coding": [{
"system": "http://loinc.org",
"code": "59284-0",
"display": "Patient Consent"
}]
}],
"patient": {
"reference": "Patient/P-001"
},
"dateTime": "2026-04-01T10:00:00+07:00",
"provision": {
"type": "permit",
"period": {
"start": "2026-04-01",
"end": "2027-04-01"
},
"actor": [{
"role": {
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/v3-ParticipationType",
"code": "PRCP",
"display": "Primary Care Provider"
}]
},
"reference": {
"reference": "Organization/BV-cho-ray"
}
}],
"action": [{
"coding": [{
"system": "http://terminology.hl7.org/CodeSystem/consentaction",
"code": "access"
}]
}],
"purpose": [{
"system": "http://terminology.hl7.org/CodeSystem/v3-ActReason",
"code": "TREAT",
"display": "Treatment"
}]
}
}
5. バックエンドサービスの認可
###5.1.署名付き JWT によるサービス認証
// Backend service authenticates with JWT assertion
@ApplicationScoped
public class FhirBackendServiceClient {
@ConfigProperty(name = "fhir.service.private-key-path")
String privateKeyPath;
@ConfigProperty(name = "fhir.service.client-id")
String clientId;
@ConfigProperty(name = "fhir.keycloak.token-url")
String tokenUrl;
public String getAccessToken() {
// Create signed JWT assertion (RFC 7523)
PrivateKey key = loadPrivateKey(privateKeyPath);
String jwt = Jwt.issuer(clientId)
.subject(clientId)
.audience(tokenUrl)
.expiresIn(Duration.ofMinutes(5))
.jws().keyId("service-key-1")
.sign(key);
// Exchange JWT for access token
return WebClient.create(tokenUrl)
.post()
.sendForm(MultiMap.caseInsensitiveMultiMap()
.add("grant_type", "client_credentials")
.add("client_assertion_type",
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
.add("client_assertion", jwt)
.add("scope", "system/*.read system/*.write"))
.await()
.bodyAsJsonObject()
.getString("access_token");
}
}
6. まとめ
このレッスンでは次のことを行います。
- SMART on FHIR フレームワークと 3 つの起動フローを理解する
- 医療リソースの SMART スコープを設計する
- Keycloak クライアント スコープを SMART 用に構成する
- スコープ検証を使用して FHIR リソース サーバー を Quarkus に実装する
- SMART Well-Known Configuration エンドポイントを構築する
- FHIR 同意リソースを使用して 患者同意 管理を実装する
- 署名付き JWT を使用して バックエンド サービス承認 を実装する
演習
- 患者コンテキストを使用して SMART EHR 起動用に Keycloak クライアントを構成する
- SMART スコープ検証を備えた FHIR 観測リソース サーバーを実装する
- SMART Well-Known 構成エンドポイントを作成する
- EHR 起動フローの統合テストを作成する
| ◀ 前の記事 | 次の記事 ▶ |
|---|---|
| レッスン 6: RBAC と ABAC - 医師、看護師、患者の分散化 | レッスン 8: MFA、パスキー、医療スタッフの緊急アクセス |