Skip to content

Healthcare-Monitoring-System/backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

178 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Healthcare Backend - OIDC Authentication System

A complete Node.js/Express implementation of OIDC login flow with Keycloak using PKCE, server-side sessions, role-based authorization, and database persistence.

🚀 Production Deployment Status

Server: 139.59.66.194 (DigitalOcean K3s) | Namespace: d2 | Service: backend-api:8000

Quick Verification

# SSH to server
ssh root@139.59.66.194

# Check backend pod status
kubectl get pods -n d2 -l app=backend-api -o wide

# View application logs
kubectl logs -n d2 -l app=backend-api --tail=20

# Test API health (from server)
kubectl port-forward -n d2 svc/backend-api 8000:8000 &
curl http://localhost:8000/health

For comprehensive verification commands and troubleshooting, see the Kubernetes Deployment section below.

🚀 Docker & Kubernetes Deployment

Docker Image

Kubernetes Deployment (Server: 139.59.66.194)

The backend runs in the same Kubernetes cluster as d2-data-intelligence in namespace d2.

Deployment Info:

  • Namespace: d2
  • Service: backend-api (ClusterIP)
  • Port: 8000
  • Replicas: 1 (configurable)
  • Image Pull Policy: Always

Verify Deployment on Server

1. SSH to the Server

ssh root@139.59.66.194

2. Check Pod Status

# View backend pod
kubectl get pods -n d2 -l app=backend-api

# Expected output:
# NAME                           READY   STATUS    RESTARTS   AGE
# backend-api-6d7bf7b9bd-7mbgs   1/1     Running   0          2m

3. Check Deployment & Service

# View deployment
kubectl get deployment backend-api -n d2

# View service
kubectl get service backend-api -n d2

# Expected output:
# NAME          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
# backend-api   ClusterIP   10.43.198.19    <none>        8000/TCP   162m

4. Check Pod Logs

# View container logs
kubectl logs -n d2 -l app=backend-api --tail=30

# Expected output should show:
# Database pool initialized
# JWKS client initialized
# Server running on http://localhost:8000

5. Verify Pod Conditions

# Check pod is Ready
kubectl describe pod -n d2 -l app=backend-api | grep -A 5 "Conditions:"

# Expected: All conditions should be True (Ready, Initialized, etc.)

6. Check Image Version

# View current image
kubectl get deployment backend-api -n d2 -o jsonpath='{.spec.template.spec.containers[0].image}'

# Expected: ghcr.io/healthcare-monitoring-system/backend:sha-6303bb98f97269c31053a22bccec775ec25ece1d

7. Test Pod Network Connectivity

# Port-forward to test locally
kubectl port-forward -n d2 svc/backend-api 8000:8000 &

# Test health endpoint (from your machine or server)
curl http://localhost:8000/health

# Expected: {"status":"ok"}

8. Check Service Endpoints

# View endpoints (pod IPs that service routes to)
kubectl get endpoints backend-api -n d2

# Expected:
# NAME          ENDPOINTS       AGE
# backend-api   10.42.0.54:8000 162m

9. Full Resource Overview

# View all backend resources
kubectl get all -n d2 | grep backend

# Should show: deployment, service, pod, replicaset

10. Real-time Pod Monitoring

# Watch pod status in real-time
kubectl get pods -n d2 -l app=backend-api -w

Environment Variables (Container)

The deployment pulls configuration from:

  • ConfigMap: d2-config (shared with d2-data-intelligence)
  • Secret: d2-secrets (shared with d2-data-intelligence)

Required env vars for backend:

  • DATABASE_URL - PostgreSQL connection string
  • KEYCLOAK_URL - Keycloak server URL
  • KEYCLOAK_REALM - Keycloak realm name
  • KEYCLOAK_CLIENT_ID - OAuth client ID
  • KEYCLOAK_CLIENT_SECRET - OAuth client secret
  • NODE_ENV - Set to production for secure cookies

See infra/k8s/base/secret.example.yaml for template.

Update Deployment

If you need to redeploy or update the image:

# Trigger new build/push (push to main or create tag v*)
cd backend
git add .
git commit -m "chore: update backend"
git push origin main

# GitHub Actions builds and pushes to GHCR automatically
# Kubernetes will pull the new image (imagePullPolicy: Always)

# Or manually restart pods:
kubectl rollout restart deployment/backend-api -n d2

# Monitor rollout:
kubectl rollout status deployment/backend-api -n d2

Check Image in GHCR

# List available tags (public package)
curl -s https://ghcr.io/v2/healthcare-monitoring-system/backend/tags/list | jq .

