NestJS + Cerner EHR System-to-System Integration
Technology Blogs

NestJS + Cerner EHR System-to-System Integration

Rohan K
Software Engineer
Table of Content

Healthcare applications often need to communicate directly with Electronic Health Record (EHR) systems to retrieve patient information, clinical notes, appointments, and other medical resources. When this communication happens between two backend systems without any user interaction, it is called system-to-system (S2S) integration.

In this blog, we will explore how to integrate Cerner EHR with a NestJS backend using S2S authentication, specifically using Cerner’s SMART v2 (asymmetric key) OAuth 2.0 model. We will also walk through JWT creation, token exchange, FHIR calls, and why this method is more secure and maintainable for backend workflows.

Why System-to-System Integration?

Most EHR tutorials focus on SMART-on-FHIR apps, where a user logs in through the EHR. But many real-world healthcare systems need background access instead. For example:

  • Syncing patient records every night
  • Pulling encounters or appointments for analytics
  • Fetching DocumentReferences for clinical workflows
  • Integrating internal systems like billing or CRM
  • Auto-processing clinical documents without manual login

In such cases, there is no end-user to authenticate, which makes S2S ideal.

Cerner supports this via JWT-based client authentication, where the backend signs a JWT using its private key and exchanges it for an access token.

Understanding Cerner SMART v2 System Authentication

Cerner requires asymmetric key authentication for S2S communication. This means:

  • You generate a private key
  • You upload your public key in JWK format to Cerner Code Console
  • Your backend signs a JWT client assertion with the private key
  • Cerner verifies it using your uploaded public key
  • Cerner returns an OAuth 2.0 access token

This method is extremely secure because the private key never leaves your server.

How the Authentication Flow Works

Here’s a simple breakdown:

1. Generate RSA Key Pair

You generate a public/private key pair in PEM format.

function generateKeyPair() {
 const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
   modulusLength: 2048,
   publicKeyEncoding: {
     type: "spki",
     format: "pem",
   },
   privateKeyEncoding: {
     type: "pkcs8",
     format: "pem",
   },
 });
 console.log("=== GENERATED KEYS ===");
 console.log("\nPrivate Key (Save this securely):");
 console.log(privateKey);
 console.log("\nPublic Key (Upload to Cerner Code Console):");
 console.log(publicKey);
 return { publicKey, privateKey };
}

You upload the public key (as JWK) to Cerner. You store the private key securely (environment variable, vault, etc).

2. Create JWT Client Assertion

Every time your backend needs a Cerner token, it creates a signed JWT.

The JWT includes:

  • iss: your client ID
  • sub: your client ID
  • aud: Cerner token endpoint
  • iat: time issued
  • exp: expiration (max 5 minutes)
  • jti: random UUID
/**
* Create JWT assertion for client authentication
*/
function createClientAssertion(privateKeyPEM, keyId) {
const now = Math.floor(Date.now() / 1000);

// JWT Header
const header = {
  alg: "RS384",
  typ: "JWT",
  kid: keyId,
};

// JWT Claims
const payload = {
  iss: CONFIG.CLIENT_ID, // Issuer
  sub: CONFIG.CLIENT_ID, // Subject
  aud: CONFIG.TOKEN_URL, // Audience (token endpoint)
  exp: now + 300, // Expiration (5 minutes)
  iat: now, // Issued at
  jti: crypto.randomUUID(), // Unique JWT ID
};

// Encode header and payload
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));

// Create signature
const signatureInput = `${encodedHeader}.${encodedPayload}`;
const signature = crypto
  .createSign("RSA-SHA384")
  .update(signatureInput)
  .sign(privateKeyPEM, "base64");

const encodedSignature = base64UrlEncode(Buffer.from(signature, "base64"));

// Combine to form JWT
return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
}

