Chuyển đến nội dung chính

レッスン 8: MFA、パスキー、医療従事者の緊急アクセス

医療環境に適した多要素認証の導入: 医師向けの TOTP/HOTP、ワークステーション向けの WebAuthn/パスキー、近接バッジ認証、条件付き MFA (内部ネットワークの MFA をスキップ)、監査証跡付きの緊急アクセス (ガラスを破る) 手順、病院の共有ワークステーション向けのセッション管理。

🏗️ アーキテクチャ — レッスン 8 レッスン 8: MFA、パスキー、緊急アクセス 医療従事者向け

マイクロサービス ヘルスケア システムの構築 — HIPAA 標準を備えた Quarkus、PostgreSQL、Keycloak

パート 2: Keycloak を使用した ID とアクセス管理

xdev.asia

1. 医療環境のための MFA 戦略

医療従事者向けの MFA マトリックス — パスキー、TOTP、緊急アクセス

###1.1.病院における MFA チャレンジ

病院は通常の企業と比較して特殊な運営環境を持っています。

チャレンジ説明ソリューション
共有ワークステーション複数の医師/看護師がコンピューターを共有する素早い切り替え、タップアンドゴー
応答時間応急処置には数秒以内にアクセスする必要があります条件付き MFA、近接バッジ
医療用手袋指紋は使用できませんWebAuthn セキュリティ キー、PIN
1 日あたりの複数ログイン医師は 1 日に 30 ~ 50 回ログインしますSSO + ショート MFA メモリ
個人用装備は限られています誰もがスマートフォンを持っているわけではありませんハードウェア トークンのオプション

###1.2. MFA 係数マトリックス

ユーザータイププライマリー二次フォールバック
医師パスキー/WebAuthnTOTPアプリリカバリーコード
看護師近接バッジ暗証番号(6桁)TOTPアプリ
ラボテックTOTPアプリセキュリティキーリカバリーコード
管理者セキュリティキーTOTPアプリ管理者の回復手順
患者ポータルSMS OTPEメールOTPサポートコール

2. 医療向けの Keycloak 認証フロー

###2.1.条件付き認証フロー

{
  "alias": "healthcare-browser-flow",
  "description": "Healthcare-specific authentication flow with conditional MFA",
  "providerId": "basic-flow",
  "topLevel": true,
  "builtIn": false,
  "authenticationExecutions": [
    {
      "authenticator": "auth-cookie",
      "requirement": "ALTERNATIVE",
      "priority": 10
    },
    {
      "authenticator": "identity-provider-redirector",
      "requirement": "ALTERNATIVE",
      "priority": 20
    },
    {
      "flowAlias": "healthcare-forms",
      "requirement": "ALTERNATIVE",
      "priority": 30
    }
  ]
}

###2.2.ヘルスケアフォームのサブフロー

healthcare-forms (ALTERNATIVE)
├── Username/Password Form (REQUIRED)
│
├── healthcare-mfa-conditional (CONDITIONAL)
│   ├── Condition: User Role = "patient"
│   │   → SMS OTP Authenticator (REQUIRED)
│   │
│   ├── Condition: IP NOT in 10.0.0.0/8 (external access)
│   │   → WebAuthn Authenticator (REQUIRED)
│   │
│   ├── Condition: User has configured WebAuthn
│   │   → WebAuthn Passwordless (ALTERNATIVE)
│   │
│   └── Condition: Default (internal network)
│       → OTP Form (CONDITIONAL - only if configured)
│
└── healthcare-session-note (REQUIRED)
    → Set session attributes (department, hospital)

###2.3. Keycloakの条件付きOTP構成

{
  "alias": "conditional-mfa-healthcare",
  "description": "MFA required for external access, optional for internal",
  "authenticationExecutions": [
    {
      "authenticator": "conditional-user-configured",
      "requirement": "REQUIRED",
      "priority": 10
    },
    {
      "authenticator": "auth-conditional-otp-form",
      "requirement": "REQUIRED",
      "priority": 20,
      "authenticatorConfig": {
        "alias": "healthcare-otp-config",
        "config": {
          "forceOtpRole": "require_mfa",
          "skipOtpRole": "skip_internal_mfa",
          "noOtpRequiredForHeaderPattern": "",
          "otpControlAttribute": "mfa_required",
          "defaultOtpOutcome": "force"
        }
      }
    }
  ]
}

