requests-unifi-auth
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.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| requests_unifi_auth-0.2.0.tar.gz | 27.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| requests_unifi_auth-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 38.1 kB
Release files / requests_unifi_auth-0.2.0.tar.gz
| Download URL | requests_unifi_auth-0.2.0.tar.gz |
|---|---|
| Size | 27.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
22fb7b96e7ff351749aa299f15cc9e711fda53ee1db0ede3511c4bbf00563057
|
|
BLAKE2b-256 checksum How to use checksums |
58c5fc1229d2590c19bb46ab418045dc77b75b805b240a00c825b24e733e3d5a
|
| 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 logRelease files / requests_unifi_auth-0.2.0-py3-none-any.whl
| Download URL | requests_unifi_auth-0.2.0-py3-none-any.whl |
|---|---|
| Size | 10.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ec1f4ddd5eeecddebbfabb95c3a2ac821d6efcce74e25787fb55a241834e131b
|
|
BLAKE2b-256 checksum How to use checksums |
1f8fef207bfb46bf9bdaa013d9c4fb1e39b96054bca9df2e5af463702e4c224f
|
| 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