Skip to main content

requests-unifi-auth

PYPI coverage UniFi Network MIT license versions CodeFactor Downloads

Authentication for the UniFi Controller and UniFi OS Web UI APIs using Python Requests. The auth handler manages login cookies, CSRF tokens, and one bounded reauthentication attempt.

For a curl-like command-line interface built on this package, see uictlapi.

Installation

pip install requests-unifi-auth

Examples

Read traffic policy-based routes

>>> import json
>>> import requests
>>> from requests_unifi_auth import UnifiControllerAuth
>>> auth = UnifiControllerAuth("your_username", "your_password", "controller.example")
>>> resp = requests.get(
...     "https://controller.example/proxy/network/v2/api/site/default/trafficroutes",
...     auth=auth,
... )
>>> print(json.dumps(resp.json(), indent=4))
[
    {
        "_id": "68fd349fcs1d3724f0021e3t",
        "description": "My Cool Domains Rule",
        "domains": [
            {
                "domain": "example.com",
                "port_ranges": [],
                "ports": []
            }
        ],
        "enabled": true,
        "ip_addresses": [],
        "ip_ranges": [],
        "kill_switch_enabled": true,
        "matching_target": "DOMAIN",
        "network_id": "78fd3e21c31v5424f0021d25",
        "next_hop": "",
        "regions": [],
        "target_devices": [
            {
                "type": "ALL_CLIENTS"
            }
        ]
    },
    {
        "_id": "68fd3ff1x31d2224d2023f56",
        "description": "Yet Another Cool Domain Rule",
        "domains": [
            {
                "domain": "foo.com",
                "port_ranges": [],
                "ports": []
            },
            {
                "domain": "bar.com",
                "port_ranges": [],
                "ports": []
            }
        ],
        "enabled": true,
        "ip_addresses": [],
        "ip_ranges": [],
        "kill_switch_enabled": false,
        "matching_target": "DOMAIN",
        "network_id": "78fd3e21c31v5424f0021d25",
        "next_hop": "",
        "regions": [],
        "target_devices": [
            {
                "type": "ALL_CLIENTS"
            }
        ]
    }
]

Update traffic policy-based route

>>> import json
>>> import requests
>>> from requests_unifi_auth import UnifiControllerAuth
>>> s = requests.Session()
>>> s.auth = UnifiControllerAuth("your_username", "your_password", "controller.example")
>>> resp = s.get(
...     "https://controller.example/proxy/network/v2/api/site/default/trafficroutes"
... )
>>> rules = resp.json()
>>> updated_rule = rules[0]
>>> updated_rule["domains"].append({"domain": "test.com", "port_ranges": [], "ports": []})
>>> resp = s.put(
...     "https://controller.example/proxy/network/v2/api/site/default/trafficroutes/68fd349fcs1d3724f0021e3t",
...     json=updated_rule,
... )
>>> print(json.dumps(resp.json(), indent=4))
{
    "_id": "68fd349fcs1d3724f0021e3t",
    "description": "My Cool Domains Rule",
    "domains": [
        {
            "domain": "example.com",
            "port_ranges": [],
            "ports": []
        },
        {
            "domain": "test.com", 
            "port_ranges": [], 
            "ports": []
        }
    ],
    "enabled": true,
    "ip_addresses": [],
    "ip_ranges": [],
    "kill_switch_enabled": true,
    "matching_target": "DOMAIN",
    "network_id": "78fd3e21c31v5424f0021d25",
    "next_hop": "",
    "regions": [],
    "target_devices": [
        {
            "type": "ALL_CLIENTS"
        }
    ]
}

Security and request behavior

controller_netloc is an authority, such as controller.example, controller.example:8443, or [2001:db8::10]:8443. HTTPS is the default and the login URL is derived only from this configured authority. Hostname case, IDNA, bracketed IPv6, and default ports are normalized before comparison.

The handler sends managed cookies and CSRF tokens only to that exact origin. It rejects cross-origin and HTTPS-to-HTTP redirects with UnsafeRedirectError. A same-origin redirect is followed only before the auth handler has any cookie or CSRF state, and only when neither the request nor the redirect response carries that state. After the first successful login, every redirect for that auth instance is rejected, including redirects from later requests. This check also raises when the caller uses allow_redirects=False, because a bare Requests AuthBase hook cannot observe that option or safely replace the session cookie jar during later redirect processing. Send a new request to the inspected target if your controller relies on that flow.

Response hooks registered after this auth handler are trusted not to rewrite the response URL or Location header. An AuthBase hook cannot revalidate a target changed by a later hook. The internal login response is never exposed in Response.history; on an outer redirect chain, Requests may also omit the challenged 401 handled internally. Do not use Response.history as an audit log of authentication attempts.

Plaintext HTTP authentication requires both explicit settings:

auth = UnifiControllerAuth(
    "user",
    "password",
    "controller.example",
    scheme="http",
    allow_insecure_http=True,
)

This sends the password without transport encryption. Use it only in an isolated test environment. verify=False is different: it keeps encryption but disables server identity verification. Prefer a trusted certificate or a CA bundle:

session = requests.Session()
session.verify = "/path/to/controller-ca.pem"
session.auth = UnifiControllerAuth("user", "password", "controller.example")

The handler retries a challenged request at most once. It rewinds seekable bodies to their original position and raises Requests' UnrewindableBodyError before retrying a generator or another body that cannot be replayed safely. Authentication state is synchronized across concurrent requests, but Requests does not guarantee that arbitrary concurrent use of one Session is thread-safe.

Compatibility

See COMPATIBILITY.md for live end-to-end results against real UniFi controllers.

Live end-to-end tests

These tests talk to a real controller on your LAN. They are skipped in CI and in a default pytest run (-m "not e2e").

1. Create a dedicated controller account

In UniFi OS → Admins / Users, add a local user used only for these tests (do not use your owner / Super Admin account):

  • Username example: e2e-requests-unifi-auth
  • Role: a least-privilege Network application role for the site under test (currently default) that can create, update, and delete user groups
  • Do not grant Owner / Super Admin, SSH, or access to Protect / Access / Talk unless you must
  • Use a long random password stored only in the config file (or a password manager)

The generated config disables writes. Set UNIFI_E2E_ENABLE_WRITE=true only when the account and controller are suitable for a temporary, unassigned user group. The test creates and renames one uniquely named group, then deletes it and verifies that it is absent. A failed or unverifiable cleanup fails the run.

2. Create the credentials file (outside the git tree)

Do not put passwords inside the repository clone or in cloud-synced folders.

Interactive setup (prompts for host, username, password, TLS and write-test flags):

chmod +x scripts/init_e2e_config.sh
./scripts/init_e2e_config.sh

The script writes ~/.config/requests-unifi-auth/e2e.env with mode 600 (or $XDG_CONFIG_HOME/requests-unifi-auth/e2e.env). To use another path:

UNIFI_E2E_CONFIG=/absolute/path/to/e2e.env ./scripts/init_e2e_config.sh

Optional overrides after the file exists:

  • Environment variables with the same UNIFI_E2E_* names override values from the file
  • Template without secrets: e2e.config.example.env

3. Run the live suite

From the repository root, use the project virtualenv (so pytest is on PATH):

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[test]"
pytest -m e2e

Without activating the venv:

.venv/bin/pip install -e ".[test]"
.venv/bin/pytest -m e2e

COMPATIBILITY.md is updated only when the complete required suite passes with UNIFI_E2E_ENABLE_WRITE=true, including cleanup. A read-only or selected run does not publish a full compatibility result. Commit the matrix update only when you want to publish it. On failure, redacted diagnostics are written to e2e-diagnostics.md — attach that file when opening a GitHub issue (use the E2E failure template). Never paste passwords, cookies, or raw CSRF tokens.

Metadata

Release files for requests-unifi-auth 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for requests-unifi-auth 0.2.1
File Size Uploaded
requests_unifi_auth-0.2.1.tar.gz 40.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for requests-unifi-auth 0.2.1
File Interpreter ABI Platform
requests_unifi_auth-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 50.6 kB

Release files / requests_unifi_auth-0.2.1.tar.gz

Download URL requests_unifi_auth-0.2.1.tar.gz
Size 40.0 kB
Tags Source
SHA-256 checksum
How to use checksums
4e29328f4d36219cbcca01e03aff38f9dfe18d4ce6917c11c36323bce6abfeaa
BLAKE2b-256 checksum
How to use checksums
66397c2800e7689687aacc0fa228c3a3bf3f2dce8e7f8f91812bf1f513a30c80
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release files / requests_unifi_auth-0.2.1-py3-none-any.whl

Download URL requests_unifi_auth-0.2.1-py3-none-any.whl
Size 10.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c0600e99ad4c3c63d4b4c03a3f59aa07ee02f730eb3ab601a7fef29ddb47f438
BLAKE2b-256 checksum
How to use checksums
eaa3e8309454447adcd32e395fb67d67bc1f628994a98dff90a4398741cb0863
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.0

2 release files

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page