3. 医療用の WebAuthn / パスキー

###3.1. Keycloak WebAuthn 設定

{
  "webAuthnPolicyRpEntityName": "Healthcare Platform",
  "webAuthnPolicyRpId": "hospital.vn",
  "webAuthnPolicySignatureAlgorithms": ["ES256", "RS256"],
  "webAuthnPolicyAttestationConveyancePreference": "direct",
  "webAuthnPolicyAuthenticatorAttachment": "cross-platform",
  "webAuthnPolicyRequireResidentKey": "Yes",
  "webAuthnPolicyUserVerificationRequirement": "preferred",
  "webAuthnPolicyCreateTimeout": 60,
  "webAuthnPolicyAvoidSameAuthenticatorRegister": true,

  "webAuthnPolicyPasswordlessRpEntityName": "Healthcare Platform",
  "webAuthnPolicyPasswordlessRpId": "hospital.vn",
  "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
  "webAuthnPolicyPasswordlessAuthenticatorAttachment": "platform",
  "webAuthnPolicyPasswordlessRequireResidentKey": "Yes",
  "webAuthnPolicyPasswordlessUserVerificationRequirement": "required"
}

###3.2.病院向けにサポートされている認証システム

┌─────────────────────────────────────────────────────┐
│           Recommended Security Keys                  │
├─────────────────────┬───────────────────────────────┤
│ YubiKey 5 NFC       │ • USB-A/C + NFC              │
│                     │ • FIDO2/WebAuthn + TOTP       │
│                     │ • Ideal for doctors (mobile)  │
├─────────────────────┼───────────────────────────────┤
│ YubiKey 5C Nano     │ • Ultra-compact USB-C         │
│                     │ • Leave in workstation         │
│                     │ • Ideal for nurse stations    │
├─────────────────────┼───────────────────────────────┤
│ Feitian BioPass     │ • Built-in fingerprint        │
│                     │ • USB-A                        │
│                     │ • Ideal for admin              │
├─────────────────────┼───────────────────────────────┤
│ Platform Passkeys   │ • Windows Hello / Touch ID     │
│                     │ • No hardware needed           │
│                     │ • Ideal for personal devices   │
└─────────────────────┴───────────────────────────────┘

4. 共有ワークステーションのセッション管理

###4.1.高速セッション切り替え

// Quarkus endpoint for fast user switching
@Path("/api/v1/session")
public class SessionResource {

    @Inject
    SecurityIdentity identity;

    @POST
    @Path("/switch")
    @PermitAll
    public Response switchUser(@HeaderParam("X-Smart-Card-ID") String smartCardId) {
        // Validate smart card / proximity badge
        if (smartCardId == null || smartCardId.isEmpty()) {
            return Response.status(400)
                .entity(Map.of("error", "Smart card ID required"))
                .build();
        }

        // Redirect to Keycloak with kc_idp_hint for smart card IDP
        URI redirectUri = UriBuilder
            .fromUri("https://keycloak.hospital.vn/realms/healthcare/protocol/openid-connect/auth")
            .queryParam("client_id", "his-web-app")
            .queryParam("response_type", "code")
            .queryParam("scope", "openid")
            .queryParam("kc_idp_hint", "smart-card")
            .queryParam("login_hint", smartCardId)
            .queryParam("prompt", "login")  // Force re-auth
            .build();

        return Response.temporaryRedirect(redirectUri).build();
    }

    @POST
    @Path("/lock")
    @Authenticated
    public Response lockSession() {
        // Lock current session (require re-auth on next access)
        // Audit log: session locked
        auditService.log("SESSION_LOCK", identity.getPrincipal().getName());

        return Response.ok(Map.of(
            "status", "locked",
            "message", "Session locked. Re-authentication required."
        )).build();
    }
}

###4.2.自動ログオフ構成

# application.properties - Session timeouts for healthcare
# Clinical workstations: short idle timeout
quarkus.oidc.token.age=300
quarkus.oidc.token.refresh-token-time-skew=30

# Force re-authentication for sensitive operations
quarkus.oidc.authentication.session-age-extension=0

# Session cookie settings
quarkus.http.auth.session.cookie-name=__Host-healthcare_session
quarkus.http.auth.session.cookie-secure=true
quarkus.http.auth.session.cookie-http-only=true
quarkus.http.auth.session.cookie-same-site=Strict