# Expected: {"name":"healthcare-monitoring-system/backend","tags":["latest","sha-abc123..."]}

Features

OIDC/Keycloak Integration

  • Authorization Code flow with PKCE for enhanced security
  • Automatic JWT signature verification using JWKS
  • Secure token exchange and claim extraction

Server-Side Session Management

  • All tokens stored server-side (never exposed to browser)
  • Opaque session ID in httpOnly, secure cookie
  • Configurable session expiration
  • In-memory store (swappable with Redis)

Role-Based Access Control

  • Role extraction from JWT claims
  • Middleware for route protection
  • Support for multiple roles per user
  • Easy per-route authorization

Database Integration

  • OIDC identity separation (auth_subjects table)
  • Doctor and patient linking to authentication
  • Automatic auth subject upsert on login
  • Repository pattern for clean queries

Security Features

  • PKCE for public client protection
  • Cryptographically secure random generation
  • JWT signature verification
  • Protection against CSRF, token interception
  • Row-level security on auth_subjects table

Project Structure

src/
  auth/
    config.ts         # Keycloak and app configuration
    jwt.ts            # JWT verification and claim extraction
    middleware.ts     # Authentication/authorization middleware
    pkce.ts           # PKCE utilities
    sessionStore.ts   # Session storage interface and in-memory impl
  db/
    authService.ts    # Auth database operations using pg Pool
    client.ts         # PostgreSQL pool initialization
  middleware/
    errorHandler.ts   # (For custom error handling if needed)
  routes/
    auth.ts           # Auth endpoints (login, callback, logout)
    alerts.ts         # (Existing)
    devices.ts        # (Existing)
    doctors.ts        # (Existing)
    patients.ts       # (Existing)
  utils/
    errors.ts         # (For shared error classes)
  index.ts            # Main Express app
supabase/
  migrations/
    003_auth_subjects.sql          # Auth subject table
    004_auth_subject_links.sql     # Doctor/patient linking

Quick Start

1. Install Dependencies

npm install

2. Set Environment Variables

Copy .env.example to .env.local and fill in your values:

KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=master
KEYCLOAK_CLIENT_ID=your-client-id
KEYCLOAK_CLIENT_SECRET=your-client-secret
DATABASE_URL=postgresql://postgres:password@localhost:5432/healthcare_db

3. Apply Database Migrations

Run migrations using psql:

psql -U postgres -d healthcare_db -f supabase/migrations/003_auth_subjects.sql
psql -U postgres -d healthcare_db -f supabase/migrations/004_auth_subject_links.sql

Alternatively, execute the SQL manually in your database client.

4. Start Development Server

npm run dev

Server runs on http://localhost:3000

API Endpoints

Public Endpoints

GET /api/auth/login

Initiates OIDC login flow.

Response:

{
  "success": true,
  "authUrl": "https://keycloak-url/auth?client_id=..."
}

POST /api/auth/callback

Handles Keycloak callback and exchanges authorization code for tokens.

Request:

{
  "code": "authorization-code-from-keycloak",
  "state": "state-parameter"
}

Response:

{
  "success": true,
  "sessionId": "opaque-session-id",
  "user": {
    "sub": "user-subject-id",
    "role": "doctor",
    "preferred_username": "john_doe",
    "name": "John Doe",
    "email": "john@example.com"
  }
}

POST /api/auth/logout

Clears server-side session and cookie.

Response:

{
  "success": true,
  "message": "Logged out"
}

Protected Endpoints

GET /api/protected/me

Returns current user session info. Requires authentication.

Response:

{
  "success": true,
  "user": {
    "sub": "user-id",
    "role": "doctor",
    "preferred_username": "john_doe",
    "name": "John Doe",
    "email": "john@example.com",
    "auth_subject_id": 123
  }
}

GET /api/protected/doctors/info

Doctor-only endpoint. Requires doctor or admin role.

GET /api/protected/patients/info

Patient-only endpoint. Requires patient role.

Usage Examples

Protecting Routes with Authentication

app.get('/api/my-data', requireAuth, (req, res) => {
  // req.user contains the session data
  const userId = req.user.sub;
  res.json({ userId });
});

Protecting Routes with Roles

app.post('/api/patients', requireRole(['doctor', 'admin']), (req, res) => {
  // Only doctors and admins
  res.json({ message: 'Created patient' });
});

app.get('/api/prescriptions', requireRole(['doctor']), (req, res) => {
  // Only doctors
  res.json({ prescriptions: [] });
});

Creating a Router with Auth

import { express } from 'express';
import { createRoleAuthorizationMiddleware } from './auth/middleware';

const doctorRouter = express.Router();
doctorRouter.use(requireRole(['doctor', 'admin']));

