Skip to content

Connect and Authenticate ​

Connect to the nomos system Controller and log in, over Socket.io and HTTP

Every call to the controller needs a login, over Socket.io as well as over HTTP. This page describes how the connection is set up, how long a login lasts, how to use HTTPS properly and which permissions your program gets.

Address and ports ​

HTTPHTTPS
Port80443
Socket.iohttp://<controller-ip>/api/v1, path /socket.io-v4https://<controller-ip>/api/v1, path /socket.io-v4
HTTP APIhttp://<controller-ip>/api/v1/<function>https://<controller-ip>/api/v1/<function>

For Socket.io, /api/v1 is the namespace, not part of the path. Use socket.io-client version 4 with the path /socket.io-v4. The path /socket.io serves older clients on Socket.io 2 and is not meant for new integrations.

Socket.io ​

Sequence ​

  1. Your client connects to the namespace /api/v1.
  2. The controller sends the event stateChange with {part: 'app', state: 'User Authorization needed'}.
  3. Only now do you send auth with user name and password.
  4. After a successful login you send init. The answer is true.
  5. From then on, every call your user may use is available.
javascript
const { io } = require('socket.io-client');

const socket = io('http://<controller-ip>/api/v1', { path: '/socket.io-v4' });

socket.on('stateChange', async (state) => {
    if (state.part !== 'app' || state.state !== 'User Authorization needed') return;

    const auth = await socket.timeout(15000).emitWithAck('auth', { username: 'USERNAME', password: 'PASSWORD' });
    if (auth.errorCode) return console.error(auth.errorCode, auth.errorText);

    await socket.timeout(15000).emitWithAck('init', { language: 'en', appName: 'my-integration' });
    // ready: call getAllComponents, componentUpdate, ...
});

WARNING

Wait for stateChange before sending auth. The controller only accepts auth once it has checked the connection. An auth sent straight after connect is lost, and your client waits for an answer that never comes. The same applies to any other call before the login.

If the connection drops and the client reconnects, the controller asks for a login again with stateChange. The code above then logs in again by itself. Read the current state again afterwards: you did not receive the changes made during the interruption.

init takes details about your client, such as language for the language of error texts and appName. All fields are on apidocs.

With a stored session ​

Instead of user name and password, your client can pass a session ID as the query parameter x-nomos-sid:

javascript
const socket = io('http://<controller-ip>/api/v1', {
    path: '/socket.io-v4',
    query: { 'x-nomos-sid': '<session-id>' },
});

socket.on('connect', async () => {
    await socket.timeout(15000).emitWithAck('init', { language: 'en', appName: 'my-integration' });
    // ready
});

If the session is valid, there is no auth; you send init right away. If it has expired, the controller sends stateChange with User Authorization needed as above, and your client logs in with auth.

You get a session ID over HTTP with POST /api/v1/auth (see HTTP API). The answer to auth over Socket.io does contain the field sessionID, but a client without cookies, such as a Node.js program, gets null there.

HTTP ​

Over HTTP you send the login with every request, either as the headers auth-username and auth-password or as a session ID in the header x-nomos-sid:

bash
curl -X POST -H "x-nomos-sid: <session-id>" http://<controller-ip>/api/v1/getVersion

The details are on the HTTP API page.

Sessions ​

  • A session lasts 24 hours from its last use. If you log in over HTTP with the header auth-persistent: true, it lasts two years from its last use.
  • Every use extends the session.
  • logout ends the session immediately.

A persistent session is worth as much as the password. Store the session ID with the same care.

HTTPS and certificate ​

On port 443 the controller uses a self-signed certificate. It contains no host name and no IP address. This has two consequences:

  • Without further settings, every client rejects the certificate, because no known certificate authority issued it.
  • Even when you trust the certificate, the usual host name check fails, because the certificate names no host.

Do not switch the check off (curl -k, Node.js rejectUnauthorized: false). Your client would then accept any certificate at all. Trust exactly this certificate instead.

Fetch the certificate ​

bash
openssl s_client -connect <controller-ip>:443 </dev/null 2>/dev/null \
  | openssl x509 -out nomos-controller.pem
openssl x509 -in nomos-controller.pem -noout -fingerprint -sha256

Fetch the certificate on a network you trust and keep the file with your program.

Node.js ​