5. 近接バッジ認証

###5.1.建築

┌──────────┐    BLE/NFC    ┌──────────────┐    HTTPS    ┌──────────┐
│ Badge    │ ────────────▶ │  Badge       │ ──────────▶ │ Keycloak │
│ Reader   │               │  Auth Service│              │          │
└──────────┘               └──────────────┘              └──────────┘
                                  │
                           ┌──────▼──────┐
                           │ Map badge   │
                           │ ID to user  │
                           │ account     │
                           └──────┬──────┘
                                  │
                           ┌──────▼──────┐
                           │ Issue       │
                           │ auth token  │
                           │ (time-      │
                           │ limited)    │
                           └─────────────┘

###5.2.バッジ認証SPI

// Custom Keycloak Authenticator SPI for proximity badge
public class BadgeAuthenticator implements Authenticator {

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        String badgeId = context.getHttpRequest()
            .getHttpHeaders()
            .getHeaderString("X-Badge-ID");

        if (badgeId == null) {
            // Show badge scan prompt
            context.challenge(
                context.form()
                    .setAttribute("realm", context.getRealm())
                    .createForm("badge-scan.ftl")
            );
            return;
        }

        // Look up user by badge ID attribute
        UserModel user = context.getSession().users()
            .searchForUserByUserAttributeStream(
                context.getRealm(), "badgeId", badgeId)
            .findFirst()
            .orElse(null);

        if (user == null) {
            context.failureChallenge(
                AuthenticationFlowError.INVALID_CREDENTIALS,
                context.form().setError("Badge not recognized")
                    .createForm("badge-scan.ftl")
            );
            return;
        }

        // Badge alone is first factor — require PIN as second factor
        context.setUser(user);
        context.success();
    }

    @Override
    public void action(AuthenticationFlowContext context) {
        // Handle badge ID from form submission
        MultivaluedMap<String, String> formData =
            context.getHttpRequest().getDecodedFormParameters();
        String badgeId = formData.getFirst("badgeId");

        if (badgeId != null) {
            context.getHttpRequest().getHttpHeaders()
                .getRequestHeaders().putSingle("X-Badge-ID", badgeId);
            authenticate(context);
        }
    }

    @Override
    public boolean requiresUser() { return false; }

    @Override
    public boolean configuredFor(KeycloakSession session,
            RealmModel realm, UserModel user) { return true; }
}

6. 回復手順

###6.1.ヘルスケアのアカウント回復フロー

Scenario 1: Bác sĩ quên password + mất phone (TOTP)
─────────────────────────────────────────────────────
1. Bác sĩ contact IT Help Desk (phone + employee ID verify)
2. Help Desk officer verifies identity (employee ID, department, manager confirm)
3. Help Desk issues temporary password via sealed envelope / secure channel
4. Doctor logs in → forced password change + MFA re-enrollment
5. Audit: recovery event logged, previous MFA devices revoked

Scenario 2: Emergency access khi Keycloak down
────────────────────────────────────────────────
1. Activate emergency access mode (requires 2 admin approvals)
2. Local authentication fallback (pre-provisioned local accounts)
3. All actions logged to local file → synced when Keycloak recovers
4. Emergency period limited to 4 hours
5. Post-incident: full audit review required

7. まとめ

このレッスンでは次のことを行います。

  • 病院環境における特定の MFA の課題を分析する
  • 医療スタッフの種類ごとに適切な MFA 戦略を設計する
  • Keycloak で 条件付き認証フロー を設定する (内部ネットワークで MFA をスキップ)
  • WebAuthn/パスキーを臨床ワークステーションに導入する
  • 共有ワークステーション用の 高速セッション切り替え を構築する
  • 近接バッジ認証 SPI を実装する
  • 医療向けの回復手順を設計する

演習

  1. 条件付き MFA を使用して Keycloak 認証フローを構成する (内部ネットワークと外部ネットワーク)
  2. WebAuthnセキュリティキーを登録し、ログインパスキーをテストする
  3. 臨床ワークステーションのアイドル時間の 10 分後に自動ログオフを実装する
  4. 緊急アクセス手順の文書を作成する


◀ 前の記事次の記事 ▶
レッスン 7: FHIR の SMART - ヘルスケア API 用の OAuth2/OIDCレッスン 9: PostgreSQL のセキュリティ強化 - 包括的なセキュリティ構成