OCPP WS IOocpp-ws-io
Core WebSocket RPC

Security Profiles

Configuring OCPP Security Profiles (0-3).

ocpp-ws-io supports all OCPP security profiles out of the box.

Profile 0 — No Security (Development)

Use this for local development or trusted internal networks.

const client = new OCPPClient({
  endpoint: "ws://localhost:3000",
  identity: "CP001",
  protocols: ["ocpp1.6"],
  securityProfile: SecurityProfile.NONE,
});

Note: The server defaults to SecurityProfile.NONE if not specified.

Profile 1 — Basic Auth (Unsecured WS)

Uses HTTP Basic Authentication over an insecure WebSocket connection.

Client:

const client = new OCPPClient({
  endpoint: "ws://localhost:3000",
  identity: "CP001",
  protocols: ["ocpp1.6"],
  securityProfile: SecurityProfile.BASIC_AUTH,
  password: "my-secret-password", // Sent in Authorization header
});

Server:

server.auth((ctx) => {
  const expectedPassword = getPasswordForStation(ctx.handshake.identity);

  // Buffer comparison to avoid timing attacks is recommended
  if (
    !ctx.handshake.password ||
    !ctx.handshake.password.equals(Buffer.from(expectedPassword))
  ) {
    return ctx.reject(401, "Invalid credentials");
  }

  ctx.accept();
});

Profile 2 — TLS + Basic Auth

Uses HTTP Basic Authentication over a secure WebSocket connection (wss://).

Client:

import fs from "fs";

const client = new OCPPClient({
  endpoint: "wss://csms.example.com",
  identity: "CP001",
  protocols: ["ocpp2.0.1"],
  securityProfile: SecurityProfile.TLS_BASIC_AUTH,
  password: "my-secret-password",
  tls: {
    // Standard Node.js TLSOptions
    ca: fs.readFileSync("./certs/ca.pem"),
    rejectUnauthorized: true,
  },
});

Server:

const server = new OCPPServer({
  protocols: ["ocpp2.0.1"],
  securityProfile: SecurityProfile.TLS_BASIC_AUTH,
  tls: {
    cert: fs.readFileSync("./certs/server.crt"),
    key: fs.readFileSync("./certs/server.key"),
  },
});

Profile 3 — Mutual TLS (Client Certificates)

Uses Client Certificates for authentication. Basic Auth is skipped.

Client:

const client = new OCPPClient({
  endpoint: "wss://csms.example.com",
  identity: "CP001",
  protocols: ["ocpp2.0.1"],
  securityProfile: SecurityProfile.TLS_CLIENT_CERT,
  tls: {
    cert: fs.readFileSync("./certs/client.crt"),
    key: fs.readFileSync("./certs/client.key"),
    ca: fs.readFileSync("./certs/ca.pem"),
  },
});

Server:

const server = new OCPPServer({
  protocols: ["ocpp2.0.1"],
  securityProfile: SecurityProfile.TLS_CLIENT_CERT,
  tls: {
    cert: fs.readFileSync("./certs/server.crt"),
    key: fs.readFileSync("./certs/server.key"),
    ca: fs.readFileSync("./certs/ca.pem"),
    requestCert: true, // Required for mTLS
    rejectUnauthorized: true, // Reject clients without valid certs
  },
});

server.auth((ctx) => {
  const cert = ctx.handshake.clientCertificate;

  // Verify the certificate CN matches the identity (OCPP requirement)
  if (!cert || cert.subject?.CN !== ctx.handshake.identity) {
    return ctx.reject(401, "Certificate identity mismatch");
  }

  ctx.accept();
});

Payload Size Limit

By default, the server rejects any WebSocket frame larger than 64KB before it is JSON-parsed. This prevents a malicious client from sending a multi-gigabyte payload that would exhaust Node.js heap memory (OOM).

You can tune the limit with maxPayloadBytes:

const server = new OCPPServer({
  maxPayloadBytes: 131072, // 128 KB
});

64 KB is well above the maximum size of any valid OCPP message. Only raise this limit if you have a documented need for larger payloads.

TLS Certificate Hot-Reload

When running Node.js as a direct TLS edge server (Security Profile 2 or 3), certificates must be rotated before they expire — typically every 90 days with Let's Encrypt. Restarting the process drops all connected charging stations.

Use server.updateTLS() to swap certificates without disconnecting anyone:

import fs from "fs";

// Called from your cert-renewal hook (e.g. Certbot post-deploy script)
server.updateTLS({
  cert: fs.readFileSync("./certs/server.crt"),
  key: fs.readFileSync("./certs/server.key"),
});

Not needed if you terminate TLS at a reverse proxy (Nginx, AWS ALB, Traefik, etc.). In that case just reload the proxy — Node.js never sees the TLS layer.

Security Event Monitoring

The server emits a securityEvent for every security-relevant action. Hook into this for SIEM integration, alerting, or audit logging — no log parsing required.

server.on("securityEvent", (event) => {
  // event.type    — "AUTH_FAILED" | "CONNECTION_RATE_LIMIT" | "UPGRADE_ABORTED"
  // event.identity — station ID (if known at the time)
  // event.ip       — remote IP address
  // event.timestamp — ISO 8601 string
  // event.details  — event-specific metadata
  console.log(event);
});

Event types

TypeWhen it fires
AUTH_FAILEDAuth callback rejected the connection (wrong password, bad cert)
CONNECTION_RATE_LIMITA single IP exceeded the connection rate limit (connectionRateLimit)
UPGRADE_ABORTEDHandshake timed out or was aborted before auth completed

SIEM integration examples

Datadog:

server.on("securityEvent", (event) => {
  datadogMetrics.increment("ocpp.security_event", 1, [`type:${event.type}`]);
});

PagerDuty (alert on brute-force):

const failures = new Map<string, number>();

server.on("securityEvent", (event) => {
  if (event.type !== "AUTH_FAILED") return;
  const count = (failures.get(event.ip!) ?? 0) + 1;
  failures.set(event.ip!, count);
  if (count >= 10)
    pagerduty.createIncident({ title: `Brute-force from ${event.ip}` });
});

On this page