Cookie + CSRF authentication for UniFi controllers.
Use this when the controller does not expose API-key auth (Cloud Key,
older UniFi OS releases) or when you need access to the legacy
/api/s/{site}/... and /v2/api/site/{site}/... endpoints, which
Ubiquiti has not exposed under x-api-key.
Quick start
client = UnifiApi.new(base_url: "https://192.168.1.1", verify_ssl: false)
{:ok, authed} =
UnifiApi.Auth.Cookie.login(client, "admin", "password",
style: :udm # or :cloud_key
)
# `authed` is a Req.Request with session cookies and CSRF token baked in.
# Pass it to any UnifiApi.Network or UnifiApi.Protect module.
UnifiApi.Network.Sites.list(authed)Style selection
Controllers use one of two login flows. Pass :style explicitly, or use
UnifiApi.detect/1 first and read the :auth_path field.
| Style | Login path | Auth header source |
|---|---|---|
:udm (UDM / UDM Pro / UDM SE / UCK-G2) | POST /api/auth/login | X-CSRF-Token response header |
:cloud_key (Cloud Key, standalone) | POST /api/login | csrf_token cookie |
CSRF rotation
UniFi controllers may rotate the CSRF token mid-session. login/4 returns
a static Req.Request.t() and does not auto-refresh.
For long-running pollers that perform writes, prefer
UnifiApi.Auth.Session — a supervised GenServer that holds the auth state
and auto-rotates CSRF from response headers on every request. This module
(Cookie) is the right call for one-shot scripts and tests.
Notes for the stateless flow:
- Read-only requests (GET) work indefinitely — CSRF is only enforced on mutating verbs.
- If a write returns 403, call
refresh_csrf/1(issues a lightweight GET against/and updates the token from the response header) or simply calllogin/4again.
Note: This module is implemented to the documented and observed shape of the UniFi login endpoints. It has been unit-tested against mocked
Req.Testplugs but not yet end-to-end against a live UDM Pro or Cloud Key. Please file an issue with the controller model and firmware version if you encounter shape mismatches.
Summary
Functions
Returns the CSRF token currently stored on the client, if any.
Authenticates against the controller and returns a session-bearing client.
Logs out and invalidates the controller-side session.
Refreshes the CSRF token by issuing a lightweight GET against the controller root and capturing the rotated token from the response header.
Types
Functions
@spec csrf_token(Req.Request.t()) :: String.t() | nil
Returns the CSRF token currently stored on the client, if any.
@spec login(Req.Request.t(), String.t(), String.t(), keyword()) :: {:ok, Req.Request.t()} | {:error, UnifiApi.Error.t()}
Authenticates against the controller and returns a session-bearing client.
Options
:style—:udm(default) or:cloud_key. Selects the login path.:remember— for:cloud_key, sets therememberflag on the login payload (default:false).
Returns
{:ok, %Req.Request{}}— the request struct has session cookies in its headers and the captured CSRF token under:private.unifi_api_csrfso subsequent calls can replay it.{:error, %UnifiApi.AuthError{}}— credentials rejected (HTTP 401/403).{:error, term()}— transport or unexpected response.
@spec logout( Req.Request.t(), keyword() ) :: :ok | {:error, :not_logged_in | UnifiApi.Error.t()}
Logs out and invalidates the controller-side session.
Returns:
:okfor 2xx — caller may safely discard the request struct.{:error, :not_logged_in}for 401/403 — the session was already gone (expired, controller reboot, etc.). Treat as success if the end goal is "session is gone on the controller side".{:error, %UnifiApi.ApiError{}}for other HTTP statuses — typically a 500 (controller fault) or 4xx that isn't an auth rejection.
Best-effort — the client struct is not modified, since Req.Request
is not mutated in place. Discard the request after calling this.
@spec refresh_csrf(Req.Request.t()) :: {:ok, Req.Request.t()} | {:error, UnifiApi.Error.t()}
Refreshes the CSRF token by issuing a lightweight GET against the controller root and capturing the rotated token from the response header.
Useful when a mutating call has returned 403 due to CSRF expiry and you want to retry without a full re-login.
Returns a new Req.Request.t() with the refreshed token; the original
is unchanged.
The probe path is hardcoded to "/" — every UniFi controller flavour
serves an unauthenticated root that rotates the CSRF cookie/header.
Earlier releases accepted a :probe_path option, but that opened a
path-traversal / SSRF surface (callers could point the session at an
arbitrary internal URL) without adding value. Removed in v0.4.0.