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
| HTTP | HTTPS | |
|---|---|---|
| Port | 80 | 443 |
| Socket.io | http://<controller-ip>/api/v1, path /socket.io-v4 | https://<controller-ip>/api/v1, path /socket.io-v4 |
| HTTP API | http://<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
- Your client connects to the namespace
/api/v1. - The controller sends the event
stateChangewith{part: 'app', state: 'User Authorization needed'}. - Only now do you send
authwith user name and password. - After a successful login you send
init. The answer istrue. - From then on, every call your user may use is available.
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:
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:
curl -X POST -H "x-nomos-sid: <session-id>" http://<controller-ip>/api/v1/getVersionThe 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.
logoutends 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
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 -sha256Fetch 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:
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 withexecuteScene, 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:
{"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:
| Permission | Effect 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:
{"errorCode": 109, "errorText": "Too many authorization attempts", "retryAfter": 8}Other answers when logging in:
errorCode | Meaning |
|---|---|
| 106 | user name or password missing, or over HTTP the session has expired |
| 110 | user name or password wrong |
| 122 | user is disabled |
| 137 | remote 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.