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.readCerner 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=20Example: Retrieve a clinical document
GET /DocumentReference?patient=12345Example: Retrieve document binary
GET /Binary/67890Most Cerner binaries are Base64 encoded, so the backend must decode them.
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.









BLOGS
NEWSROOM
CASE STUDIES
WEBINARS
PODCASTS
ASSET HUB
EVENT CALENDAR 


