Node.js checks the chain against the fetched certificate (ca). Instead of the host name, checkServerIdentity compares the certificate's fingerprint:

javascript
const fs = require('fs');
const crypto = require('crypto');
const https = require('https');
const { io } = require('socket.io-client');

const ca = fs.readFileSync('nomos-controller.pem');
const fingerprint = new crypto.X509Certificate(ca).fingerprint256;

// Trust exactly this certificate. It names no host, so the
// certificate itself is compared instead of the host name.
const tlsOptions = {
    ca,
    checkServerIdentity: (host, cert) =>
        cert.fingerprint256 === fingerprint ? undefined : new Error(`unexpected certificate from ${host}`),
};

// HTTP API over HTTPS
https.request({
    host: '<controller-ip>', port: 443, path: '/api/v1/getVersion', method: 'POST',
    headers: { 'x-nomos-sid': '<session-id>' },
    ...tlsOptions,
}, (res) => res.pipe(process.stdout)).end();

// Socket.io over HTTPS: only the websocket transport passes checkServerIdentity on
const socket = io('https://<controller-ip>/api/v1', {
    path: '/socket.io-v4',
    transports: ['websocket'],
    ...tlsOptions,
});

For Socket.io, transports: ['websocket'] is required: the polling transport of socket.io-client does not pass checkServerIdentity on, and the connection then fails with xhr poll error. If the controller presents a different certificate, the connection fails (websocket error, for HTTPS self-signed certificate).

curl ​

curl --cacert nomos-controller.pem is not enough here. curl always checks the host name as well and aborts (error 60), because the certificate names none. curl has no option to skip only this check. Use plain HTTP with curl on a network you trust, or a program like the one above for HTTPS.

Permissions ​

Your program has exactly the permissions of the user it logs in with. You create users and set their permissions in the configuration interface, see Users in the integrator handbook.

Admin and regular user ​

  • Group "admin": may use every call.
  • All other users: may only use a fixed selection of calls. It covers reading components, rooms, zones and scenes, switching with componentUpdate, executing scenes with executeScene, as well as logging in and out and changing one's own password. Everything that configures the controller needs an admin.

If a regular user calls something outside this selection, the controller answers:

json
{"errorCode": 117, "errorText": "Not allowed"}

Over HTTP this answer comes with status 400.

Rooms and scenes ​

The permissions from the configuration interface apply to the API as well:

PermissionEffect in the API
Room "Off"The room's components are missing from the lists, changes to their values are not reported, componentUpdate returns false for them.
Room "View only"The room's components can be read, componentUpdate returns false for them.
Scene "Off"The scene is missing from getScenes, executeScene answers with error 117.

Recommendation ​

Create a separate user for every third-party system, in the group "user" and with only the rooms and scenes it needs. A lockout after failed attempts then only hits this account, you can switch the integration off on its own, and a leaked password does not open the whole installation. An integration only needs an admin if it is meant to configure the controller.

Lockout after failed attempts ​

The controller slows down password guessing, over Socket.io as well as over HTTP:

  • Failed logins are counted per user name and per source address. The stricter lockout applies.
  • Five failed attempts have no effect. From the sixth on, the controller blocks further logins: first for 1 second, twice as long after every further failed attempt, at most 15 minutes.
  • A successful login resets the counter. After 15 minutes without a failed attempt, it starts from zero.

During a lockout the controller answers with error 109. retryAfter gives the waiting time in seconds:

json
{"errorCode": 109, "errorText": "Too many authorization attempts", "retryAfter": 8}

Other answers when logging in:

errorCodeMeaning
106user name or password missing, or over HTTP the session has expired
110user name or password wrong
122user is disabled
137remote access is not enabled for this user

A program that logs in again straight after error 110 locks out itself and, through the source address, every other account on the same computer. Report errors 110 and 122 instead of retrying.

Remote access ​

When remote access is switched on, the nomos app reaches the controller from anywhere. Requests along this path are subject to further rules:

  • Besides the login, the controller requires a remote access token. It only issues this token on the local network.
  • Only users with the "Remote Access" permission can log in. Without it the controller answers with error 137, even when the password is right.
  • Failed attempts are only counted per user name there, not per source address.
  • Some functions are blocked over remote access, among them MCP and Node-RED.

Run your own integrations on the local network.