doctorRouter.get('/patients', (req, res) => {
  // All routes here require doctor role
  res.json({ patients: [] });
});

app.use('/api/doctors', doctorRouter);

Authentication Flow Diagram

1. Login Initiation
   Frontend → GET /api/auth/login
   ← Return authUrl
   Frontend redirects to authUrl

2. User Authentication (at Keycloak)
   User enters credentials
   Keycloak verifies and generates auth code

3. Callback & Token Exchange
   Keycloak → POST /api/auth/callback?code=...&state=...
   Backend validates state (CSRF protection)
   Backend exchanges code + code_verifier for tokens

4. JWT Verification
   Backend verifies JWT signature using JWKS
   Backend extracts claims (sub, roles, email, etc)

5. Session Creation
   Backend creates server-side session with tokens & claims
   Backend stores session in InMemorySessionStore
   Backend links auth subject to doctor/patient
   Backend sets session_id in httpOnly cookie

6. Response to Frontend
   Backend ← { success, user, sessionId }
   Frontend stores user in state (not localStorage!)
   Cookie automatically sent on subsequent requests

7. Subsequent Requests
   Frontend → GET /api/protected/me
   Browser sends Cookie: session_id=...
   Middleware loads session from server-side store
   Middleware attaches req.user
   Route handler uses req.user

8. Logout
   Frontend → POST /api/auth/logout
   Backend clears session
   Backend clears cookie
   Frontend clears user state

Security Considerations

PKCE (Proof Key for Public Clients)

Implemented per RFC 7636 to prevent authorization code interception:

  • Code verifier: 128-character random string
  • Code challenge: SHA-256 hash of verifier
  • Only server knows code_verifier, protects against code theft

Token Security

  • Never in browser: Access/refresh tokens stored only on server
  • Verified on arrival: JWT signatures checked against Keycloak JWKS
  • Claims extracted safely: Role and user info parsed securely

Session Security

  • Opaque session ID: No sensitive data in cookie
  • httpOnly flag: Prevents JavaScript access
  • Secure flag: HTTPS only in production
  • SameSite=strict: CSRF protection
  • Server-side expiration: Automatic cleanup of old sessions

Database Relationships

  • auth_subjects: Single source of truth for OIDC identity
  • doctors.auth_subject_id (unique FK): One doctor per auth subject
  • patients.auth_subject_id (unique FK): One patient per auth subject
  • RLS policies: Enhanced row-level access control

Extending the Implementation

Adding Custom Claims

In src/auth/jwt.ts, extend UserClaims:

export interface UserClaims {
  sub: string;
  roles: string[];
  preferred_username: string;
  name: string;
  email: string;
  phone?: string;           // Add custom fields
  organization?: string;
}

export function extractClaims(decodedToken: any): UserClaims {
  // Add extraction logic
  return {
    // ... existing fields
    phone: decodedToken.phone,
    organization: decodedToken.org,
  };
}

Switching to Redis for Sessions

Create src/auth/redisSessionStore.ts:

import { createClient } from 'redis';
import { SessionStore, SessionData } from './sessionStore';

export class RedisSessionStore implements SessionStore {
  private client = createClient();

  async setSession(sessionId: string, data: SessionData): Promise<void> {
    await this.client.setEx(
      `session:${sessionId}`,
      data.expires_at - Date.now(),
      JSON.stringify(data)
    );
  }

  // ... implement other methods
}

Then in src/index.ts:

import { RedisSessionStore } from './auth/redisSessionStore';
const sessionStore = new RedisSessionStore();

Adding Token Refresh

Implement silent refresh in middleware:

export async function refreshTokenIfNeeded(
  session: SessionData,
  sessionStore: SessionStore
) {
  if (session.expires_at - Date.now() < 5 * 60 * 1000) {
    // Less than 5 min remaining
    const newTokens = await exchangeRefreshToken(session.refresh_token);
    session.access_token = newTokens.access_token;
    session.expires_at = Date.now() + newTokens.expires_in * 1000;
    await sessionStore.setSession(sessionId, session);
  }
}

Custom Role Mapping

Modify getPrimaryRole() in src/auth/jwt.ts:

export function getPrimaryRole(roles: string[]): string {
  // Custom priority
  if (roles.includes('super_admin')) return 'super_admin';
  if (roles.includes('admin')) return 'admin';
  if (roles.includes('radiologist')) return 'doctor';
  if (roles.includes('nurse')) return 'medical';
  // ... more roles
  return 'user';
}