/**
* Base64 URL encode (JWT standard)
*/
function base64UrlEncode(data) {
const base64 = Buffer.from(data).toString("base64");
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

This JWT is signed using RS384, the algorithm Cerner requires.

Need a Production-ready Cerner S2S Integration? Talk to Our Healthcare Engineers.

3. Exchange JWT for an Access Token

Your NestJS backend sends:

grant_type=client_credentials
client_assertion=<your-signed-jwt>
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
scope=system/Patient.read system/Encounter.read ...
/**
* Get access token using client credentials flow
*/
async function getAccessToken(
privateKeyPEM = PRIVATE_KEY,
keyId = CONFIG.KEY_ID,
scopes = ["system/Patient.rs", "system/Patient.c", "system/Patient.u"]
) {
const clientAssertion = createClientAssertion(privateKeyPEM, keyId);

const postData = new URLSearchParams({
  grant_type: "client_credentials",
  client_assertion_type:
    "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
  client_assertion: clientAssertion,
  scope: scopes.join(" "),
}).toString();

const url = new URL(CONFIG.TOKEN_URL);

const options = {
  hostname: url.hostname,
  path: url.pathname,
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    "Content-Length": Buffer.byteLength(postData),
  },
};

return new Promise((resolve, reject) => {
  const req = https.request(options, (res) => {
    let data = "";

    res.on("data", (chunk) => {
      data += chunk;
    });

    res.on("end", () => {
      if (res.statusCode === 200) {
        resolve(JSON.parse(data));
      } else {
        reject(
          new Error(`Token request failed: ${res.statusCode} - ${data}`)
        );
      }
    });
  });

  req.on("error", reject);
  req.write(postData);
  req.end();
});
}

Cerner returns:

{
  "access_token": "eyJhbGc...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

You should cache this token until it expires to avoid unnecessary authentication calls.

4. Use the Access Token for FHIR API Calls

Once you receive the token, you can call any allowed FHIR endpoint.

Example:

GET /Patient/12345
Authorization: Bearer <token>
Accept: application/fhir+json
x-cerner-tenant-id: <tenant>
x-cerner-service-root: <fhir-base-url>

This request returns the patient’s data in FHIR JSON format.

Building This Flow in a NestJS Backend

A clean NestJS architecture for Cerner looks like this:

src/
├── auth/
│     ├── cerner-auth.service.ts
│     ├── token-cache.service.ts
├── fhir/
│     ├── fhir-client.service.ts
├── modules/
│     ├── patients/
│     ├── encounters/
│     ├── documents/
│     ├── appointments/

Each FHIR resource gets its own module, making the system easy to extend.

Implementing the JWT Authentication Logic

Your code correctly follows Cerner’s requirements for:

  • RSA key generation
  • JWKS formatting
  • RS384 signing
  • JWT creation
  • Token exchange
  • FHIR calls

Below is a conceptual summary for the blog (not copy/pasted from your code).

const header = { alg: "RS384", typ: "JWT", kid: KEY_ID };
const payload = {
  iss: CLIENT_ID,
  sub: CLIENT_ID,
  aud: TOKEN_URL,
  exp: now + 300,
  iat: now,
  jti: randomUUID()
};
const jwt = signWithPrivateKey(header, payload);

Exchange JWT for Access Token

POST /token
grant_type=client_credentials
client_assertion=<jwt>
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
scope=system/Patient.read system/Encounter.read

Cerner returns an access token which your backend stores.

Making FHIR API Calls

After authentication, calling Cerner APIs is straightforward.

Example: Fetch a patient

await this.fhirClient.get(`/Patient/${patientId}`, {
  headers: {
    Authorization: `Bearer ${token}`,
    Accept: "application/fhir+json",
    "x-cerner-tenant-id": TENANT_ID,
    "x-cerner-service-root": FHIR_BASE_URL
  }
});

Example: Search for encounters

GET /Encounter?patient=12345&_count=20

Example: Retrieve a clinical document

GET /DocumentReference?patient=12345

Example: Retrieve document binary

GET /Binary/67890

Most Cerner binaries are Base64 encoded, so the backend must decode them.

coma

Conclusion

System-to-system integration with Cerner EHR enables healthcare applications to run automated workflows without requiring clinician logins, improving efficiency and reducing operational overhead. NestJS provides a clean, modular backend architecture that simplifies managing authentication, services, and Cerner-specific logic while supporting long-term scalability and maintainability.

Although Cerner’s SMART v2 authentication model may seem complex at first, it delivers a highly secure and standards-based approach to backend access of clinical data. Once implemented, it supports scalable, compliant integrations suitable for enterprise healthcare environments.

Rohan K

Rohan K

Software Engineer

Rohan is a passionate individual who thrives on developing sustainable and scalable web applications. He has more than a year of experience utilizing Angular and Node.js to create full-stack applications. He has a problem-solving mentality and is constantly eager to learn new technologies.

Share This Blog

Read More Similar Blogs

Let’s Transform
Healthcare,
Together.

Partner with us to design, build, and scale digital solutions that drive better outcomes.

Location

5900 Balcones Dr, Ste 100-7286, Austin, TX 78731, United States

Contact form