Skip to main content

Users & Authentication

HomeCore uses JWT (HS256) for REST API authentication and per-user role-based access control.

Roles

RoleDescription
AdminFull access — can manage users, plugins, all devices and automations
UserRead and write access to devices, automations, scenes, areas — cannot manage users or plugins
ReadOnlyRead-only access to everything

Scope reference

Every protected API route enforces a specific scope. A 403 Forbidden response means the JWT lacks the required scope.

ScopeEndpoints
devices:readGET /devices, GET /devices/{id}, GET /devices/{id}/history, GET /events
devices:writePATCH /devices/{id}/state, PATCH /devices (bulk), DELETE /devices/{id}, DELETE /devices (bulk)
automations:readGET /automations, GET /automations/{id}, POST /automations/{id}/test, GET /automations/export, GET /automations/groups, GET /automations/{id}/history
automations:writePOST /automations, PUT /automations/{id}, PATCH /automations/{id}, PATCH /automations, DELETE /automations/{id}, POST /automations/import, POST /automations/{id}/clone, group CRUD
areas:readGET /areas
areas:writePOST /areas, PUT /areas/{id}/devices
scenes:readGET /scenes
scenes:writePOST /scenes, POST /scenes/{id}/activate
plugins:readGET /plugins, GET /plugins/{id}, GET /plugins/{id}/config
plugins:writeDELETE /plugins/{id}, PATCH /plugins/{id}, PUT /plugins/{id}/config, POST /plugins/{id}/start, POST /plugins/{id}/stop, POST /plugins/{id}/restart
users:writePOST /auth/users, PUT /auth/users/{id}, DELETE /auth/users/{id}

Public routes (no auth required): GET /health, POST /auth/login, POST /webhooks/{path}, GET /api/v1/events/stream (token in query param).

Logging in

TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"YOUR_PASSWORD"}' | jq -r .token)

Use the token in subsequent requests:

curl -s http://localhost:8080/api/v1/devices \
-H "Authorization: Bearer $TOKEN" | jq

Managing users

List users

curl -s http://localhost:8080/api/v1/auth/users \
-H "Authorization: Bearer $TOKEN" | jq

Create a user

# Dashboard user — read only
curl -s -X POST http://localhost:8080/api/v1/auth/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"dashboard","password":"secret","role":"ReadOnly"}' | jq

# Automation manager — can write automations but not manage users
curl -s -X POST http://localhost:8080/api/v1/auth/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"automation-mgr","password":"secret","role":"User"}' | jq

Change a user's password or role

curl -s -X PUT http://localhost:8080/api/v1/auth/users/USERNAME \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"password":"new-password","role":"User"}' | jq

Delete a user

curl -s -X DELETE http://localhost:8080/api/v1/auth/users/USERNAME \
-H "Authorization: Bearer $TOKEN"

IP whitelist bypass

Requests from whitelisted IPs are granted Admin-level access without a JWT:

# config/homecore.toml
[server]
whitelist = ["127.0.0.1", "192.168.1.0/24"]

Important: When a Bearer token IS present in the request, JWT validation runs regardless of the whitelist. The whitelist only applies to requests without any token.

This makes it safe to add your LAN subnet — dashboard apps on the same network get Admin access, but any app that presents a token is still validated normally.

WebSocket authentication

Browsers cannot set custom headers during a WebSocket upgrade, so the JWT is passed as a query parameter:

ws://homecore.local/api/v1/events/stream?token=<jwt>

The token is validated before the WebSocket upgrade is accepted. Invalid or missing tokens return HTTP 401 and the connection is never established.

Token lifetime

Tokens expire after token_expiry_hours (default: 24 hours). After expiry, re-authenticate to get a new token. There is no token refresh endpoint — just log in again.

If jwt_secret changes (or HomeCore restarts without a fixed secret), all existing tokens are immediately invalidated.

Setting a persistent JWT secret

By default, if no jwt_secret is configured, a random secret is generated at startup. This means all tokens expire on restart.

For production, set a fixed secret:

[auth]
jwt_secret = "use-openssl-rand-hex-32-for-this"

Generate a strong secret:

openssl rand -hex 32