Skip to content

Support self-hosted UniFi OS Server - #1035

Merged
KodeStar merged 5 commits into
linuxserver:masterfrom
brandon-dacrib:fix/unifi-os-self-hosted
Sep 16, 2026
Merged

KodeStar merged 5 commits into
linuxserver:masterfrom
brandon-dacrib:fix/unifi-os-self-hosted

Conversation

@brandon-dacrib

Copy link
Copy Markdown
Contributor

Summary

Make the enhanced UniFi app work against a self-hosted UniFi OS Server (model UOSSERVER).

The config toggle is labelled "Self Hosted", but it selects an API, not a hosting model. Self-hosting is no longer the same thing as running the legacy UniFi Network application — Ubiquiti now ships UniFi OS Server, which you host yourself and which speaks the UniFi OS API.

So the intuitive choice for anyone hosting their own controller is the wrong one. Enabling the toggle points Heimdall at /api/login, and a UniFi OS device answers that path with a bare 401 without ever checking the credentials:

POST /api/login       -> 401 {"error":{"code":401,"message":"Unauthorized"}}
POST /api/auth/login  -> 403 {"message":"Invalid username or password", ...}

Heimdall reports Invalid credentials, which sends users looking for a password problem that does not exist. Note the wrong endpoint does not 404, so the status code alone cannot distinguish this from a genuine bad password.

Changes

  • Name the toggle after the API it selects, and say which devices should leave it off. The config key stays self_hosted, so existing items keep working.
  • Retry a refused login once against the other endpoint pair, keeping whichever one the device actually implements.

Only a refused login (401/403/404) retries, so a working configuration is never affected. The retry reuses the same login attributes, because the first request never reached authentication — confirmed below. A retry is accepted on any status other than the same flat refusal, including 499, so an account requiring two-factor authentication surfaces that instead of falling back to the misleading message.

Validation

Against a live self-hosted UniFi OS Server (UOSSERVER, UniFi OS 1.0.1) on :11443:

Configuration Before After
Toggle on, 2FA account Invalid credentials Unexpected response (HTTP 499) — reaches the real endpoint
Toggle on, working credentials Invalid credentials Successfully connected to UniFi
Toggle off (control) Successfully connected to UniFi unchanged
Bad credentials, toggle on or off Invalid credentials unchanged
livestats() with the toggle on error tile error tile, no fatal

Also verified that a login refused at the wrong endpoint does not consume a one-time 2FA code: the same code was accepted by /api/auth/login immediately afterwards, and the resulting session returned 200 from the stats endpoint. That is what makes reusing the attributes for the retry safe.

Static checks: PHP 8.2 syntax check, PHPCS with PSR-12 and the repository's multiline-function exclusion, node testRunner.js, git diff --check.

Notes

  • The legacy Network application half of the fallback is unchanged in behaviour and could not be exercised against real legacy hardware; the retry is written so that path is only reached after a login has already been refused.
  • This touches the same two files as Transmission #1 (TOTP support) and will need a trivial rebase depending on merge order. The two changes are independent, and the table above shows they compose: with Transmission #1 applied, the first row reads Two-factor authentication required.

UniFi OS rejects a TOTP code that has already been used, so the dashboard
refresh loop failed on every livestats poll that landed in an already-spent
time step. Cache a successful result until the next code is due so at most
one login happens per period; nothing is cached when no TOTP is configured.

Verified against a live UniFi OS Server that a wrong or replayed code is
rejected with 401/403 rather than 499, so report that case as a credential
or TOTP failure and reserve the 499 message for a missing second factor.

Also guard the stats response against a null connection result before
decoding it.
The UniFi config toggle is labelled "Self Hosted", but it selects an API
rather than a hosting model. Self-hosting is no longer the same thing as
running the legacy UniFi Network application: a self-hosted UniFi OS Server
(model UOSSERVER) speaks the UniFi OS API, so enabling the toggle - the
intuitive choice for anyone hosting their own controller - points Heimdall at
/api/login, which a UniFi OS device answers with a bare 401 without ever
checking the credentials. The app then reports "Invalid credentials", which
sends users looking for a password problem that does not exist.

Relabel the toggle after the API it selects, and retry a refused login once
against the other endpoint pair, keeping whichever one the device actually
implements. Only a refused login retries, so a working configuration is
unaffected, and the retry reuses the same attributes because the first
request never reached authentication.

The config key stays self_hosted so existing items keep working.
…-hosted

Resolve the overlap in test() and livestats(): authenticate() now runs
inside the InvalidArgumentException handling added for malformed TOTP
URIs, and the TOTP result cache wraps the endpoint-fallback login.
@KodeStar
KodeStar merged commit 7bb5596 into linuxserver:master Sep 16, 2026
3 checks passed
@LinuxServer-CI LinuxServer-CI moved this from PRs to Done in Issue & PR Tracker Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants