Skip to content

Patient Impersonation

Overview

Patient impersonation allows administrators to temporarily assume the identity of a patient for support purposes. This feature is critical for customer support scenarios where a patient needs assistance but cannot complete tasks themselves (e.g., technical issues, accessibility needs, language barriers).

What is Impersonation?

Impersonation generates a time-limited session token that grants an admin the ability to act as a specific patient within the system. All actions performed during impersonation are:

  1. Logged to the audit trail with the impersonation_id
  2. Tagged with the original admin's identity for accountability
  3. Limited in duration (max 1 hour, non-renewable)
  4. Rate limited to prevent abuse

How It Works

1. Admin Initiates Impersonation

bash
POST /v1/patients/100/impersonate
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "reason": "Patient unable to complete intake form, assisting via phone"
}

2. System Validates Request

  • Check admin has admin role
  • Validate reason is at least 10 characters
  • Check rate limit (max 3 impersonations per 5 minutes)
  • Verify patient exists and is accessible

3. Generate Impersonation Token

go
impersonationToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "user_id":          patientUserID,
    "impersonated_by":  adminUserID,
    "impersonation_id": impersonationID,
    "exp":              time.Now().Add(1 * time.Hour).Unix(),
    "role":             "patient",
    "organization_id":  orgID,
})

4. Log Impersonation Event

sql
INSERT INTO audit_log (
    organization_id,
    user_id,
    action,
    entity_type,
    entity_id,
    changes,
    action_context
) VALUES (
    1,                      -- organization_id
    1,                      -- admin_user_id
    'IMPERSONATE',
    'patient',
    100,                    -- patient_id
    '{"reason": "..."}'::jsonb,
    'impersonation'
);

5. Return Impersonation Token

json
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user_id": 42,
    "expires_at": "2025-01-15T11:00:00Z",
    "impersonation_id": 17
  }
}

6. Admin Uses Token

All subsequent requests use the impersonation token:

bash
GET /v1/appointments
Authorization: Bearer <impersonation_token>
# Returns appointments for patient (user_id: 42)

PUT /v1/forms/201
Authorization: Bearer <impersonation_token>
{
  "values": {
    "pain_level": "Big pain"
  }
}
# Saves form as if patient filled it out

7. All Actions Tagged in Audit Log

Every action during impersonation is logged with the impersonation_id:

json
{
  "action": "UPDATE",
  "entity_type": "form",
  "entity_id": 201,
  "user_id": 42,              // Patient being impersonated
  "impersonated_by": 1,       // Admin who initiated
  "impersonation_id": 17,     // Links back to original impersonation event
  "changes": {
    "values": {
      "pain_level": "Big pain"
    }
  }
}

API Reference

Initiate Impersonation

Endpoint: POST /v1/patients/{id}/impersonate

Authorization: Admin only

Request:

json
{
  "reason": "Patient unable to complete intake form, assisting via phone"
}

Response: 200 OK

json
{
  "data": {
    "token": "eyJhbGciOi...",
    "user_id": 42,
    "expires_at": "2025-01-15T11:00:00Z",
    "impersonation_id": 17
  }
}

Errors:

  • 400 reason_required - Reason must be at least 10 characters
  • 403 forbidden - Non-admin attempting impersonation
  • 404 not_found - Patient doesn't exist
  • 429 rate_limited - Max 3 impersonations per 5 minutes

Security Considerations

1. Admin-Only Access

Only users with role='admin' can impersonate:

go
if currentUserRole != "admin" {
    return errors.New("forbidden: admin role required")
}

2. Mandatory Reason

Every impersonation requires a reason (min 10 characters) for the audit trail:

go
if len(req.Reason) < 10 {
    return errors.New("reason_required: minimum 10 characters")
}

This ensures accountability and makes it easy to review impersonation events later.

3. Time-Limited Tokens

Tokens expire after 1 hour and cannot be renewed:

go
expiry := time.Now().Add(1 * time.Hour)

After expiry, the admin must initiate a new impersonation session (with a new reason).

4. Rate Limiting

Maximum 3 impersonations per 5 minutes per admin:

go
if impersonationCount > 3 {
    return errors.New("rate_limited: max 3 impersonations per 5 minutes")
}

This prevents abuse and forces admins to be deliberate about impersonation.

5. Full Audit Trail

Every action during impersonation is logged with:

  • user_id - Patient being impersonated
  • impersonated_by - Admin who initiated
  • impersonation_id - Links to original impersonation event

This creates a complete audit trail for compliance reviews.

6. Token Claims

Impersonation tokens include special claims:

json
{
  "user_id": 42,              // Patient's user ID
  "impersonated_by": 1,       // Admin's user ID
  "impersonation_id": 17,     // Audit trail link
  "role": "patient",          // Patient role (not admin)
  "organization_id": 1,
  "exp": 1642525200           // 1 hour expiry
}

The backend can detect impersonation by checking for the impersonated_by claim.


Audit Logging

Impersonation Start Event

json
{
  "id": 1001,
  "organization_id": 1,
  "user_id": 1,                // Admin
  "action": "IMPERSONATE",
  "entity_type": "patient",
  "entity_id": 100,            // Patient
  "changes": {
    "reason": "Patient unable to complete intake form, assisting via phone",
    "impersonation_id": 17,
    "expires_at": "2025-01-15T11:00:00Z"
  },
  "action_context": "impersonation",
  "created_at": "2025-01-15T10:00:00Z"
}

Actions During Impersonation

json
{
  "id": 1002,
  "organization_id": 1,
  "user_id": 42,               // Patient being impersonated
  "action": "UPDATE",
  "entity_type": "form",
  "entity_id": 201,
  "changes": {
    "values": {
      "pain_level": "Big pain"
    }
  },
  "impersonated_by": 1,        // Admin who initiated
  "impersonation_id": 17,      // Links to impersonation start event
  "created_at": "2025-01-15T10:05:00Z"
}

Querying Impersonation Events

Find all impersonation sessions:

sql
SELECT * FROM audit_log
WHERE action = 'IMPERSONATE'
ORDER BY created_at DESC;

Find all actions during a specific impersonation:

sql
SELECT * FROM audit_log
WHERE impersonation_id = 17
ORDER BY created_at;

Find all impersonations by a specific admin:

sql
SELECT * FROM audit_log
WHERE action = 'IMPERSONATE'
  AND user_id = 1  -- Admin user ID
ORDER BY created_at DESC;

Use Cases

1. Technical Support

Scenario: Patient calls support saying "I can't submit the intake form, it keeps failing"

Solution:

  1. Admin: POST /v1/patients/123/impersonate { "reason": "Form submission error, debugging issue" }
  2. Admin navigates to patient's form view
  3. Admin reproduces the issue and identifies the problem
  4. Admin fixes or escalates to engineering

2. Assisted Form Completion

Scenario: Elderly patient calls support saying "I don't know how to fill out this computer form"

Solution:

  1. Admin: POST /v1/patients/123/impersonate { "reason": "Patient unable to complete intake form, assisting via phone" }
  2. Admin guides patient through questions over the phone
  3. Admin fills out form on patient's behalf
  4. Session expires after 1 hour (or admin completes task sooner)

3. Accessibility Assistance

Scenario: Patient with vision impairment needs help booking an appointment

Solution:

  1. Admin: POST /v1/patients/123/impersonate { "reason": "Vision impaired patient needs assistance booking appointment" }
  2. Admin creates appointment on patient's behalf
  3. Admin confirms details with patient over the phone

4. Language Barrier

Scenario: Patient speaks limited English and needs help understanding the appointment system

Solution:

  1. Admin (bilingual support): POST /v1/patients/123/impersonate { "reason": "Non-English speaking patient needs assistance" }
  2. Admin explains the process in patient's language
  3. Admin performs actions on patient's behalf

Best Practices

For Admins

  1. Always provide a clear reason - Make it specific enough that you could explain it 6 months later in a compliance review
  2. Minimize session duration - Don't keep the impersonation token open longer than necessary
  3. Verify patient identity - Confirm you're helping the right patient before impersonating
  4. Document the interaction - Add notes to the patient record about what was done

For Development

  1. Never bypass audit logging - Every action during impersonation MUST be logged
  2. Enforce token expiry - Never allow token renewal - force new impersonation with new reason
  3. Rate limit strictly - Prevent abuse by limiting frequency
  4. Include reason in audit - Store the reason in the audit log for compliance
  5. Visual indicators in UI - Show clear "IMPERSONATING: John Doe" banner

For Compliance

  1. Regular audit reviews - Review impersonation logs monthly
  2. Investigate anomalies - Flag high-frequency impersonators for review
  3. Training - Ensure all admins understand when impersonation is appropriate
  4. Policy documentation - Maintain written policy on acceptable impersonation use cases

Debugging & Support

Check if request is impersonated

go
func isImpersonated(claims jwt.MapClaims) bool {
    _, ok := claims["impersonated_by"]
    return ok
}

func getImpersonationID(claims jwt.MapClaims) *int64 {
    if id, ok := claims["impersonation_id"].(float64); ok {
        id64 := int64(id)
        return &id64
    }
    return nil
}

Extract impersonation context

go
type ImpersonationContext struct {
    PatientUserID   int64
    AdminUserID     int64
    ImpersonationID int64
}

func extractImpersonation(claims jwt.MapClaims) *ImpersonationContext {
    if !isImpersonated(claims) {
        return nil
    }

    return &ImpersonationContext{
        PatientUserID:   int64(claims["user_id"].(float64)),
        AdminUserID:     int64(claims["impersonated_by"].(float64)),
        ImpersonationID: int64(claims["impersonation_id"].(float64)),
    }
}

Audit log helper

go
func logWithImpersonation(ctx context.Context, action string, entityType string, entityID int64, changes map[string]interface{}) error {
    impCtx := extractImpersonation(ctx.Value("claims").(jwt.MapClaims))

    log := AuditLog{
        OrganizationID: getCurrentOrgID(ctx),
        UserID:         impCtx.PatientUserID,
        Action:         action,
        EntityType:     entityType,
        EntityID:       entityID,
        Changes:        changes,
    }

    if impCtx != nil {
        log.ImpersonatedBy = &impCtx.AdminUserID
        log.ImpersonationID = &impCtx.ImpersonationID
    }

    return db.Create(&log).Error
}