Skip to main content

JWT Authentication

OmniEHR uses JSON Web Tokens (JWT) for stateless authentication. All API requests (except login) require a valid JWT bearer token in the Authorization header.

Authentication Flow

1

User Login

Client sends credentials to /api/auth/login
2

Token Generation

Server validates credentials and generates JWT token
3

Token Storage

Client stores token (typically in localStorage or memory)
4

Authenticated Requests

Client includes token in Authorization header for all subsequent requests
5

Token Verification

Server verifies token signature and expiration on each request

Login Endpoint

POST /api/auth/login

Authenticate with email and password to receive a JWT token.
// From ~/workspace/source/server/src/routes/authRoutes.js:23
router.post(
  "/login",
  asyncHandler(async (req, res) => {
    const payload = loginSchema.parse(req.body);
    const user = await User.findOne({ email: payload.email });

    if (!user || !user.active) {
      throw new ApiError(401, "Invalid credentials");
    }

    const passwordOk = await bcrypt.compare(payload.password, user.passwordHash);

    if (!passwordOk) {
      throw new ApiError(401, "Invalid credentials");
    }

    user.lastLoginAt = new Date();
    await user.save();

    const token = signAccessToken({
      sub: String(user._id),
      email: user.email,
      role: user.role,
      name: user.fullName
    });

    res.json({ token, user: formatUser(user) });
  })
);
Request:
curl -X POST https://api.omniehr.com/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "secure-password"
  }'
Response:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "email": "[email protected]",
    "fullName": "Dr. Jane Smith",
    "organization": "General Hospital",
    "role": "practitioner",
    "active": true,
    "lastLoginAt": "2026-03-04T10:30:00.000Z",
    "createdAt": "2025-01-15T08:00:00.000Z"
  }
}
The login endpoint updates the user’s lastLoginAt timestamp for audit purposes (see ~/workspace/source/server/src/routes/authRoutes.js:39).

Security Features

  1. Password Hashing: Passwords are hashed with bcrypt (cost factor 12)
  2. Active User Check: Inactive users cannot authenticate
  3. Generic Error Messages: Returns “Invalid credentials” for both invalid email and password to prevent user enumeration

Token Structure

JWT Payload

From ~/workspace/source/server/src/routes/authRoutes.js:42, the JWT contains:
const token = signAccessToken({
  sub: String(user._id),      // Subject: User ID
  email: user.email,           // User email
  role: user.role,             // User role (admin/practitioner/auditor)
  name: user.fullName          // Full name for display
});

Token Generation

From ~/workspace/source/server/src/utils/jwt.js:4:
export const signAccessToken = (payload) => {
  return jwt.sign(payload, env.jwtSecret, { expiresIn: env.jwtExpiresIn });
};
Environment Variables:
  • JWT_SECRET - Secret key for signing tokens (keep secure!)
  • JWT_EXPIRES_IN - Token expiration time (e.g., “8h”, “1d”)
The JWT_SECRET must be a strong, random string. Never commit it to version control. Use a secrets manager in production.

Authentication Middleware

All protected routes use the authenticate middleware from ~/workspace/source/server/src/middleware/auth.js:4:
export const authenticate = (req, _res, next) => {
  const authorization = req.headers.authorization;

  if (!authorization?.startsWith("Bearer ")) {
    return next(new ApiError(401, "Missing bearer token"));
  }

  const token = authorization.slice(7);

  try {
    const payload = verifyAccessToken(token);
    req.user = payload;
    return next();
  } catch {
    return next(new ApiError(401, "Invalid or expired token"));
  }
};

Token Verification

From ~/workspace/source/server/src/utils/jwt.js:8:
export const verifyAccessToken = (token) => {
  return jwt.verify(token, env.jwtSecret);
};
The verification:
  • ✅ Validates token signature
  • ✅ Checks expiration time
  • ✅ Throws error if invalid or expired

Making Authenticated Requests

Using the Token

Include the JWT in the Authorization header with the Bearer scheme:
curl https://api.omniehr.com/api/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

JavaScript Example

const response = await fetch('https://api.omniehr.com/api/fhir/Patient', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});

React Hook Example

import { useState, useEffect } from 'react';

function useAuth() {
  const [token, setToken] = useState(localStorage.getItem('token'));
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (token) {
      // Verify token is still valid
      fetch('/api/auth/me', {
        headers: { 'Authorization': `Bearer ${token}` }
      })
        .then(res => res.json())
        .then(data => setUser(data.user))
        .catch(() => {
          // Token expired or invalid
          setToken(null);
          localStorage.removeItem('token');
        });
    }
  }, [token]);

  const login = async (email, password) => {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });
    
    const data = await res.json();
    localStorage.setItem('token', data.token);
    setToken(data.token);
    setUser(data.user);
  };

  const logout = () => {
    localStorage.removeItem('token');
    setToken(null);
    setUser(null);
  };

  return { user, token, login, logout };
}

Get Current User

GET /api/auth/me

Retrieve the currently authenticated user’s information. From ~/workspace/source/server/src/routes/authRoutes.js:53:
router.get(
  "/me",
  authenticate,
  asyncHandler(async (req, res) => {
    const user = await User.findById(req.user.sub);

    if (!user) {
      throw new ApiError(404, "User not found");
    }

    res.json({ user: formatUser(user) });
  })
);
Request:
curl https://api.omniehr.com/api/auth/me \
  -H "Authorization: Bearer <token>"
Response:
{
  "user": {
    "id": "507f1f77bcf86cd799439011",
    "email": "[email protected]",
    "fullName": "Dr. Jane Smith",
    "organization": "General Hospital",
    "role": "practitioner",
    "active": true,
    "lastLoginAt": "2026-03-04T10:30:00.000Z",
    "createdAt": "2025-01-15T08:00:00.000Z"
  }
}

Error Handling

Authentication Errors

Status CodeErrorDescription
401Missing bearer tokenNo Authorization header or invalid format
401Invalid or expired tokenToken signature invalid or expired
401Invalid credentialsWrong email or password during login
404User not foundAuthenticated user no longer exists in database

Example Error Response

{
  "status": "error",
  "message": "Invalid or expired token",
  "statusCode": 401
}

Token Expiration

When a token expires, the API returns a 401 error. Clients should:
  1. Catch 401 errors globally
  2. Redirect to login page
  3. Clear stored token
  4. Prompt user to re-authenticate
// Axios interceptor example
axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

Security Best Practices

Store Tokens Securely

Use httpOnly cookies or memory storage. Avoid localStorage for high-security applications.

Use HTTPS Only

Never send tokens over unencrypted connections. Always use HTTPS in production.

Short Expiration

Use reasonable token expiration times (8 hours recommended for healthcare).

Validate on Each Request

Server validates token signature and expiration on every request.

Next Steps

RBAC

Learn about role-based authorization

HIPAA Overview

Understand HIPAA compliance measures

Build docs developers (and LLMs) love