Production Deployment Checklist

  • Set NODE_ENV=production
  • Set SECURE_COOKIE=true (requires HTTPS)
  • Use HTTPS for all URLs
  • Replace InMemorySessionStore with Redis/Memcached
  • Set strong random KEYCLOAK_CLIENT_SECRET
  • Configure Keycloak to use HTTPS
  • Enable rate limiting on /api/auth/callback
  • Implement request logging and monitoring
  • Set up CORS properly for frontend domain
  • Enable HTTPS in Keycloak OAuth client settings
  • Verify email verification is enabled in Keycloak
  • Backup and secure Supabase credentials
  • Test failover and recovery scenarios
  • Implement audit logging for auth events

Performance Optimization

Caching JWKS

The JWKS client caches public keys automatically. No additional action needed.

Session Cleanup

In-memory store cleans up expired sessions every 5 minutes. For Redis, set TTL on keys.

Connection Pooling

The pg library handles connection pooling automatically via Pool:

  • Max connections: 10 (configurable via DB_POOL_MAX)
  • Idle timeout: 30s (configurable via DB_IDLE_TIMEOUT)
  • Automatic reconnection on pool errors
  • Graceful cleanup on app shutdown

Troubleshooting

401 Unauthorized on all protected routes

  • Check SESSION_COOKIE_NAME matches between setting and reading
  • Verify session_id cookie is being sent with requests
  • Check session hasn't expired in server store

"JWKS client not initialized"

  • Ensure KEYCLOAK_URL and KEYCLOAK_REALM are correct
  • Verify Keycloak is running and accessible
  • Check firewall rules

"No doctor found with email"

  • Verify email in Keycloak matches email in doctors table exactly
  • Check email_verified flag in Keycloak
  • Ensure no typos or case mismatches

Keycloak Identity Sync Validation

The deployable migration for users.keycloak_sub lives in postgres/deploy-migrations/010_keycloak_identity_sync.sql and is applied by the backend migration Job during deployment.

-- Columns and uniqueness constraints expected by the login sync.
SELECT column_name, is_nullable, data_type
FROM information_schema.columns
WHERE table_name = 'users'
  AND column_name IN ('email', 'first_name', 'last_name', 'keycloak_sub')
ORDER BY column_name;

SELECT conname
FROM pg_constraint
WHERE conrelid = 'users'::regclass
  AND conname IN ('users_email_key', 'users_keycloak_sub_key');

-- Any placeholder rows should be reviewed and corrected from Keycloak/admin data.
SELECT id, email, first_name, last_name, role, keycloak_sub
FROM users
WHERE email LIKE 'local-user-%@placeholder.invalid'
   OR first_name = 'Unknown'
   OR last_name = 'User';

-- Role profile rows should exist only where required by the app role.
SELECT u.id, u.email, u.role, u.keycloak_sub, d.doctor_id, p.id AS patient_id
FROM users u
LEFT JOIN doctor d ON d.user_id = u.id
LEFT JOIN patients p ON p.user_id = u.id
WHERE u.role IN ('doctor', 'patient', 'admin')
ORDER BY u.id;

GitOps Schema Migrations

Backend is the source of truth for MediPulse core app schema and auth logic. Put production schema migrations in postgres/deploy-migrations/ as incremental, non-destructive SQL files. Do not put demo/test seed data in this directory.

Deployment runs scripts/run-migrations.sh through the Kubernetes Job infra/k8s/base/backend-migrate-job.yaml. The Job is an Argo CD PreSync hook, uses d2-secrets.BACKEND_DATABASE_URL, applies SQL files in sorted order, and records completed files in schema_migrations using filename, checksum, and applied_at. If a migration fails, the Job fails visibly and the sync does not silently continue.

Manual verification:

kubectl exec -n d2 d2-postgres-0 -- psql -U d2 -d d2_vitals -c "\d users"
kubectl exec -n d2 d2-postgres-0 -- psql -U d2 -d d2_vitals -c "select * from schema_migrations order by applied_at desc;"

Manual local migration run:

DATABASE_URL="postgresql://..." scripts/run-migrations.sh

Demo seed data is manual only. Use the safe idempotent D2 seed explicitly when needed:

DATABASE_URL="postgresql://..." scripts/run-demo-seed.sh

Never add destructive demo seeds to the backend deployment pipeline. Real Keycloak-synced users must not be deleted or overwritten by seed scripts.

"Invalid or expired state"

  • State expires after 1 hour
  • User may have taken too long to authenticate
  • Browser cookies may be disabled/cleared

Contributing

When extending the auth system:

  1. Keep session store interface clean (for easy backend swapping)
  2. Add TypeScript types to all functions
  3. Document middleware usage with examples
  4. Update this README with new features
  5. Test with multiple roles and edge cases

License

ISC

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors