UnifiApi.Auth.Cookie (UnifiApi v0.4.0)

Copy Markdown View Source

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.

StyleLogin pathAuth header source
:udm (UDM / UDM Pro / UDM SE / UCK-G2)POST /api/auth/loginX-CSRF-Token response header
:cloud_key (Cloud Key, standalone)POST /api/logincsrf_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 call login/4 again.

Note: This module is implemented to the documented and observed shape of the UniFi login endpoints. It has been unit-tested against mocked Req.Test plugs 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

style()

@type style() :: :udm | :cloud_key

Functions

csrf_token(request)

@spec csrf_token(Req.Request.t()) :: String.t() | nil

Returns the CSRF token currently stored on the client, if any.

login(client, username, password, opts \\ [])

@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 the remember flag 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_csrf so subsequent calls can replay it.
  • {:error, %UnifiApi.AuthError{}} — credentials rejected (HTTP 401/403).
  • {:error, term()} — transport or unexpected response.

logout(client, opts \\ [])

@spec logout(
  Req.Request.t(),
  keyword()
) :: :ok | {:error, :not_logged_in | UnifiApi.Error.t()}

Logs out and invalidates the controller-side session.

Returns:

  • :ok for 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.

refresh_csrf(client)

@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.