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

Lesson 8: MFA, Passkeys & Emergency Access for Healthcare Workers

Deploy Multi-Factor Authentication suitable for medical environments: TOTP/HOTP for doctors, WebAuthn/Passkeys for workstations, proximity badge authentication, conditional MFA (skip MFA in the internal network), Emergency Access (break-the-glass) procedure with audit trail, and Session Management for shared workstations in hospitals.

🏗️ Architecture — Lesson 8 Lesson 8: MFA, Passkeys & Emergency Access for Medical Staff

Building a Microservices Healthcare System — Quarkus, PostgreSQL, Keycloak with HIPAA standards

Part 2: Identity & Access Management with Keycloak

xdev.asia

1. MFA Strategy for Healthcare Environment

MFA matrix for healthcare workers — Passkeys, TOTP, Emergency Access

1.1. MFA Challenge in Hospitals

Hospitals have a special operating environment compared to regular businesses:

ChallengeDescriptionSolution
Shared workstationsMultiple doctors/nurses share a computerFast switching, tap-and-go
Response timeFirst aid needs access in secondsConditional MFA, proximity badge
Medical glovesCannot use fingerprintWebAuthn security key, PIN
Multiple logins/dayDoctors log in 30-50 times/daySSO + short MFA memory
Limited personal equipmentNot everyone has a smartphoneHardware token options

1.2. MFA Factor Matrix

User TypePrimarySecondaryFallback
DoctorPasskey / WebAuthnTOTP AppRecovery codes
NurseProximity BadgePIN (6 digits)TOTP App
Lab TechTOTP AppSecurity KeyRecovery codes
AdminSecurity KeyTOTP AppAdmin recovery procedure
Patient PortalSMS OTPEmail OTPSupport call

2. Keycloak Authentication Flow for Healthcare

2.1. Conditional Authentication Flow

{
  "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 Sub-flow

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 Conditional OTP Configuration

{
  "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 / Passkeys for Healthcare

3.1. Keycloak WebAuthn Configuration

{
  "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. Supported Authenticators for Hospitals

┌─────────────────────────────────────────────────────┐
│           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. Session Management for Shared Workstations

4.1. Fast Session Switching

// 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. Auto-Logoff Configuration

# 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. Proximity Badge Authentication

5.1. Architecture

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

5.2. Badge Authentication 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. Recovery Procedures

6.1. Account Recovery flow for Healthcare

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

In this lesson, we have:

  • Analyze specific MFA challenges in the hospital environment
  • Design an appropriate MFA strategy for each type of medical staff
  • Configure Conditional Authentication Flow in Keycloak (skip MFA on internal network)
  • Deploy WebAuthn/Passkeys for clinical workstations
  • Build Fast Session Switching for shared workstations
  • Implement Proximity Badge Authentication SPI
  • Design Recovery Procedures for healthcare

Exercises

  1. Configure Keycloak authentication flow with conditional MFA (internal vs external network)
  2. Register WebAuthn security key and test login passkey
  3. Implement auto-logoff after 10 minutes of idle time for clinical workstations
  4. Write documentation for Emergency Access procedure


◀ Previous articleNext article ▶
Lesson 7: SMART on FHIR - OAuth2/OIDC for Healthcare APIsLesson 9: PostgreSQL Security Hardening - Comprehensive Security Configuration