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
| Type | When it fires |
|---|---|
AUTH_FAILED | Auth callback rejected the connection (wrong password, bad cert) |
CONNECTION_RATE_LIMIT | A single IP exceeded the connection rate limit (connectionRateLimit) |
UPGRADE_ABORTED | Handshake 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}` });
});