Compare commits
26 Commits
v1.0.0-rc3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
763ab5cd5a | ||
|
|
843c415f88 | ||
|
|
9d9025164a | ||
|
|
d251ebfb92 | ||
|
|
d3c359064d | ||
|
|
c7370ed266 | ||
|
|
04abb0fdb3 | ||
|
|
7f657411ad | ||
|
|
1bcc65d649 | ||
|
|
819b3fb679 | ||
|
|
8205c846f6 | ||
|
|
f51e84849e | ||
|
|
5250fd2de9 | ||
|
|
a154ffc197 | ||
|
|
c0a6e03fa7 | ||
|
|
fe706abdb5 | ||
|
|
1e5b89fa32 | ||
|
|
2a5d3e589f | ||
|
|
3ba65adc8b | ||
|
|
f90a7d5346 | ||
|
|
084e0e606e | ||
|
|
16c45544d3 | ||
|
|
556e9a0fbf | ||
|
|
a027c532fe | ||
|
|
681610241d | ||
|
|
02babe0022 |
20
.github/ISSUE_TEMPLATE/0-anything-else.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Anything else
|
||||||
|
about: If your issue isn't any of the other types below (please review those first)
|
||||||
|
title: ''
|
||||||
|
labels: question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe your issue**
|
||||||
|
A clear and concise description of what the issue is.
|
||||||
|
|
||||||
|
**Version**
|
||||||
|
- Home Assistant version: `for example 2026.4.1`
|
||||||
|
- Home Assistant install method: `for example Container or HA OS`
|
||||||
|
- Integration version: `for example 1.0.2`
|
||||||
|
- If applicable for a frontend bug, browser (type and version): `for example Chrome 102`
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Something is going wrong (or not matching expected behavior)
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Version**
|
||||||
|
- Home Assistant version: `for example 2026.4.1`
|
||||||
|
- Home Assistant install method: `for example Container or HA OS`
|
||||||
|
- Integration version: `for example 1.0.2`
|
||||||
|
- If applicable for a frontend bug, browser (type and version): `for example Chrome 102`
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Feature Request
|
||||||
|
url: https://github.com/christiaangoossens/hass-oidc-auth/discussions/categories/ideas
|
||||||
|
about: Please submit your feature request in the Ideas section of the Discussions.
|
||||||
|
- name: Question
|
||||||
|
url: https://github.com/christiaangoossens/hass-oidc-auth/discussions/categories/q-a
|
||||||
|
about: You can ask your question in the Q&A section of the Discussions.
|
||||||
2
.github/workflows/lint.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
- name: Sync dependencies
|
- name: Sync dependencies
|
||||||
|
|||||||
2
.github/workflows/security.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
- name: Scan dependencies for vulnerabilities
|
- name: Scan dependencies for vulnerabilities
|
||||||
|
|||||||
2
.github/workflows/test.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
- name: Sync dependencies
|
- name: Sync dependencies
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ If you are not a programmer, you can still contribute by:
|
|||||||
- If you want to, contributing financially through [Github Sponsors](https://github.com/sponsors/christiaangoossens)
|
- If you want to, contributing financially through [Github Sponsors](https://github.com/sponsors/christiaangoossens)
|
||||||
|
|
||||||
## Code contributions
|
## Code contributions
|
||||||
You may also submit Pull Requests (PRs) to add features yourself! You can find a list that we are currently working on below. Please note that workflows will be run on your pull request and a pull request will only be merged when all checks pass and a review has been conducted (together with a manual test).
|
You may also submit Pull Requests (PRs) to add features yourself! You can find TODOs to work on in the [Issue Tracker](https://github.com/christiaangoossens/hass-oidc-auth/issues), the [Feature Requests](https://github.com/christiaangoossens/hass-oidc-auth/discussions/categories/ideas) and in the [FAQ](./docs/faq.md).
|
||||||
|
|
||||||
|
|
||||||
|
Please note that workflows will be run on your pull request (linting, tests, security audit) and a pull request will only be merged when all checks pass and a review has been conducted (together with a manual test).
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
This project uses the uv package manager for development. You can find installation instructions here: https://docs.astral.sh/uv/getting-started/installation/. Start by installing the dependencies using `uv sync` and then point your editor towards the environment created in the .venv directory.
|
This project uses the uv package manager for development. You can find installation instructions here: https://docs.astral.sh/uv/getting-started/installation/. Start by installing the dependencies using `uv sync` and then point your editor towards the environment created in the .venv directory.
|
||||||
|
|||||||
71
README.md
@@ -15,21 +15,20 @@
|
|||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/christiaangoossens/hass-oidc-auth/">
|
<a href="https://github.com/christiaangoossens/hass-oidc-auth/">
|
||||||
<img src="logo.png" alt="Logo" width="80" height="80">
|
<img src="https://raw.githubusercontent.com/christiaangoossens/hass-oidc-auth/main/docs/logo.png" alt="Logo" width="80" height="80">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">OpenID Connect for Home Assistant</h3>
|
<h3 align="center">OpenID Connect for Home Assistant</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
OpenID Connect (OIDC) implementation for Home Assistant through a custom component/integration
|
OpenID Connect (OIDC) implementation for Home Assistant through a custom component/integration,<br/>with a strong focus on <b>security, stability and accessibility.</b>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a href="./docs/usage.md">Usage Guide</a>
|
<a href="https://github.com/christiaangoossens/hass-oidc-auth/blob/main/docs/configuration.md">YAML Configuration Guide</a>
|
||||||
·
|
·
|
||||||
<a href="./docs/configuration.md">Configuration Guide</a>
|
<a href="https://github.com/christiaangoossens/hass-oidc-auth/blob/main/CONTRIBUTING.md">Contribution Guide</a>
|
||||||
·
|
·
|
||||||
<a href="./CONTRIBUTING.md">Contribution Guide</a>
|
<a href="https://github.com/christiaangoossens/hass-oidc-auth/blob/main/docs/faq.md">Frequently Asked Questions (FAQ)</a>
|
||||||
<br />
|
|
||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/christiaangoossens/hass-oidc-auth/discussions?discussions_q=is%3Aopen+category%3AAnnouncements+category%3APolls">Announcements & Polls</a>
|
<a href="https://github.com/christiaangoossens/hass-oidc-auth/discussions?discussions_q=is%3Aopen+category%3AAnnouncements+category%3APolls">Announcements & Polls</a>
|
||||||
·
|
·
|
||||||
@@ -41,52 +40,64 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Provides an OpenID Connect (OIDC) implementation for Home Assistant through a custom component/integration. Through this integration, you can create an SSO (single-sign-on) environment within your self-hosted application stack / homelab.
|
Provides a **stable and secure** OpenID Connect (OIDC) implementation for Home Assistant through a custom component/integration. With this integration, you can create a single-sign-on (SSO) environment in your self-hosted application stack / homelab.
|
||||||
|
|
||||||
### Background
|
The core values for this integration are:
|
||||||
If you would like to read the background/open letter that lead to this component, you can find the original post at https://community.home-assistant.io/t/open-letter-for-improving-home-assistants-authentication-system-oidc-sso/494223. It is currently one of the most upvoted feature requests for Home Assistant.
|
|
||||||
|
1. **Security**: strict adherence to the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html), [RFC 6749 (OAuth2)](https://datatracker.ietf.org/doc/html/rfc6749), [RFC 7519 (JWT)](https://datatracker.ietf.org/doc/html/rfc7519), [RFC 7636 (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636) and [RFC 9700 (OAuth2 Security Best Practices)](https://datatracker.ietf.org/doc/html/rfc9700) as well as a focus on security tests in the automated test suite.
|
||||||
|
2. **Stability**: minimal patching of the core Home Assistant code such that updates of HA are less likely to break the integration and leave you without a way to login.
|
||||||
|
3. **Accessibility**: the integration should work for everyone as much as possible with default settings, regardless of your preferred authentication method.
|
||||||
|
|
||||||
|
**TLDR**: *Login to Home Assistant with this integration should 'just work', every time, for everyone in your household ([even your dad](https://github.com/home-assistant/architecture/issues/832#issuecomment-1328052330)), securely.*
|
||||||
|
|
||||||
|
If you are deciding if this integration is the right fit for your setup, please see the [Frequently Asked Questions (FAQ)](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/docs/faq.md) for more information.
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> If you support the addition of this feature to the Home Assistant core, please upvote https://github.com/orgs/home-assistant/discussions/48. It's the successor of the Home Assistant Community post mentioned above (with almost 900 upvotes).
|
|
||||||
|
|
||||||
## Installation guide
|
## Installation guide
|
||||||
|
|
||||||
1. Add this repository to [HACS](https://hacs.xyz/) (or search for "OpenID Connect" in HACS).
|
The easiest way to install the integration is through [the Home Assistant Community Store (HACS)](https://hacs.xyz/). You can find usage instructions for HACS here: https://hacs.xyz/docs/use/.
|
||||||
|
|
||||||
|
After installing HACS, search for "OpenID Connect" in the HACS search box or click the button below:
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
||||||
|
|
||||||
2. Add the YAML configuration that matches your OIDC provider to `configuration.yaml`. See the [Configuration Guide](./docs/configuration.md) for more details or pick your OIDC provider below:
|
Next, set up your OIDC provider. You can find setup guides for common providers here:
|
||||||
|
|
||||||
| <img src="https://goauthentik.io/img/icon_top_brand_colour.svg" width="100"> | <img src="https://www.authelia.com/images/branding/logo-cropped.png" width="100"> | <img src="https://github.com/user-attachments/assets/4ceb2708-9f29-4694-b797-be833efce17d" width="100"> |
|
| <img src="https://goauthentik.io/img/icon_top_brand_colour.svg" width="100"> | <img src="https://www.authelia.com/images/branding/logo-cropped.png" width="100"> | <img src="https://github.com/user-attachments/assets/4ceb2708-9f29-4694-b797-be833efce17d" width="100"> |
|
||||||
|:-----------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
|
|:-----------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
|
||||||
| [Authentik](./docs/provider-configurations/authentik.md) | [Authelia](./docs/provider-configurations/authelia.md) | [Pocket ID](./docs/provider-configurations/pocket-id.md) |
|
| [authentik](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/docs/provider-configurations/authentik.md) | [Authelia](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/docs/provider-configurations/authelia.md) | [Pocket ID](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/docs/provider-configurations/pocket-id.md) |
|
||||||
|
|
||||||
By default, the integration assumes you configure Home Assistant as a **public client** and thus only specify the `client_id` and no `client_secret`. For example, your configuration might look like:
|
You can also find additional provider guides in the [the Provider Configurations folder](https://github.com/christiaangoossens/hass-oidc-auth/tree/main/docs/provider-configurations). If your provider isn't specified, you can use either a **public client** (recommended) or **confidential client** with the callback URL set to `<your HA URL>/auth/oidc/callback`.
|
||||||
|
|
||||||
```yaml
|
Finally, choose your preferred configuration style (UI or YAML). After configuration, you should automatically be sent to the OIDC login page(s) if you open Home Assistant (web or app).
|
||||||
auth_oidc:
|
|
||||||
client_id: "example"
|
|
||||||
discovery_url: "https://example.com/.well-known/openid-configuration"
|
|
||||||
```
|
|
||||||
|
|
||||||
When registering Home Assistant at your OIDC provider, use `<your HA URL>/auth/oidc/callback` as the callback URL and select 'public client'. You should now get the `client_id` and `issuer_url` or `discovery_url` to fill in.
|
### Configuration in the HA UI
|
||||||
|
|
||||||
3. Restart Home Assistant
|
The recommended setup method for beginners is through the "Integrations" panel within the Home Assistant UI.
|
||||||
|
|
||||||
4. Login through the OIDC Welcome URL at `<your HA URL>/auth/oidc/welcome`. You will have to go there manually for now. For example, it might be located at http://homeassistant.local:8123/auth/oidc/welcome.
|
Many configuration options are available through this method, but some advanced features are only available in YAML to simplify the setup process in the UI.
|
||||||
|
|
||||||
More (detailed) usage instructions can be found in the [Usage Guide](./docs/usage.md).
|
1. Open Home Assistant and go to **Settings -> Devices & Services**.
|
||||||
|
2. Click Add Integration and select **OpenID Connect/SSO Authentication**.
|
||||||
|
3. Follow the prompts on screen carefully.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Configuration by YAML
|
||||||
|
|
||||||
|
Alternatively, you can configure the integration using YAML. You can find a full configuration guide for YAML here: [YAML Configuration Guide](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/docs/configuration.md).
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
Contibutions are very welcome! If you program in Python or have worked with Home Assistant integrations before, please try to contribute. A list of requested contributions/future goals is in the [Contribution Guide](./CONTRIBUTING.md).
|
Contributions are very welcome! If you program in Python or have worked with Home Assistant integrations before, please try to contribute. You can find more information in the [Contribution Guide](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/CONTRIBUTING.md).
|
||||||
|
|
||||||
Please see the [Contribution Guide](./CONTRIBUTING.md) for more information.
|
### Security issue?
|
||||||
|
Please see [SECURITY.md](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/SECURITY.md) for more information on how to submit your security issue securely. You can find previously found vulnerabilities and their corresponding security advisories at the [Security Advisories page](https://github.com/christiaangoossens/hass-oidc-auth/security/advisories).
|
||||||
|
|
||||||
### Found a security issue?
|
## Background
|
||||||
Please see [SECURITY.md](./SECURITY.md) for more information on how to submit your security issue securely. You can find previously found vulnerablities and their corresponding security advisories at the [Security Advisories page](https://github.com/christiaangoossens/hass-oidc-auth/security/advisories).
|
If you would like to read the background/open letter that led to this component, you can find it at https://github.com/orgs/home-assistant/discussions/48. It is currently one of the most upvoted feature requests for Home Assistant.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Distributed under the MIT license with no warranty. You are fully liable for configuring this integration correctly to keep your Home Assistant installation secure. Use at your own risk. The full license can be found in [LICENSE.md](./LICENSE.md)
|
Distributed under the MIT license with no warranty. You are fully liable for configuring this integration correctly to keep your Home Assistant installation secure. Use at your own risk. The full license can be found in [LICENSE.md](https://github.com/christiaangoossens/hass-oidc-auth/blob/main/LICENSE.md)
|
||||||
|
|
||||||
|
|
||||||
<!-- MARKDOWN LINKS & IMAGES -->
|
<!-- MARKDOWN LINKS & IMAGES -->
|
||||||
@@ -100,4 +111,4 @@ Distributed under the MIT license with no warranty. You are fully liable for con
|
|||||||
[issues-shield]: https://img.shields.io/github/issues/christiaangoossens/hass-oidc-auth.svg?style=for-the-badge
|
[issues-shield]: https://img.shields.io/github/issues/christiaangoossens/hass-oidc-auth.svg?style=for-the-badge
|
||||||
[issues-url]: https://github.com/christiaangoossens/hass-oidc-auth/issues
|
[issues-url]: https://github.com/christiaangoossens/hass-oidc-auth/issues
|
||||||
[license-shield]: https://img.shields.io/github/license/christiaangoossens/hass-oidc-auth.svg?style=for-the-badge
|
[license-shield]: https://img.shields.io/github/license/christiaangoossens/hass-oidc-auth.svg?style=for-the-badge
|
||||||
[license-url]: https://github.com/christiaangoossens/hass-oidc-auth/blob/master/LICENSE.txt
|
[license-url]: https://github.com/christiaangoossens/hass-oidc-auth/blob/main/LICENSE.md
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import OrderedDict
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.components.http import StaticPathConfig
|
||||||
|
|
||||||
# Import and re-export config schema explictly
|
# Import and re-export config schema explictly
|
||||||
# pylint: disable=useless-import-alias
|
# pylint: disable=useless-import-alias
|
||||||
@@ -27,6 +28,7 @@ from .config import (
|
|||||||
ROLES,
|
ROLES,
|
||||||
NETWORK,
|
NETWORK,
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE,
|
FEATURES_INCLUDE_GROUPS_SCOPE,
|
||||||
|
FEATURES_DEFAULT_REDIRECT,
|
||||||
FEATURES_FORCE_HTTPS,
|
FEATURES_FORCE_HTTPS,
|
||||||
REQUIRED_SCOPES,
|
REQUIRED_SCOPES,
|
||||||
)
|
)
|
||||||
@@ -42,6 +44,7 @@ from .endpoints import (
|
|||||||
OIDCDeviceSSE,
|
OIDCDeviceSSE,
|
||||||
)
|
)
|
||||||
from .tools.oidc_client import OIDCClient
|
from .tools.oidc_client import OIDCClient
|
||||||
|
from .tools.types import OIDCWelcomeOptions
|
||||||
from .provider import OpenIDAuthProvider
|
from .provider import OpenIDAuthProvider
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -87,24 +90,60 @@ async def async_unload_entry(_hass: HomeAssistant, _entry: ConfigEntry):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_name: str):
|
async def _register_oidc_provider(hass: HomeAssistant, my_config: dict):
|
||||||
"""Set up the OIDC provider with the given configuration."""
|
"""Register the OIDC provider in Home Assistant's auth system."""
|
||||||
providers = OrderedDict()
|
|
||||||
|
|
||||||
# Use private APIs until there is a real auth platform
|
# Use private APIs until there is a real auth platform
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
providers = OrderedDict()
|
||||||
provider = OpenIDAuthProvider(hass, hass.auth._store, my_config)
|
provider = OpenIDAuthProvider(hass, hass.auth._store, my_config)
|
||||||
|
|
||||||
|
existing_auth_providers = hass.auth._providers.copy()
|
||||||
|
_LOGGER.debug("Current auth providers: %s", list(existing_auth_providers.keys()))
|
||||||
|
auth_provider_count = len(existing_auth_providers)
|
||||||
|
has_trusted_networks_provider_first = False
|
||||||
|
|
||||||
|
if auth_provider_count > 0:
|
||||||
|
# Pop the first provider from the existing providers to check if it's trusted_networks
|
||||||
|
first_provider_key, first_provider_obj = next(
|
||||||
|
iter(existing_auth_providers.items())
|
||||||
|
)
|
||||||
|
existing_auth_providers.pop(first_provider_key)
|
||||||
|
|
||||||
|
if first_provider_key[0] == "trusted_networks":
|
||||||
|
_LOGGER.info(
|
||||||
|
"Trusted Networks provider detected as the first auth provider. "
|
||||||
|
+ "Keeping registration order intact."
|
||||||
|
)
|
||||||
|
providers[first_provider_key] = first_provider_obj
|
||||||
|
has_trusted_networks_provider_first = True
|
||||||
|
else:
|
||||||
|
# Reset back to what we had before
|
||||||
|
existing_auth_providers = hass.auth._providers.copy()
|
||||||
|
|
||||||
|
# Register OIDC at the start of the array
|
||||||
|
# OIDC needs to be first because it needs to process the login cookie after sign-in
|
||||||
providers[(provider.type, provider.id)] = provider
|
providers[(provider.type, provider.id)] = provider
|
||||||
|
|
||||||
# Get current provider count
|
# Add back any other providers that were already registered
|
||||||
has_other_auth_providers = len(hass.auth._providers) > 0
|
providers.update(existing_auth_providers)
|
||||||
|
|
||||||
providers.update(hass.auth._providers)
|
_LOGGER.debug("Final auth providers: %s", list(providers.values()))
|
||||||
hass.auth._providers = providers
|
hass.auth._providers = providers
|
||||||
# pylint: enable=protected-access
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
_LOGGER.info("Registered OIDC provider")
|
_LOGGER.info("Registered OIDC provider")
|
||||||
|
return provider, auth_provider_count, has_trusted_networks_provider_first
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_name: str):
|
||||||
|
"""Set up the OIDC provider with the given configuration."""
|
||||||
|
(
|
||||||
|
provider,
|
||||||
|
auth_provider_count,
|
||||||
|
has_trusted_networks_provider_first,
|
||||||
|
) = await _register_oidc_provider(hass, my_config)
|
||||||
|
|
||||||
# Set the correct scopes
|
# Set the correct scopes
|
||||||
# Always use 'openid' & 'profile' as they are specified in the OIDC spec
|
# Always use 'openid' & 'profile' as they are specified in the OIDC spec
|
||||||
@@ -145,9 +184,32 @@ async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_nam
|
|||||||
name = re.sub(r"[^A-Za-z0-9 _\-\(\)]", "", name)
|
name = re.sub(r"[^A-Za-z0-9 _\-\(\)]", "", name)
|
||||||
|
|
||||||
force_https = features_config.get(FEATURES_FORCE_HTTPS, False)
|
force_https = features_config.get(FEATURES_FORCE_HTTPS, False)
|
||||||
|
default_redirect = features_config.get(FEATURES_DEFAULT_REDIRECT, False)
|
||||||
|
|
||||||
|
await hass.http.async_register_static_paths(
|
||||||
|
[
|
||||||
|
StaticPathConfig(
|
||||||
|
"/auth/oidc/static/style.css",
|
||||||
|
hass.config.path("custom_components/auth_oidc/static/style.css"),
|
||||||
|
cache_headers=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
has_only_trusted_networks = (
|
||||||
|
auth_provider_count == 1 and has_trusted_networks_provider_first
|
||||||
|
)
|
||||||
|
|
||||||
hass.http.register_view(
|
hass.http.register_view(
|
||||||
OIDCWelcomeView(provider, name, force_https, has_other_auth_providers)
|
OIDCWelcomeView(
|
||||||
|
provider,
|
||||||
|
OIDCWelcomeOptions(
|
||||||
|
name=name,
|
||||||
|
force_https=force_https,
|
||||||
|
has_other_auth_providers=auth_provider_count > 0,
|
||||||
|
prefers_skipping=default_redirect or has_only_trusted_networks,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
hass.http.register_view(OIDCDeviceSSE(provider))
|
hass.http.register_view(OIDCDeviceSSE(provider))
|
||||||
hass.http.register_view(OIDCRedirectView(oidc_client, provider, force_https))
|
hass.http.register_view(OIDCRedirectView(oidc_client, provider, force_https))
|
||||||
@@ -157,6 +219,8 @@ async def _setup_oidc_provider(hass: HomeAssistant, my_config: dict, display_nam
|
|||||||
_LOGGER.info("Registered OIDC views")
|
_LOGGER.info("Registered OIDC views")
|
||||||
|
|
||||||
# Inject OIDC code into the frontend for /auth/authorize for automatic redirect
|
# Inject OIDC code into the frontend for /auth/authorize for automatic redirect
|
||||||
await OIDCInjectedAuthPage.inject(hass, force_https)
|
await OIDCInjectedAuthPage.inject(
|
||||||
|
hass, provider, force_https, has_trusted_networks_provider_first
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
BIN
custom_components/auth_oidc/brand/icon.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
custom_components/auth_oidc/brand/icon@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -8,7 +8,7 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
DEFAULT_TITLE = "OpenID Connect (SSO)"
|
DEFAULT_TITLE = "OpenID Connect (SSO)"
|
||||||
DOMAIN = "auth_oidc"
|
DOMAIN = "auth_oidc"
|
||||||
REPO_ROOT_URL = "https://github.com/christiaangoossens/hass-oidc-auth/tree/v1.0.0-rc3"
|
REPO_ROOT_URL = "https://github.com/christiaangoossens/hass-oidc-auth/tree/v1.0.2"
|
||||||
|
|
||||||
## ===
|
## ===
|
||||||
## Config keys
|
## Config keys
|
||||||
@@ -27,6 +27,7 @@ FEATURES_AUTOMATIC_PERSON_CREATION = "automatic_person_creation"
|
|||||||
FEATURES_DISABLE_PKCE = "disable_rfc7636"
|
FEATURES_DISABLE_PKCE = "disable_rfc7636"
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE = "include_groups_scope"
|
FEATURES_INCLUDE_GROUPS_SCOPE = "include_groups_scope"
|
||||||
FEATURES_FORCE_HTTPS = "force_https"
|
FEATURES_FORCE_HTTPS = "force_https"
|
||||||
|
FEATURES_DEFAULT_REDIRECT = "default_redirect"
|
||||||
CLAIMS = "claims"
|
CLAIMS = "claims"
|
||||||
CLAIMS_DISPLAY_NAME = "display_name"
|
CLAIMS_DISPLAY_NAME = "display_name"
|
||||||
CLAIMS_USERNAME = "username"
|
CLAIMS_USERNAME = "username"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .const import (
|
|||||||
FEATURES_DISABLE_PKCE,
|
FEATURES_DISABLE_PKCE,
|
||||||
FEATURES_INCLUDE_GROUPS_SCOPE,
|
FEATURES_INCLUDE_GROUPS_SCOPE,
|
||||||
FEATURES_FORCE_HTTPS,
|
FEATURES_FORCE_HTTPS,
|
||||||
|
FEATURES_DEFAULT_REDIRECT,
|
||||||
CLAIMS,
|
CLAIMS,
|
||||||
CLAIMS_DISPLAY_NAME,
|
CLAIMS_DISPLAY_NAME,
|
||||||
CLAIMS_USERNAME,
|
CLAIMS_USERNAME,
|
||||||
@@ -75,6 +76,13 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(FEATURES_FORCE_HTTPS, default=False): vol.Coerce(
|
vol.Optional(FEATURES_FORCE_HTTPS, default=False): vol.Coerce(
|
||||||
bool
|
bool
|
||||||
),
|
),
|
||||||
|
# Welcome page will be skipped automatically if there are no
|
||||||
|
# other auth providers.
|
||||||
|
# This flag enables this behavior regardless of the amount
|
||||||
|
# of other auth providers.
|
||||||
|
vol.Optional(
|
||||||
|
FEATURES_DEFAULT_REDIRECT, default=False
|
||||||
|
): vol.Coerce(bool),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
# Determine which specific claims will be used from the id_token
|
# Determine which specific claims will be used from the id_token
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from homeassistant.components.http import HomeAssistantView, StaticPathConfig
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .welcome import PATH as WELCOME_PATH
|
from .welcome import PATH as WELCOME_PATH
|
||||||
|
from ..provider import OpenIDAuthProvider
|
||||||
from ..tools.helpers import get_url
|
from ..tools.helpers import get_url
|
||||||
|
|
||||||
PATH = "/auth/authorize"
|
PATH = "/auth/authorize"
|
||||||
@@ -24,7 +25,12 @@ async def read_file(path: str) -> str:
|
|||||||
return await f.read()
|
return await f.read()
|
||||||
|
|
||||||
|
|
||||||
async def frontend_injection(hass: HomeAssistant, force_https: bool) -> None:
|
async def frontend_injection(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
provider: OpenIDAuthProvider,
|
||||||
|
force_https: bool,
|
||||||
|
has_trusted_networks_provider_first: bool,
|
||||||
|
) -> None:
|
||||||
"""Inject new frontend code into /auth/authorize."""
|
"""Inject new frontend code into /auth/authorize."""
|
||||||
router = hass.http.app.router
|
router = hass.http.app.router
|
||||||
frontend_path = None
|
frontend_path = None
|
||||||
@@ -67,7 +73,7 @@ async def frontend_injection(hass: HomeAssistant, force_https: bool) -> None:
|
|||||||
frontend_code = await read_file(frontend_path)
|
frontend_code = await read_file(frontend_path)
|
||||||
|
|
||||||
# Inject JS and register that route
|
# Inject JS and register that route
|
||||||
injection_js = "<script src='/auth/oidc/static/injection.js?v=6'></script>"
|
injection_js = "<script src='/auth/oidc/static/injection.js?v=7'></script>"
|
||||||
frontend_code = frontend_code.replace("</body>", f"{injection_js}</body>")
|
frontend_code = frontend_code.replace("</body>", f"{injection_js}</body>")
|
||||||
|
|
||||||
await hass.http.async_register_static_paths(
|
await hass.http.async_register_static_paths(
|
||||||
@@ -75,18 +81,17 @@ async def frontend_injection(hass: HomeAssistant, force_https: bool) -> None:
|
|||||||
StaticPathConfig(
|
StaticPathConfig(
|
||||||
"/auth/oidc/static/injection.js",
|
"/auth/oidc/static/injection.js",
|
||||||
hass.config.path("custom_components/auth_oidc/static/injection.js"),
|
hass.config.path("custom_components/auth_oidc/static/injection.js"),
|
||||||
cache_headers=False,
|
cache_headers=True,
|
||||||
),
|
)
|
||||||
StaticPathConfig(
|
|
||||||
"/auth/oidc/static/style.css",
|
|
||||||
hass.config.path("custom_components/auth_oidc/static/style.css"),
|
|
||||||
cache_headers=False,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# If everything is succesful, register a fake view that just returns the modified HTML
|
# If everything is succesful, register a fake view that just returns the modified HTML
|
||||||
hass.http.register_view(OIDCInjectedAuthPage(frontend_code, force_https))
|
hass.http.register_view(
|
||||||
|
OIDCInjectedAuthPage(
|
||||||
|
frontend_code, provider, force_https, has_trusted_networks_provider_first
|
||||||
|
)
|
||||||
|
)
|
||||||
_LOGGER.info("Performed OIDC frontend injection")
|
_LOGGER.info("Performed OIDC frontend injection")
|
||||||
|
|
||||||
|
|
||||||
@@ -97,21 +102,36 @@ class OIDCInjectedAuthPage(HomeAssistantView):
|
|||||||
url = PATH
|
url = PATH
|
||||||
name = "auth:oidc:authorize_page"
|
name = "auth:oidc:authorize_page"
|
||||||
|
|
||||||
def __init__(self, html: str, force_https: bool) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
html: str,
|
||||||
|
provider: OpenIDAuthProvider,
|
||||||
|
force_https: bool,
|
||||||
|
has_trusted_networks_provider_first: bool,
|
||||||
|
) -> None:
|
||||||
"""Initialize the injected auth page."""
|
"""Initialize the injected auth page."""
|
||||||
self.html = html
|
self.html = html
|
||||||
|
self.provider = provider
|
||||||
self.force_https = force_https
|
self.force_https = force_https
|
||||||
|
self.has_trusted_networks_provider_first = has_trusted_networks_provider_first
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def inject(hass: HomeAssistant, force_https: bool) -> None:
|
async def inject(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
provider: OpenIDAuthProvider,
|
||||||
|
force_https: bool,
|
||||||
|
has_trusted_networks_provider_first: bool,
|
||||||
|
) -> None:
|
||||||
"""Inject the OIDC auth page into the frontend."""
|
"""Inject the OIDC auth page into the frontend."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await frontend_injection(hass, force_https)
|
await frontend_injection(
|
||||||
|
hass, provider, force_https, has_trusted_networks_provider_first
|
||||||
|
)
|
||||||
except Exception as e: # pylint: disable=broad-except
|
except Exception as e: # pylint: disable=broad-except
|
||||||
_LOGGER.error("Failed to inject OIDC auth page: %s", e)
|
_LOGGER.error("Failed to inject OIDC auth page: %s", e)
|
||||||
|
|
||||||
@staticmethod
|
def _should_do_oidc_redirect(self, req: web.Request) -> bool:
|
||||||
def _should_do_oidc_redirect(req: web.Request) -> bool:
|
|
||||||
"""Check if we should redirect to the OIDC flow."""
|
"""Check if we should redirect to the OIDC flow."""
|
||||||
# Set when we return from finish
|
# Set when we return from finish
|
||||||
if req.query.get("skip_oidc_redirect") == "true":
|
if req.query.get("skip_oidc_redirect") == "true":
|
||||||
@@ -123,14 +143,25 @@ class OIDCInjectedAuthPage(HomeAssistantView):
|
|||||||
if not redirect_uri:
|
if not redirect_uri:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check if we are on a trusted network if we have trusted networks registered first
|
||||||
|
if (
|
||||||
|
self.has_trusted_networks_provider_first
|
||||||
|
and self.provider.is_trusted_network_host()
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
# Handle both encoded and plain redirect_uri values.
|
# Handle both encoded and plain redirect_uri values.
|
||||||
decoded_redirect_uri = unquote(redirect_uri)
|
decoded_redirect_uri = unquote(redirect_uri)
|
||||||
return "skip_oidc_redirect=true" not in decoded_redirect_uri
|
return "skip_oidc_redirect=true" not in decoded_redirect_uri
|
||||||
|
|
||||||
def _get_welcome_redirect_location(self, req: web.Request) -> str:
|
def _get_welcome_redirect_location(self, req: web.Request) -> str:
|
||||||
"""Build the welcome URL for the injected auth page redirect."""
|
"""Build the welcome URL for the injected auth page redirect."""
|
||||||
|
url = str(req.url)
|
||||||
|
if self.force_https:
|
||||||
|
url = url.replace("http://", "https://")
|
||||||
|
|
||||||
encoded_current_url = quote(
|
encoded_current_url = quote(
|
||||||
base64.b64encode(str(req.url).encode("utf-8")).decode("ascii")
|
base64.b64encode(url.encode("utf-8")).decode("ascii")
|
||||||
)
|
)
|
||||||
return get_url(
|
return get_url(
|
||||||
f"{WELCOME_PATH}?redirect_uri={encoded_current_url}",
|
f"{WELCOME_PATH}?redirect_uri={encoded_current_url}",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Welcome route to show the user the OIDC login button and give instructions."""
|
"""Welcome route to show the user the OIDC login button and give instructions."""
|
||||||
|
|
||||||
from ast import List
|
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
from urllib.parse import urlparse, parse_qs, unquote, urlencode
|
from urllib.parse import urlparse, parse_qs, unquote, urlencode
|
||||||
@@ -8,6 +7,7 @@ from aiohttp import web
|
|||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from ..tools.helpers import error_response, get_url, template_response
|
from ..tools.helpers import error_response, get_url, template_response
|
||||||
from ..provider import OpenIDAuthProvider
|
from ..provider import OpenIDAuthProvider
|
||||||
|
from ..tools.types import OIDCWelcomeOptions
|
||||||
|
|
||||||
PATH = "/auth/oidc/welcome"
|
PATH = "/auth/oidc/welcome"
|
||||||
|
|
||||||
@@ -20,18 +20,15 @@ class OIDCWelcomeView(HomeAssistantView):
|
|||||||
name = "auth:oidc:welcome"
|
name = "auth:oidc:welcome"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, oidc_provider: OpenIDAuthProvider, options: OIDCWelcomeOptions
|
||||||
oidc_provider: OpenIDAuthProvider,
|
|
||||||
name: str,
|
|
||||||
force_https: bool,
|
|
||||||
has_other_auth_providers: bool,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.oidc_provider = oidc_provider
|
self.oidc_provider = oidc_provider
|
||||||
self.name = name
|
self.name = options.get("name")
|
||||||
self.force_https = force_https
|
self.force_https = options.get("force_https")
|
||||||
self.has_other_auth_providers = has_other_auth_providers
|
self.has_other_auth_providers = options.get("has_other_auth_providers")
|
||||||
|
self.prefers_skipping = options.get("prefers_skipping")
|
||||||
|
|
||||||
async def _process_url(self, redirect_uri: str) -> List[str, bool]:
|
async def _process_url(self, redirect_uri: str) -> tuple[str, bool]:
|
||||||
"""Processes the redirect URI to determine if we need setTokens and if this is mobile."""
|
"""Processes the redirect URI to determine if we need setTokens and if this is mobile."""
|
||||||
# decodeURIComponent(btoa(...)) -> unquote first, then base64 decode
|
# decodeURIComponent(btoa(...)) -> unquote first, then base64 decode
|
||||||
redirect_uri = base64.b64decode(unquote(redirect_uri), validate=True).decode(
|
redirect_uri = base64.b64decode(unquote(redirect_uri), validate=True).decode(
|
||||||
@@ -108,7 +105,9 @@ class OIDCWelcomeView(HomeAssistantView):
|
|||||||
|
|
||||||
# If this is the only provider and we are on desktop,
|
# If this is the only provider and we are on desktop,
|
||||||
# automatically go through the OIDC login
|
# automatically go through the OIDC login
|
||||||
if not is_mobile and not self.has_other_auth_providers:
|
if not is_mobile and (
|
||||||
|
not self.has_other_auth_providers or self.prefers_skipping
|
||||||
|
):
|
||||||
raise web.HTTPFound(
|
raise web.HTTPFound(
|
||||||
location=get_url("/auth/oidc/redirect", self.force_https),
|
location=get_url("/auth/oidc/redirect", self.force_https),
|
||||||
headers=cookie_header,
|
headers=cookie_header,
|
||||||
|
|||||||
@@ -18,5 +18,5 @@
|
|||||||
"jinja2",
|
"jinja2",
|
||||||
"joserfc"
|
"joserfc"
|
||||||
],
|
],
|
||||||
"version": "1.0.0-rc3"
|
"version": "1.0.2"
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,12 @@ import logging
|
|||||||
|
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
import asyncio
|
import asyncio
|
||||||
from homeassistant.auth import EVENT_USER_ADDED
|
from ipaddress import (
|
||||||
|
ip_address,
|
||||||
|
IPv4Address,
|
||||||
|
IPv6Address,
|
||||||
|
)
|
||||||
|
from homeassistant.auth import EVENT_USER_ADDED, InvalidAuthError as HAInvalidAuthError
|
||||||
from homeassistant.auth.providers import (
|
from homeassistant.auth.providers import (
|
||||||
AUTH_PROVIDERS,
|
AUTH_PROVIDERS,
|
||||||
AuthProvider,
|
AuthProvider,
|
||||||
@@ -20,7 +25,6 @@ from homeassistant.auth.providers import (
|
|||||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.components import http, person
|
from homeassistant.components import http, person
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
|
|
||||||
from .config.const import (
|
from .config.const import (
|
||||||
FEATURES,
|
FEATURES,
|
||||||
@@ -31,6 +35,8 @@ from .config.const import (
|
|||||||
from .stores.state_store import StateStore
|
from .stores.state_store import StateStore
|
||||||
from .tools.types import UserDetails
|
from .tools.types import UserDetails
|
||||||
|
|
||||||
|
type IPAddress = IPv4Address | IPv6Address
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PROVIDER_TYPE = "auth_oidc"
|
PROVIDER_TYPE = "auth_oidc"
|
||||||
@@ -38,7 +44,7 @@ HASS_PROVIDER_TYPE = "homeassistant"
|
|||||||
COOKIE_NAME = "auth_oidc_state"
|
COOKIE_NAME = "auth_oidc_state"
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(HomeAssistantError):
|
class InvalidAuthError(HAInvalidAuthError):
|
||||||
"""Raised when submitting invalid authentication."""
|
"""Raised when submitting invalid authentication."""
|
||||||
|
|
||||||
|
|
||||||
@@ -59,8 +65,11 @@ class OpenIDAuthProvider(AuthProvider):
|
|||||||
{
|
{
|
||||||
# Currently register as default, might be used when we have multiple OIDC providers
|
# Currently register as default, might be used when we have multiple OIDC providers
|
||||||
CONF_ID: "default",
|
CONF_ID: "default",
|
||||||
# Name displayed in the UI
|
# Stable label for HA's native auth-picker row. Kept fixed so the
|
||||||
CONF_NAME: config.get("display_name", DEFAULT_TITLE),
|
# frontend-injection script can match it without threading the
|
||||||
|
# user-configurable display_name through. The user's display_name
|
||||||
|
# is still rendered on the welcome page.
|
||||||
|
CONF_NAME: DEFAULT_TITLE,
|
||||||
# Type
|
# Type
|
||||||
CONF_TYPE: PROVIDER_TYPE,
|
CONF_TYPE: PROVIDER_TYPE,
|
||||||
},
|
},
|
||||||
@@ -114,6 +123,41 @@ class OpenIDAuthProvider(AuthProvider):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def is_trusted_network_host(self) -> bool:
|
||||||
|
"""Check if the current request is coming from a trusted network host."""
|
||||||
|
ip = self._resolve_ip()
|
||||||
|
if ip is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if trusted networks auth provider is present
|
||||||
|
trusted_network_provider = self.hass.auth.get_auth_provider(
|
||||||
|
"trusted_networks", None
|
||||||
|
)
|
||||||
|
if not trusted_network_provider:
|
||||||
|
return False
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Trusted networks present and checking if we should OIDC redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
trusted_network_provider.async_validate_access(ip_address(ip))
|
||||||
|
_LOGGER.info("IP %s is in a trusted network, skipping OIDC flow", ip)
|
||||||
|
return True
|
||||||
|
except HAInvalidAuthError:
|
||||||
|
# Log the error
|
||||||
|
_LOGGER.info(
|
||||||
|
"IP %s is not in a trusted network, proceeding with OIDC flow", ip
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
# Catch every other error, HA might have changed the API.
|
||||||
|
# pylint: disable=broad-exception-caught
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Error while validating trusted network for IP %s: %s", ip, e
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
async def async_create_state(self, redirect_uri: str, ip: str | None = None) -> str:
|
async def async_create_state(self, redirect_uri: str, ip: str | None = None) -> str:
|
||||||
"""Create a new OIDC state and return the state id."""
|
"""Create a new OIDC state and return the state id."""
|
||||||
if self._state_store is None:
|
if self._state_store is None:
|
||||||
|
|||||||
@@ -1,37 +1,55 @@
|
|||||||
/**
|
/**
|
||||||
* hass-oidc-auth - UX script to automatically select the Home Assistant auth provider when the "Login aborted" message is shown.
|
* Frontend helpers for /auth/authorize: auto-select on aborted login,
|
||||||
|
* and route picker clicks on our provider through /auth/oidc/welcome.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const OIDC_PROVIDER_NAME = "OpenID Connect (SSO)" // matches provider.py CONF_NAME
|
||||||
|
const OIDC_WELCOME_PATH = "/auth/oidc/welcome"
|
||||||
|
|
||||||
let authFlowElement = null
|
let authFlowElement = null
|
||||||
|
let pickerIntercepted = false
|
||||||
|
|
||||||
|
function interceptPickerRow(authProviderElement) {
|
||||||
|
if (pickerIntercepted) return
|
||||||
|
if (!authProviderElement) return
|
||||||
|
if (!authProviderElement.shadowRoot) {
|
||||||
|
console.warn("[OIDC] ha-pick-auth-provider has no shadowRoot; HA frontend may have changed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const items = authProviderElement.shadowRoot.querySelectorAll('ha-list-item')
|
||||||
|
if (items.length === 0) return // not yet populated; retry on next mutation
|
||||||
|
for (const item of items) {
|
||||||
|
if ((item.innerText || '').trim() !== OIDC_PROVIDER_NAME) continue
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
window.location.href =
|
||||||
|
OIDC_WELCOME_PATH +
|
||||||
|
'?redirect_uri=' + encodeURIComponent(btoa(window.location.href))
|
||||||
|
}, true)
|
||||||
|
pickerIntercepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
// Find ha-auth-flow
|
authFlowElement = document.querySelector('ha-auth-flow')
|
||||||
authFlowElement = document.querySelector('ha-auth-flow');
|
if (!authFlowElement) return
|
||||||
|
|
||||||
if (!authFlowElement) {
|
const authProviderElement = document.querySelector('ha-pick-auth-provider')
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the text "Login aborted" is present on the page
|
// Intercept picker clicks so the OIDC cookie is set before submit.
|
||||||
if (!authFlowElement.innerText.includes('Login aborted')) {
|
interceptPickerRow(authProviderElement)
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the ha-pick-auth-provider element
|
// Auto-select on "Login aborted".
|
||||||
const authProviderElement = document.querySelector('ha-pick-auth-provider');
|
if (!authFlowElement.innerText.includes('Login aborted')) return
|
||||||
|
if (!authProviderElement) return
|
||||||
if (!authProviderElement) {
|
const firstListItem = authProviderElement.shadowRoot?.querySelector('ha-list-item')
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click the first ha-list-item element inside the ha-pick-auth-provider
|
|
||||||
const firstListItem = authProviderElement.shadowRoot?.querySelector('ha-list-item');
|
|
||||||
if (!firstListItem) {
|
if (!firstListItem) {
|
||||||
console.warn("[OIDC] No ha-list-item found inside ha-pick-auth-provider. Not automatically selecting HA provider.");
|
console.warn("[OIDC] No ha-list-item found inside ha-pick-auth-provider. Not automatically selecting OIDC provider.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
firstListItem.click()
|
||||||
firstListItem.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the content until ready
|
// Hide the content until ready
|
||||||
@@ -58,4 +76,5 @@ setTimeout(() => {
|
|||||||
|
|
||||||
// Force display the content
|
// Force display the content
|
||||||
document.querySelector(".content").style.display = "";
|
document.querySelector(".content").style.display = "";
|
||||||
|
update();
|
||||||
}, 300)
|
}, 300)
|
||||||
@@ -372,7 +372,7 @@ class OIDCClient:
|
|||||||
tcp_connector_args["ssl"] = ssl_context
|
tcp_connector_args["ssl"] = ssl_context
|
||||||
|
|
||||||
self.http_session = aiohttp.ClientSession(
|
self.http_session = aiohttp.ClientSession(
|
||||||
connector=aiohttp.TCPConnector(**tcp_connector_args)
|
trust_env=True, connector=aiohttp.TCPConnector(**tcp_connector_args)
|
||||||
)
|
)
|
||||||
return self.http_session
|
return self.http_session
|
||||||
|
|
||||||
|
|||||||
@@ -39,3 +39,19 @@ class OIDCState(dict):
|
|||||||
|
|
||||||
# IP address
|
# IP address
|
||||||
ip_address: str | None
|
ip_address: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class OIDCWelcomeOptions(dict):
|
||||||
|
"""Options for the welcome screen"""
|
||||||
|
|
||||||
|
# User friendly SSO name to display
|
||||||
|
name: str
|
||||||
|
|
||||||
|
# Does the user force HTTPS on all generated URLs?
|
||||||
|
force_https: bool
|
||||||
|
|
||||||
|
# Has the user registered any other auth providers?
|
||||||
|
has_other_auth_providers: bool
|
||||||
|
|
||||||
|
# Does the user prefer to skip the welcome screen?
|
||||||
|
prefers_skipping: bool
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="/auth/oidc/static/style.css">
|
<link rel="stylesheet" href="/auth/oidc/static/style.css?v=2">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-gray-300 bg-white p-6 text-left">
|
<div class="rounded-lg border border-gray-300 bg-white p-6 text-left">
|
||||||
<div class="mb-4 flex items-center justify-between text-gray-700">
|
<h2 class="mb-2 text-lg font-semibold text-gray-800">Use a code from another device</h2>
|
||||||
<span class="text-lg font-semibold">Use a code from another device</span>
|
|
||||||
</div>
|
|
||||||
<div class="border-t border-gray-200 pt-4">
|
|
||||||
<p class="mb-2 text-sm text-gray-600">On your other device, open the Home Assistant app. You will see a
|
<p class="mb-2 text-sm text-gray-600">On your other device, open the Home Assistant app. You will see a
|
||||||
6-digit code.</p>
|
6-digit code.</p>
|
||||||
<p class="mb-4 text-sm text-gray-600">Input that code here and click Approve to login on the other device.
|
<p class="mb-4 text-sm text-gray-600">Input that code here and click Approve to login on the other device.
|
||||||
@@ -62,5 +59,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
# Configuration methods
|
# UI Configuration
|
||||||
|
|
||||||
Currently, the only available configuration method is YAML in your `configuration.yaml` file. In the future, we will also add limited UI configuration for the most common configurations (Authentik, Authelia and Pocket-ID). Advanced users will need to use the YAML configuration in any case.
|
If you want to use the (limited) UI configuration method, please see [the README](../README.md).
|
||||||
|
|
||||||
# YAML Configuration
|
# YAML Configuration
|
||||||
For now, this integration is configured using YAML in your `configuration.yaml` file. By default, only two fields are required:
|
|
||||||
|
You can configure this integration using YAML in your `configuration.yaml` file. All features of the integration will always be available within the YAML configuration.
|
||||||
|
|
||||||
|
By default, only two fields are required:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
auth_oidc:
|
auth_oidc:
|
||||||
@@ -11,7 +14,7 @@ auth_oidc:
|
|||||||
discovery_url: ""
|
discovery_url: ""
|
||||||
```
|
```
|
||||||
|
|
||||||
The default settings assume that you configure Home Assistant as a **public client**, without a client secret. If so, you should only need to provide the `client_id` from your OIDC provider and it's discovery URL (ending in `.well-known/openid-configuration`).
|
The default settings assume that you configure Home Assistant as a **public client**, without a client secret. If so, you should only need to provide the `client_id` from your OIDC provider and its discovery URL (ending in `.well-known/openid-configuration`).
|
||||||
You don't have to configure other settings in most cases, as they have secure defaults set. If your provider requires manually configuring the callback URL, use `<your HA URL>/auth/oidc/callback`.
|
You don't have to configure other settings in most cases, as they have secure defaults set. If your provider requires manually configuring the callback URL, use `<your HA URL>/auth/oidc/callback`.
|
||||||
|
|
||||||
## Provider Configurations
|
## Provider Configurations
|
||||||
@@ -23,6 +26,7 @@ Here are some documentation links for specific providers that you may want to fo
|
|||||||
* [Kanidm](./provider-configurations/kanidm.md)
|
* [Kanidm](./provider-configurations/kanidm.md)
|
||||||
* [Microsoft Entra ID](./provider-configurations/microsoft-entra.md)
|
* [Microsoft Entra ID](./provider-configurations/microsoft-entra.md)
|
||||||
* [Zitadel](./provider-configurations/zitadel.md)
|
* [Zitadel](./provider-configurations/zitadel.md)
|
||||||
|
* [Keycloak](./provider-configurations/keycloak.md)
|
||||||
|
|
||||||
_Missing a provider? Submit your guide using a PR._
|
_Missing a provider? Submit your guide using a PR._
|
||||||
|
|
||||||
@@ -75,6 +79,19 @@ auth_oidc:
|
|||||||
|
|
||||||
This will show the provider on the login screen as: "Login with Example".
|
This will show the provider on the login screen as: "Login with Example".
|
||||||
|
|
||||||
|
### Skipping the welcome screen
|
||||||
|
If you would like to skip the welcome screen, you can either enable the `features.default_redirect` feature, or [disable the Home Assistant auth provider](https://github.com/christiaangoossens/hass-oidc-auth/discussions/67).
|
||||||
|
|
||||||
|
If you want to keep the default login (backup login) enabled, but still skip the welcome screen by default, you can configure the following yaml:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_oidc:
|
||||||
|
features:
|
||||||
|
default_redirect: true
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have this feature enabled and you would like to use the backup login, make sure to append `?skip_oidc_redirect=true` to your login URL. For example, if your HA is at `https://ha.example.com`, you can go to `https://ha.example.com/?skip_oidc_redirect=true` to see the HA username/password login screen.
|
||||||
|
|
||||||
### Forcing HTTPS
|
### Forcing HTTPS
|
||||||
First check if you are setting the header `X-Forwarded-Proto` in your proxy and if the [proxy settings for Home Assistant](https://www.home-assistant.io/integrations/http/#use_x_forwarded_for) are configured correctly. You should also check if IP addresses in your logs actually match the origin IP (instead of proxy IP). If you cannot find any mistakes, you may use the following config option to force HTTPS regardless:
|
First check if you are setting the header `X-Forwarded-Proto` in your proxy and if the [proxy settings for Home Assistant](https://www.home-assistant.io/integrations/http/#use_x_forwarded_for) are configured correctly. You should also check if IP addresses in your logs actually match the origin IP (instead of proxy IP). If you cannot find any mistakes, you may use the following config option to force HTTPS regardless:
|
||||||
|
|
||||||
@@ -86,7 +103,7 @@ auth_oidc:
|
|||||||
|
|
||||||
### Disabling registration for new users
|
### Disabling registration for new users
|
||||||
This integration does not allow disabling registration for new users, as there is no way to abort registration that late in the process while providing a good user experience.
|
This integration does not allow disabling registration for new users, as there is no way to abort registration that late in the process while providing a good user experience.
|
||||||
You can however set both roles to groups that only contain certain users or to a non-existant group.
|
You can however set both roles to groups that only contain certain users or to a non-existent group.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
auth_oidc:
|
auth_oidc:
|
||||||
@@ -151,18 +168,18 @@ Here's a table of all options that you can set:
|
|||||||
| `discovery_url` | `string` | Yes | | The OIDC well-known configuration URL. |
|
| `discovery_url` | `string` | Yes | | The OIDC well-known configuration URL. |
|
||||||
| `display_name` | `string` | No | `"OpenID Connect (SSO)"` | The name to display on the login screen, both for the Home Assistant screen and the OIDC welcome screen. |
|
| `display_name` | `string` | No | `"OpenID Connect (SSO)"` | The name to display on the login screen, both for the Home Assistant screen and the OIDC welcome screen. |
|
||||||
| `id_token_signing_alg` | `string` | No | `RS256` | The signing algorithm that is used for your id_tokens.
|
| `id_token_signing_alg` | `string` | No | `RS256` | The signing algorithm that is used for your id_tokens.
|
||||||
| `groups_scope` | `string` | No | `groups` | Override the default grups scope with another scope of your choice. |
|
| `groups_scope` | `string` | No | `groups` | Override the default groups scope with another scope of your choice. |
|
||||||
| `additional_scopes`|`list of strings`| No | `empty list` | Add additional scopes to request for custom identity provider configurations in addition to the automatic `openid` and `profile` scopes and the `groups_scope` configuration option |
|
| `additional_scopes`|`list of strings`| No | `empty list` | Add additional scopes to request for custom identity provider configurations in addition to the automatic `openid` and `profile` scopes and the `groups_scope` configuration option |
|
||||||
| `features.automatic_user_linking` | `boolean`| No | `false` | Automatically links users to existing Home Assistant users based on the OIDC username claim. Disabled by default for security. When disabled, OIDC users will get their own new user profile upon first login. |
|
| `features.automatic_user_linking` | `boolean`| No | `false` | Automatically links users to existing Home Assistant users based on the OIDC username claim. Disabled by default for security. When disabled, OIDC users will get their own new user profile upon first login. |
|
||||||
| `features.automatic_person_creation` | `boolean` | No | `true` | Automatically creates a person entry for new user profiles created by this integration. Recommended if you would like to assign presence detection to OIDC users. |
|
| `features.automatic_person_creation` | `boolean` | No | `true` | Automatically creates a person entry for new user profiles created by this integration. Recommended if you would like to assign presence detection to OIDC users. |
|
||||||
| `features.disable_rfc7636` | `boolean`| No | `false` | Disables PKCE (RFC 7636) for OIDC providers that don't support it. You should not need this with most providers. |
|
| `features.disable_rfc7636` | `boolean`| No | `false` | Disables PKCE (RFC 7636) for OIDC providers that don't support it. You should not need this with most providers. |
|
||||||
| `features.include_groups_scope` | `boolean` | No | `true` | Include the 'groups' scope in the OIDC request. Set to `false` to exclude it. |
|
| `features.include_groups_scope` | `boolean` | No | `true` | Include the 'groups' scope in the OIDC request. Set to `false` to exclude it. |
|
||||||
| `features.disable_frontend_changes` | `boolean` | No | `false` | Set to `true` to disable all changes made to the HA frontend for better compatbility with future HA versions, or if you are not comfortable with injecting Javascript into the existing frontend code. |
|
|
||||||
| `features.force_https` | `boolean` | No | `false` | Set to `true` to force all URLs generated to use `https` instead of automatically determining based on the request scheme or `X-Forwarded-Proto`. |
|
| `features.force_https` | `boolean` | No | `false` | Set to `true` to force all URLs generated to use `https` instead of automatically determining based on the request scheme or `X-Forwarded-Proto`. |
|
||||||
|
| `features.default_redirect` | `boolean` | No | `false` | Set to `true` to always skip the welcome screen (on desktop), regardless of if there are any other auth providers registered. |
|
||||||
| `claims.display_name` | `string` | No | `name` | The claim to use to obtain the display name.
|
| `claims.display_name` | `string` | No | `name` | The claim to use to obtain the display name.
|
||||||
| `claims.username` | `string` | No | `preferred_username` | The claim to use to obtain the username.
|
| `claims.username` | `string` | No | `preferred_username` | The claim to use to obtain the username.
|
||||||
| `claims.groups` | `string` | No | `groups` | The claim to use to obtain the user's group(s). |
|
| `claims.groups` | `string` | No | `groups` | The claim to use to obtain the user's group(s). |
|
||||||
| `roles.admin` | `string` | No | `admins` | Group name to require for users to get the 'admin' role in Home Assistant. Defaults to 'admins', the default group name for admins in Authentik. Doesn't do anything if no groups claim is found in your token. |
|
| `roles.admin` | `string` | No | `admins` | Group name to require for users to get the 'admin' role in Home Assistant. Defaults to 'admins', the default group name for admins in Authentik. Doesn't do anything if no groups claim is found in your token. |
|
||||||
| `roles.user` | `string` | No | | Group name to require for users to get the 'user' role in Home Assistant. Defaults to giving all users this role, unless configured. |
|
| `roles.user` | `string` | No | | Group name to require for users to get the 'user' role in Home Assistant. Defaults to giving all users this role, unless configured. |
|
||||||
| `network.tls_verify` | `boolean` | No | `true` | Verify TLS certificate. You may want to set this set to `false` when testing locally. |
|
| `network.tls_verify` | `boolean` | No | `true` | Verify TLS certificate. You may want to set this to `false` when testing locally. |
|
||||||
| `network.tls_ca_path` | `string` | No | | Path to file containing a private certificate authority chain. |
|
| `network.tls_ca_path` | `string` | No | | Path to file containing a private certificate authority chain. |
|
||||||
|
|||||||
59
docs/faq.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
## What are the values of this project? Why would I choose this integration over alternatives?
|
||||||
|
|
||||||
|
Provides a **stable and secure** OpenID Connect (OIDC) implementation for Home Assistant through a custom component/integration. With this integration, you can create a single-sign-on (SSO) environment in your self-hosted application stack / homelab.
|
||||||
|
|
||||||
|
The core values for this integration are:
|
||||||
|
|
||||||
|
1. **Security**: strict adherence to the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html), [RFC 6749 (OAuth2)](https://datatracker.ietf.org/doc/html/rfc6749), [RFC 7519 (JWT)](https://datatracker.ietf.org/doc/html/rfc7519), [RFC 7636 (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636) and [RFC 9700 (OAuth2 Security Best Practices)](https://datatracker.ietf.org/doc/html/rfc9700) as well as a focus on security tests in the automated test suite.
|
||||||
|
2. **Stability**: minimal patching of the core Home Assistant code such that updates of HA are less likely to break the integration and leave you without a way to login.
|
||||||
|
3. **Accessibility**: the integration should work for everyone as much as possible with default settings, regardless of your preferred authentication method.
|
||||||
|
|
||||||
|
**TLDR**: *Login to Home Assistant with this integration should 'just work', every time, for everyone in your household ([even your dad](https://github.com/home-assistant/architecture/issues/832#issuecomment-1328052330)), securely.*
|
||||||
|
|
||||||
|
## Is the integration stable?
|
||||||
|
|
||||||
|
Yes, this integration has been tested in production environments for multiple years and has almost full automated test coverage to test both security and regressions. Security issues as well as dependency updates are actively monitored through automated pipelines and [a security policy is available here](../SECURITY.md).
|
||||||
|
|
||||||
|
## What does this integration not do (yet)?
|
||||||
|
|
||||||
|
The integration is currently very suitable for homelab use, but not for enterprise use, because these specs/todos have not been implemented yet:
|
||||||
|
|
||||||
|
- [OpenID Connect Session Management 1.0](https://openid.net/specs/openid-connect-session-1_0.html): users that are disabled at the IdP do not get logged out in Home Assistant until their refresh token expires/they logout manually
|
||||||
|
- [OpenID Connect Front-Channel Logout 1.0](https://openid.net/specs/openid-connect-frontchannel-1_0.html): logout in Home Assistant does not automatically log the user out at the IdP
|
||||||
|
- [OpenID Connect Back-Channel Logout 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-backchannel-1_0.html)
|
||||||
|
- *Open TODO*: Permissions are only set upon first login (https://github.com/christiaangoossens/hass-oidc-auth/discussions/187), as permission changes would necessitate revoking refresh tokens/implementing session management
|
||||||
|
- Other RFC's and best practices with regards to token expiration and revocation in the app itself
|
||||||
|
|
||||||
|
These features are hard to implement correctly within a custom integration, as they involve the full authentication lifecycle. Home Assistant does currently implement some features to see which refresh tokens were issued (and thus which sessions are open), which work well with this integration, but lacks any further security focussed features.
|
||||||
|
|
||||||
|
For home use where users rarely change permissions/status, these features aren't commonly required. However, if you would like to help implement any of these specifications (while sticking to the value of 'Stability' and minimal Home Assistant core code patching), feel free to create a PR.
|
||||||
|
|
||||||
|
## Why does this integration only allow for sign-in on mobile with a device code?
|
||||||
|
Several attempts have been made at implementing a direct mobile sign-in, but due to many issues (which can be found in https://github.com/orgs/home-assistant/discussions/48 and https://github.com/christiaangoossens/hass-oidc-auth/discussions/95), an approach was chosen that works for all setups and all authentication methods. The mobile apps now show a code, which can be entered into either the Chrome (Android)/Safari (iOS) apps on the mobile device or on another computer, after which the app automatically links and continues with the setup.
|
||||||
|
|
||||||
|
If you would like to make another attempt at implementing direct sign-in anyway, please submit a PR.
|
||||||
|
|
||||||
|
## I am using a proxy setup where my reverse proxy authenticates users
|
||||||
|
This integration is intended to be public-facing (as most OIDC apps). If you are authenticating users at the reverse proxy level (such as if you are migrating from https://github.com/BeryJu/hass-auth-header), **you should remove this authentication layer after installing this integration.**.
|
||||||
|
|
||||||
|
In general, make sure to set your Home Assistant configuration correctly for your reverse proxy as well (see https://www.home-assistant.io/integrations/http/#reverse-proxies). It is important that the original visitor IP is passed through to Home Assistant for optimal security.
|
||||||
|
|
||||||
|
## Help! I have no styling/CSS!
|
||||||
|
Did you install the integration using HACS, or directly using the `hass-oidc-auth.zip` from the release? If so, this might be a bug. Please submit an issue so we can debug this.
|
||||||
|
|
||||||
|
If you installed the integration by cloning master/downloading the source code (either from Github or the releases page), this is intended. You are using a development build in that case, for which you need to build the styles yourself using these instructions: [CONTRIBUTING.md](./CONTRIBUTING.md#compiling-css).
|
||||||
|
|
||||||
|
Installing development builds is not recommended! See the question below for more information.
|
||||||
|
|
||||||
|
## Why should I only install releases (either from Github or HACS) instead of using the source code?
|
||||||
|
The `main` branch is in constant development, both manually (PR's) as well as automatically (Renovate Bot/dependency management). While a CI pipeline with tests is run on the `main` branch, manual tests are only performed upon PR merge and during the release creation.
|
||||||
|
|
||||||
|
Therefore, issues that occur because of the use of the `main` branch (or any other specific commmit/release source code) will not be processed. At any time, the state of `main` may be broken, as it is only intended for development.
|
||||||
|
|
||||||
|
Releases, available at [Releases](https://github.com/christiaangoossens/hass-oidc-auth/releases), by constrast, are immutable and tested manually before release. While a release may be deleted, releases cannot be altered after publishing.
|
||||||
|
|
||||||
|
This ensures that - even if a security issue with my accounts occured - no malicious code enters an existing release and that the release is installed exactly as intended.
|
||||||
|
|
||||||
|
HACS automatically - and only - uses these releases and is thus the recommended install method. If you want to install a release manually, please specifically use the `hass-oidc-auth.zip` (and not the release source code zip), such that you get styling.
|
||||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@@ -1,6 +1,22 @@
|
|||||||
# Authelia
|
# Authelia
|
||||||
|
|
||||||
## Public client configuration
|
> [!TIP]
|
||||||
|
> This guide describes configuring Authelia using the UI method. You can also configure Authelia by hand with YAML. Instructions for configuring any provider using YAML can be found here: [YAML Configuration Guide](../configuration.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Step 1. Install the integration
|
||||||
|
|
||||||
|
Make sure that you have fully installed the latest release of the integration. The easiest way to install the integration is through [the Home Assistant Community Store (HACS)](https://hacs.xyz/). You can find usage instructions for HACS here: https://hacs.xyz/docs/use/.
|
||||||
|
|
||||||
|
After installing HACS, search for "OpenID Connect" in the HACS search box or click the button below:
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
||||||
|
|
||||||
|
## Step 2. Configure Authelia
|
||||||
|
|
||||||
|
You can choose between configuring Authelia as a public or confidential client.
|
||||||
|
|
||||||
|
### Public client configuration
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This configuration strictly requires a HTTPS redirect uri.
|
> This configuration strictly requires a HTTPS redirect uri.
|
||||||
@@ -17,24 +33,11 @@ identity_providers:
|
|||||||
public: true
|
public: true
|
||||||
require_pkce: true
|
require_pkce: true
|
||||||
pkce_challenge_method: 'S256'
|
pkce_challenge_method: 'S256'
|
||||||
authorization_policy: 'two_factor'
|
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
- 'https://hass.example.com/auth/oidc/callback'
|
- 'https://hass.example.com/auth/oidc/callback'
|
||||||
scopes:
|
|
||||||
- 'openid'
|
|
||||||
- 'profile'
|
|
||||||
- 'groups'
|
|
||||||
id_token_signed_response_alg: 'RS256'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Home Assistant `configuration.yaml`
|
### Confidential client configuration:
|
||||||
```yaml
|
|
||||||
auth_oidc:
|
|
||||||
client_id: "homeassistant"
|
|
||||||
discovery_url: "https://auth.example.com/.well-known/openid-configuration"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Confidential client configuration:
|
|
||||||
|
|
||||||
Authelia `configuration.yml`
|
Authelia `configuration.yml`
|
||||||
```yaml
|
```yaml
|
||||||
@@ -49,21 +52,43 @@ identity_providers:
|
|||||||
public: false
|
public: false
|
||||||
require_pkce: true
|
require_pkce: true
|
||||||
pkce_challenge_method: 'S256'
|
pkce_challenge_method: 'S256'
|
||||||
authorization_policy: 'two_factor'
|
|
||||||
redirect_uris:
|
redirect_uris:
|
||||||
- 'https://hass.example.com/auth/oidc/callback'
|
- 'https://hass.example.com/auth/oidc/callback'
|
||||||
scopes:
|
|
||||||
- 'openid'
|
|
||||||
- 'profile'
|
|
||||||
- 'groups'
|
|
||||||
id_token_signed_response_alg: 'RS256'
|
|
||||||
token_endpoint_auth_method: 'client_secret_post'
|
token_endpoint_auth_method: 'client_secret_post'
|
||||||
```
|
```
|
||||||
|
|
||||||
Home Assistant `configuration.yaml`
|
## Step 3. Home Assistant configuration
|
||||||
```yaml
|
|
||||||
auth_oidc:
|
The recommended setup method for beginners is through the "Integrations" panel within the Home Assistant UI. You can also use YAML setup, for which you can find the configuration guide here: [YAML Configuration Guide](../configuration.md).
|
||||||
client_id: "homeassistant"
|
|
||||||
client_secret: "insecure_secret"
|
1. Open Home Assistant and go to **Settings -> Devices & Services**.
|
||||||
discovery_url: "https://auth.example.com/.well-known/openid-configuration"
|
2. Click Add Integration and select **OpenID Connect/SSO Authentication**.
|
||||||
```
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Now click "Authelia" and continue to the next screen
|
||||||
|
4. Set the discovery URL to `https://<your Authelia URL>/.well-known/openid-configuration` and click **Submit**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Your URL will be tested. You may see an error, such as the picture below. Check your URL and verify that Home Assistant can access your Authelia installation. Change the URL or retry.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. If your discovery URL is tested succesfully, you will see something like this and you can continue with the **Submit** button to continue.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. You will then be prompted to fill in the client details, the **Client ID** and the **Client Secret** (if you used the Public Client type in the Authelia configuration, there is no Client Secret required). Paste them in the relevant input boxes and continue setup with **Submit**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
8. You will then be asked about **Groups & Role Configuration** and **User Linking**. Configure these options as you wish or leave the defaults in place. You can also change these settings later by opening the integration settings and clicking the reconfiguration icon.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Done!
|
||||||
|
|
||||||
|
You should now automatically see the welcome screen upon opening your Home Assistant URL. On the welcome screen you can choose to either start login through SSO or to use an alternative login method, which will bring you back to the normal Home Assistant username/password login screen.
|
||||||
@@ -1,40 +1,63 @@
|
|||||||
# Authentik
|
# authentik
|
||||||
|
|
||||||
## Public client configuration
|
> [!TIP]
|
||||||
Under construction.
|
> This guide describes configuring authentik using the UI method. You can also configure authentik by hand with YAML. Instructions for configuring any provider using YAML can be found here: [YAML Configuration Guide](../configuration.md).
|
||||||
|
|
||||||
## Confidential client configuration
|
|
||||||
|
|
||||||
1. From the admin interface, go to `Applications > Providers` and click on `Create`
|
## Step 1. Install the integration
|
||||||
2. Select `OAuth2/OpenID Provider` and click `Next`
|
|
||||||
3. Fill the following details:
|
|
||||||
- Name: `Home Assistant Provider`
|
|
||||||
- Authorization flow: `default-provider-authorization-explicit-consent`
|
|
||||||
- Client type: `Confidential`
|
|
||||||
- Client ID: `homeassistant`
|
|
||||||
- Client Secret: **Copy this value**
|
|
||||||
- Redirect URIs/Origins: Click on `Add entry` (You can use either DNS, Internal/External IP or localhost)
|
|
||||||
- Strict: https://hass.example.com/auth/oidc/callback
|
|
||||||
4. Click `Finish` to save the provider configuration
|
|
||||||
5. Open the created Provider
|
|
||||||
6. On the Assigned to application section click on `Create`:
|
|
||||||
- Name: `Home Assistant`
|
|
||||||
- Slug: `home-assistant`
|
|
||||||
- Provider: `Home Assistant Provider`
|
|
||||||
|
|
||||||
Then save the configuration
|
Make sure that you have fully installed the latest release of the integration. The easiest way to install the integration is through [the Home Assistant Community Store (HACS)](https://hacs.xyz/). You can find usage instructions for HACS here: https://hacs.xyz/docs/use/.
|
||||||
|
|
||||||
## Home Assistant configuration
|
After installing HACS, search for "OpenID Connect" in the HACS search box or click the button below:
|
||||||
|
|
||||||
> [!IMPORTANT]
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
||||||
> For HTTPS configuration make sure to have a public valid SSL certificate (i.e. LetsEncrypt), if not, use HTTP instead (more insecure) or add your Authentik CA certificate to `network.tls_ca_path`.
|
|
||||||
|
|
||||||
After installing this HACS addon, edit your `configuration.yaml` file and add:
|
## Step 2. Configure authentik
|
||||||
```yaml
|
|
||||||
auth_oidc:
|
|
||||||
client_id: "homeassistant"
|
|
||||||
client_secret: "client_secret"
|
|
||||||
discovery_url: "https://auth.example.com/application/o/home-assistant/.well-known/openid-configuration"
|
|
||||||
```
|
|
||||||
|
|
||||||
Restart Home Assistant and go to https://hass.example.com/auth/oidc/welcome
|
1. Log in to authentik as an administrator and open the authentik Admin interface.
|
||||||
|
|
||||||
|
2. Navigate to **Applications > Applications** and click **Create with Provider** to create an application and provider pair. (Alternatively you can first create a provider separately, then create the application and connect it with the provider.)
|
||||||
|
|
||||||
|
- **Application**: provide a descriptive name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||||
|
- Choose a **Provider Type**: select **OAuth2/OpenID Connect** as the provider type.
|
||||||
|
- **Configure the Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and the following required configurations.
|
||||||
|
- Note the **Client ID**, **Client Secret**, and **slug** values because they will be required later.
|
||||||
|
- Set a `Strict` redirect URI to `https://<your HA URL>/auth/oidc/callback`.
|
||||||
|
- Select any available signing key (to use the RS256 `id_token_signing_alg`)
|
||||||
|
- Configure Bindings (optional): you can create a binding (policy, group, or user) to manage the listing and access to applications on a user's **My applications** page.
|
||||||
|
|
||||||
|
## Step 3. Home Assistant configuration
|
||||||
|
|
||||||
|
The recommended setup method for beginners is through the "Integrations" panel within the Home Assistant UI. You can also use YAML setup, for which you can find the configuration guide here: [YAML Configuration Guide](../configuration.md).
|
||||||
|
|
||||||
|
1. Open Home Assistant and go to **Settings -> Devices & Services**.
|
||||||
|
2. Click Add Integration and select **OpenID Connect/SSO Authentication**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Now click "Authentik" and continue to the next screen
|
||||||
|
4. Set the discovery URL to `https://<your Authentik URL>/application/o/<application_slug>/.well-known/openid-configuration` using the **slug** from the earlier authentik configuration step and click **Submit**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Your URL will be tested. You may see an error, such as the picture below. Check your URL and verify that Home Assistant can access your authentik installation. Change the URL or retry.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. If your discovery URL is tested succesfully, you will see something like this and you can continue with the **Submit** button to continue.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. You will then be prompted to fill in the client details, the **Client ID** and the **Client Secret** (if you used the Public Client type in authentik, there is no Client Secret required). Paste them in the relevant input boxes and continue setup with **Submit**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
8. You will then be asked about **Groups & Role Configuration** and **User Linking**. Configure these options as you wish or leave the defaults in place. You can also change these settings later by opening the integration settings and clicking the reconfiguration icon.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Done!
|
||||||
|
|
||||||
|
You should now automatically see the welcome screen upon opening your Home Assistant URL. On the welcome screen you can choose to either start login through SSO or to use an alternative login method, which will bring you back to the normal Home Assistant username/password login screen.
|
||||||
77
docs/provider-configurations/keycloak.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Keycloak
|
||||||
|
|
||||||
|
|
||||||
|
## Step 1. Install the integration
|
||||||
|
|
||||||
|
Make sure that you have fully installed the latest release of the integration. The easiest way to install the integration is through [the Home Assistant Community Store (HACS)](https://hacs.xyz/).
|
||||||
|
|
||||||
|
After installing HACS, search for "OpenID Connect" in the HACS search box or click the button below:
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
||||||
|
|
||||||
|
## Step 2. Configure Keycloak
|
||||||
|
|
||||||
|
1. Log in to your Keycloak Admin Console and select the Realm you want to use.
|
||||||
|
2. Navigate to **Clients** and click **Create client**.
|
||||||
|
* **Client ID**: `homeassistant` (or a name of your choice).
|
||||||
|
* **Client Authentication**: Turn **ON** if you want to use a Client Secret (Confidential Client), or leave **OFF** for a Public Client.
|
||||||
|
* **Valid redirect URIs**: `https://<your HA URL>/auth/oidc/callback`
|
||||||
|
* Save the client. If you enabled Client Authentication, go to the **Credentials** tab and copy your **Client Secret**.
|
||||||
|
|
||||||
|
*(If you are using the UI configuration in Home Assistant, you can stop here and proceed to Step 3. Group and role mapping is only supported via `configuration.yaml`.)*
|
||||||
|
|
||||||
|
3. Navigate to **Groups** and create the groups you want to use for Home Assistant access.
|
||||||
|
* Example: `homeassistant` (for standard users) and `homeassistantadmin` (for administrators).
|
||||||
|
* Assign your users to these groups.
|
||||||
|
|
||||||
|
### Step 2.1 Configure the Group Mapper (YAML only)
|
||||||
|
|
||||||
|
By default, Keycloak does not send a user's groups in the OIDC token in a format that Home Assistant expects. You must create a specific mapper:
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you name the scope something other than `groups`, you have to set `claims.groups` to the correct name and `groups_scope` to the new name in your Home Assistant configuration.
|
||||||
|
|
||||||
|
1. In Keycloak, go to **Client Scopes**. Create a dedicated scope `groups` and assign it to your `homeassistant` client as a Default Scope.
|
||||||
|
2. Click into the scope and go to the **Mappers** tab.
|
||||||
|
3. Click **Configure a new mapper** (or Add mapper -> By configuration) and select **Group Membership**.
|
||||||
|
4. Configure the mapper exactly as follows:
|
||||||
|
* **Name**: `groups`
|
||||||
|
* **Token Claim Name**: `groups`
|
||||||
|
* **Full group path**: **OFF** *(Important: This ensures Home Assistant receives `homeassistant` instead of the full path `/users/homeassistant`, if you use nested groups)*.
|
||||||
|
* **Add to ID token**: **ON**
|
||||||
|
* **Add to access token**: **ON**
|
||||||
|
* **Add to userinfo**: **ON**
|
||||||
|
5. Save the mapper.
|
||||||
|
|
||||||
|
## Step 3. Home Assistant Configuration
|
||||||
|
|
||||||
|
You can configure this via the UI, or by using `configuration.yaml`.
|
||||||
|
|
||||||
|
### Option A: Configuration via UI (Simple)
|
||||||
|
|
||||||
|
The UI flow is the easiest way to get started. Note that the UI does not currently offer group/role customization for OpenID Connect (SSO), so the group mapper setup from Keycloak is not needed.
|
||||||
|
|
||||||
|
1. Go to **Settings** -> **Devices & Services** in Home Assistant.
|
||||||
|
2. Click **Add Integration** and search for **OpenID Connect**.
|
||||||
|
3. As OIDC Provider select **OpenID Connect (SSO)**.
|
||||||
|
4. Follow the UI flow and enter the following details:
|
||||||
|
* **Discovery URL**: `https://<your-keycloak-domain>/realms/<your-realm>/.well-known/openid-configuration`
|
||||||
|
* **Client ID**: The Client ID you created in Keycloak (e.g., `homeassistant`).
|
||||||
|
* **Client Secret**: The Client Secret from Keycloak (if Client Authentication was enabled).
|
||||||
|
5. Finish the setup in the UI.
|
||||||
|
|
||||||
|
### Option B: Configuration via `configuration.yaml` (Advanced / Group Mapping)
|
||||||
|
|
||||||
|
Here is the minimal `configuration.yaml` setup for Keycloak if you want to use group-based role mapping:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_oidc:
|
||||||
|
client_id: "homeassistant"
|
||||||
|
client_secret: !secret oidc_client_secret # Remove this line if Client Authentication is OFF in Keycloak
|
||||||
|
discovery_url: "https://<your-keycloak-domain>/realms/<your-realm>/.well-known/openid-configuration"
|
||||||
|
|
||||||
|
roles:
|
||||||
|
# These must exactly match the group names you created in Keycloak
|
||||||
|
user: homeassistant
|
||||||
|
admin: homeassistantadmin
|
||||||
|
```
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
# Pocket ID
|
# Pocket ID
|
||||||
|
|
||||||
## Public client configuration
|
> [!TIP]
|
||||||
|
> This guide describes configuring Pocket ID using the UI method. You can also configure Pocket ID by hand with YAML. Instructions for configuring any provider using YAML can be found here: [YAML Configuration Guide](../configuration.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Step 1. Install the integration
|
||||||
|
|
||||||
|
Make sure that you have fully installed the latest release of the integration. The easiest way to install the integration is through [the Home Assistant Community Store (HACS)](https://hacs.xyz/). You can find usage instructions for HACS here: https://hacs.xyz/docs/use/.
|
||||||
|
|
||||||
|
After installing HACS, search for "OpenID Connect" in the HACS search box or click the button below:
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
||||||
|
|
||||||
|
## Step 2. Configure Pocket ID
|
||||||
|
|
||||||
|
You can choose between configuring Pocket ID as a public or confidential client.
|
||||||
|
|
||||||
|
### Public client configuration
|
||||||
|
|
||||||
### Pocket ID configuration
|
|
||||||
1. Login to Pocket ID and go to `OIDC Clients`
|
1. Login to Pocket ID and go to `OIDC Clients`
|
||||||
|
|
||||||
2. Click on `Add OIDC Client`
|
2. Click on `Add OIDC Client`
|
||||||
@@ -16,19 +31,8 @@
|
|||||||
|
|
||||||
5. Click on `Show more details` and note down your `Client ID` and `OIDC Discovery URL` since you will need them later.
|
5. Click on `Show more details` and note down your `Client ID` and `OIDC Discovery URL` since you will need them later.
|
||||||
|
|
||||||
### Home Assistant configuration
|
### Confidential client configuration:
|
||||||
1. Add following configuration in Home Assistant's configuration.yaml:
|
|
||||||
```yaml
|
|
||||||
auth_oidc:
|
|
||||||
client_id: <The Client ID you have noted down>
|
|
||||||
discovery_url: <The OIDC Discovery URL you have noted down> (for example: https://id.example.com/.well-known/openid-configuration)
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Restart Home Assistant and go to your Home Assistant OIDC URL (for example: https://hass.example.com/auth/oidc/welcome)
|
|
||||||
|
|
||||||
## Confidential client configuration
|
|
||||||
|
|
||||||
### Pocket ID configuration
|
|
||||||
1. Login to Pocket ID and go to `OIDC Clients`
|
1. Login to Pocket ID and go to `OIDC Clients`
|
||||||
|
|
||||||
2. Click on `Add OIDC Client`
|
2. Click on `Add OIDC Client`
|
||||||
@@ -44,15 +48,38 @@ auth_oidc:
|
|||||||
- `Client secret`
|
- `Client secret`
|
||||||
- `OIDC Discovery URL`
|
- `OIDC Discovery URL`
|
||||||
|
|
||||||
### Home Assistant configuration
|
## Step 3. Home Assistant configuration
|
||||||
1. Add following configuration in Home Assistant's configuration.yaml:
|
|
||||||
```yaml
|
|
||||||
auth_oidc:
|
|
||||||
client_id: <The Client ID you have noted down>
|
|
||||||
client_secret: <The Client secret you have noted down>
|
|
||||||
discovery_url: <The OIDC Discovery URL you have noted down> (for example: https://id.example.com/.well-known/openid-configuration)
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Restart Home Assistant and go to your Home Assistant OIDC URL (for example: https://hass.example.com/auth/oidc/welcome)
|
The recommended setup method for beginners is through the "Integrations" panel within the Home Assistant UI. You can also use YAML setup, for which you can find the configuration guide here: [YAML Configuration Guide](../configuration.md).
|
||||||
|
|
||||||
|
1. Open Home Assistant and go to **Settings -> Devices & Services**.
|
||||||
|
2. Click Add Integration and select **OpenID Connect/SSO Authentication**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Now click "Pocket ID" and continue to the next screen
|
||||||
|
4. Set the discovery URL to `https://<your Pocket ID URL>/.well-known/openid-configuration` and click **Submit**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Your URL will be tested. You may see an error, such as the picture below. Check your URL and verify that Home Assistant can access your Pocket ID installation. Change the URL or retry.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
6. If your discovery URL is tested succesfully, you will see something like this and you can continue with the **Submit** button to continue.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
7. You will then be prompted to fill in the client details, the **Client ID** and the **Client Secret** (if you used the Public Client type in the Pocket ID configuration, there is no Client Secret required). Paste them in the relevant input boxes and continue setup with **Submit**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
8. You will then be asked about **Groups & Role Configuration** and **User Linking**. Configure these options as you wish or leave the defaults in place. You can also change these settings later by opening the integration settings and clicking the reconfiguration icon.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Done!
|
||||||
|
|
||||||
|
You should now automatically see the welcome screen upon opening your Home Assistant URL. On the welcome screen you can choose to either start login through SSO or to use an alternative login method, which will bring you back to the normal Home Assistant username/password login screen.
|
||||||
BIN
docs/ui-config-steps/client-details.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/ui-config-steps/discovery-url-failure.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
docs/ui-config-steps/discovery-url-success.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/ui-config-steps/discovery-url.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/ui-config-steps/ui-configuration.gif
Normal file
|
After Width: | Height: | Size: 1018 KiB |
BIN
docs/ui-config-steps/ui-reconfigure.gif
Normal file
|
After Width: | Height: | Size: 221 KiB |
@@ -1,84 +1,3 @@
|
|||||||
# How do I use the OIDC Integration for Home Assistant?
|
# Usage Guide
|
||||||
|
|
||||||
Here's a step by step guide to use the integration:
|
The usage instructions have moved to [the main README](../README.md)
|
||||||
|
|
||||||
### Step 1: HACS
|
|
||||||
Install the integration through [HACS](https://hacs.xyz/). You can add it automatically using the button below, or use the Github URL and type `Integration` in the manual Custom Repository add dialog.
|
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/hacs_repository/?owner=christiaangoossens&repository=hass-oidc-auth&category=Integration)
|
|
||||||
|
|
||||||
|
|
||||||
### Step 2: Configuration of the integration
|
|
||||||
The integration is currently configurable through YAML only. See the [Configuration Guide](./configuration.md) for more details or pick your OIDC provider below (additional providers are available in the Configuration Guide):
|
|
||||||
|
|
||||||
| <img src="https://goauthentik.io/img/icon_top_brand_colour.svg" width="100"> | <img src="https://www.authelia.com/images/branding/logo-cropped.png" width="100"> | <img src="https://github.com/user-attachments/assets/4ceb2708-9f29-4694-b797-be833efce17d" width="100"> |
|
|
||||||
|:-----------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------:|
|
|
||||||
| [Authentik](./provider-configurations/authentik.md) | [Authelia](./provider-configurations/authelia.md) | [Pocket ID](./provider-configurations/pocket-id.md) |
|
|
||||||
|
|
||||||
By default, the integration assumes you configure Home Assistant as a **public client** and thus only specify the `client_id` and no `client_secret`. For example, your configuration might look like:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
auth_oidc:
|
|
||||||
client_id: "example"
|
|
||||||
discovery_url: "https://example.com/.well-known/openid-configuration"
|
|
||||||
```
|
|
||||||
|
|
||||||
When registering Home Assistant at your OIDC provider, use `<your HA URL>/auth/oidc/callback` as the callback URL and select 'public client'. You should now get the `client_id` and `issuer_url` or `discovery_url` to fill in.
|
|
||||||
|
|
||||||
### Step 3: Restart
|
|
||||||
Restart Home Assistant. You can do so by going to the Reparations/Update section in Home Assistant.
|
|
||||||
|
|
||||||
### Step 4: Go to the OIDC login screen
|
|
||||||
After restarting Home Assistant, you should now be able to get to the login screen. You can find it at `<your HA URL>/auth/oidc/welcome`. You will have to go there manually for now. For example, it might be located at http://homeassistant.local:8123/auth/oidc/welcome.
|
|
||||||
|
|
||||||
It should look like this:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If you have configured everything correctly, you should be redirected to your OIDC Provider after clicking the button. Please login there.
|
|
||||||
|
|
||||||
You should return to a screen like this:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Either click the automatic sign in button or copy the code.
|
|
||||||
This screen will give you a one-time code to login that expires in 5 minutes.
|
|
||||||
|
|
||||||
#### Step 4a: Automatic login
|
|
||||||
If you would like to login automatically, click the button. It will log you in to your user in the current browser window.
|
|
||||||
|
|
||||||
#### Step 4b: Code login
|
|
||||||
If you would like to login using the code, go to your normal Home Assistant URL without any user logged in, such as on your mobile device/wall tablet/smart watch. You will now see the following screen:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If you don't, you likely see:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If so, click "OpenID Connect (SSO)" to get to the first screen. If you have configured a [display name](./configuration.md#configuring-a-display-name-for-your-oidc-provider), that will show instead.
|
|
||||||
|
|
||||||
Enter your code into the single input field:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Upon clicking login, you should now login.
|
|
||||||
If the code is wrong, you will see this instead:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Step 5: Logged in
|
|
||||||
You will be logged in after following this guide.
|
|
||||||
|
|
||||||
With the default configuration, [a person entry](https://www.home-assistant.io/integrations/person/) will be created for every new OIDC user logging in. New OIDC users will get their own fresh user, linked to their persistent ID (subject) at the OpenID Connect provider. You may change your name, username or email at the provider and still have the same Home Assistant user profile.
|
|
||||||
|
|
||||||
# How can I make this easier for my users?
|
|
||||||
|
|
||||||
You can link the user directly to one of these following URLs:
|
|
||||||
|
|
||||||
- `/auth/oidc/welcome` (if you would like a nice welcome screen for your users)
|
|
||||||
- `/auth/oidc/redirect` (if you would like to just redirect them without a welcome screen)
|
|
||||||
|
|
||||||
For a seamless user experience, configure a new domain on your proxy to redirect to the `/auth/oidc/welcome` path or configure that path on your homelab dashboard or in your OIDC provider (such as in the app settings in Authentik). Users will then always start on the OIDC welcome page, which will allow them to visit the dashboard if they are already logged in.
|
|
||||||
|
|
||||||
*Note: do not replace the standard path with a redirect to the OIDC screen. This breaks login with code.*
|
|
||||||
142
package-lock.json
generated
@@ -369,27 +369,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/cli": {
|
"node_modules/@tailwindcss/cli": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.4.tgz",
|
||||||
"integrity": "sha512-iJS+8kAFZ8HPqnh0O5DHCLjo4L6dD97DBQEkrhfSO4V96xeefUus2jqsBs1dUMt3OU9Ks4qIkiY0mpL5UW+4LQ==",
|
"integrity": "sha512-e87GGhuXxnyQPyA0TS8an/3wNpj+OUmx8u0F4BicYr48TF72032AIu5917rRYaWm7HorXi3GSZ/uG+ohqP6AKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@parcel/watcher": "^2.5.1",
|
"@parcel/watcher": "^2.5.1",
|
||||||
"@tailwindcss/node": "4.2.2",
|
"@tailwindcss/node": "4.2.4",
|
||||||
"@tailwindcss/oxide": "4.2.2",
|
"@tailwindcss/oxide": "4.2.4",
|
||||||
"enhanced-resolve": "^5.19.0",
|
"enhanced-resolve": "^5.19.0",
|
||||||
"mri": "^1.2.0",
|
"mri": "^1.2.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"tailwindcss": "4.2.2"
|
"tailwindcss": "4.2.4"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tailwindcss": "dist/index.mjs"
|
"tailwindcss": "dist/index.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz",
|
||||||
"integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
|
"integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
@@ -398,36 +398,36 @@
|
|||||||
"lightningcss": "1.32.0",
|
"lightningcss": "1.32.0",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"source-map-js": "^1.2.1",
|
"source-map-js": "^1.2.1",
|
||||||
"tailwindcss": "4.2.2"
|
"tailwindcss": "4.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz",
|
||||||
"integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
|
"integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tailwindcss/oxide-android-arm64": "4.2.2",
|
"@tailwindcss/oxide-android-arm64": "4.2.4",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.2.2",
|
"@tailwindcss/oxide-darwin-arm64": "4.2.4",
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.2.2",
|
"@tailwindcss/oxide-darwin-x64": "4.2.4",
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.2.2",
|
"@tailwindcss/oxide-freebsd-x64": "4.2.4",
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4",
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.4",
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
|
"@tailwindcss/oxide-linux-arm64-musl": "4.2.4",
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
|
"@tailwindcss/oxide-linux-x64-gnu": "4.2.4",
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.2.2",
|
"@tailwindcss/oxide-linux-x64-musl": "4.2.4",
|
||||||
"@tailwindcss/oxide-wasm32-wasi": "4.2.2",
|
"@tailwindcss/oxide-wasm32-wasi": "4.2.4",
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.4",
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
|
"@tailwindcss/oxide-win32-x64-msvc": "4.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz",
|
||||||
"integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
|
"integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -441,9 +441,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz",
|
||||||
"integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
|
"integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -457,9 +457,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz",
|
||||||
"integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
|
"integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -473,9 +473,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz",
|
||||||
"integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
|
"integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -489,9 +489,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz",
|
||||||
"integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
|
"integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -505,9 +505,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz",
|
||||||
"integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
|
"integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -524,9 +524,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz",
|
||||||
"integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
|
"integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -543,9 +543,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz",
|
||||||
"integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
|
"integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -562,9 +562,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz",
|
||||||
"integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
|
"integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -581,9 +581,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz",
|
||||||
"integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
|
"integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==",
|
||||||
"bundleDependencies": [
|
"bundleDependencies": [
|
||||||
"@napi-rs/wasm-runtime",
|
"@napi-rs/wasm-runtime",
|
||||||
"@emnapi/core",
|
"@emnapi/core",
|
||||||
@@ -668,9 +668,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz",
|
||||||
"integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
|
"integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -684,9 +684,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz",
|
||||||
"integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
|
"integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -709,13 +709,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.20.1",
|
"version": "5.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
|
||||||
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
|
"integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.4",
|
"graceful-fs": "^4.2.4",
|
||||||
"tapable": "^2.3.0"
|
"tapable": "^2.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
@@ -1070,15 +1070,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz",
|
||||||
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
"integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
|
||||||
"integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
|
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
|
from collections import OrderedDict
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, unquote, urlparse
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.auth import InvalidAuthError
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@@ -17,6 +19,8 @@ from custom_components.auth_oidc import DOMAIN
|
|||||||
from custom_components.auth_oidc.config.const import (
|
from custom_components.auth_oidc.config.const import (
|
||||||
DISCOVERY_URL,
|
DISCOVERY_URL,
|
||||||
CLIENT_ID,
|
CLIENT_ID,
|
||||||
|
DEFAULT_TITLE,
|
||||||
|
DISPLAY_NAME,
|
||||||
FEATURES,
|
FEATURES,
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION,
|
FEATURES_AUTOMATIC_PERSON_CREATION,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING,
|
FEATURES_AUTOMATIC_USER_LINKING,
|
||||||
@@ -24,6 +28,10 @@ from custom_components.auth_oidc.config.const import (
|
|||||||
from .mocks.oidc_server import MockOIDCServer, mock_oidc_responses
|
from .mocks.oidc_server import MockOIDCServer, mock_oidc_responses
|
||||||
|
|
||||||
FAKE_REDIR_URL = "http://example.com/auth/authorize?response_type=code&redirect_uri=http%3A%2F%2Fexample.com%3A8123%2F%3Fauth_callback%3D1&client_id=http%3A%2F%2Fexample.com%3A8123%2F&state=example"
|
FAKE_REDIR_URL = "http://example.com/auth/authorize?response_type=code&redirect_uri=http%3A%2F%2Fexample.com%3A8123%2F%3Fauth_callback%3D1&client_id=http%3A%2F%2Fexample.com%3A8123%2F&state=example"
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def setup(hass: HomeAssistant, config: dict, expect_success: bool) -> bool:
|
async def setup(hass: HomeAssistant, config: dict, expect_success: bool) -> bool:
|
||||||
@@ -40,10 +48,7 @@ async def test_setup_success_auth_provider_registration(hass: HomeAssistant):
|
|||||||
"""Test successful setup"""
|
"""Test successful setup"""
|
||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
DEFAULT_CONFIG,
|
||||||
CLIENT_ID: "dummy",
|
|
||||||
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
|
||||||
},
|
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,6 +60,24 @@ async def test_setup_success_auth_provider_registration(hass: HomeAssistant):
|
|||||||
assert auth_providers[0].support_mfa is False
|
assert auth_providers[0].support_mfa is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_provider_name_is_stable_regardless_of_display_name(hass: HomeAssistant):
|
||||||
|
"""CONF_NAME stays at DEFAULT_TITLE so injection.js can match the picker
|
||||||
|
row regardless of the configured display_name."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
||||||
|
DISPLAY_NAME: "Custom / Branded IdP",
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
assert provider.name == DEFAULT_TITLE
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_provider_ip_fallback_fails_closed_without_request_context(
|
async def test_provider_ip_fallback_fails_closed_without_request_context(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -62,10 +85,7 @@ async def test_provider_ip_fallback_fails_closed_without_request_context(
|
|||||||
"""Provider should not invent a shared IP when request context is missing."""
|
"""Provider should not invent a shared IP when request context is missing."""
|
||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
DEFAULT_CONFIG,
|
||||||
CLIENT_ID: "dummy",
|
|
||||||
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
|
||||||
},
|
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,10 +103,7 @@ async def test_provider_cookie_header_sets_secure_when_requested(hass: HomeAssis
|
|||||||
"""Cookie header should include Secure when HTTPS is in use."""
|
"""Cookie header should include Secure when HTTPS is in use."""
|
||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
DEFAULT_CONFIG,
|
||||||
CLIENT_ID: "dummy",
|
|
||||||
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
|
||||||
},
|
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -98,6 +115,142 @@ async def test_provider_cookie_header_sets_secure_when_requested(hass: HomeAssis
|
|||||||
assert "Secure" in cookie_header
|
assert "Secure" in cookie_header
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_provider_is_trusted_network_host_true_for_allowed_ip(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
):
|
||||||
|
"""Provider should detect trusted network host when trusted provider allows the IP."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
|
||||||
|
class TrustedNetworksAllowProvider:
|
||||||
|
def async_validate_access(self, _ip_addr):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
hass.auth._providers = OrderedDict(
|
||||||
|
[
|
||||||
|
(("trusted_networks", None), TrustedNetworksAllowProvider()),
|
||||||
|
((provider.type, provider.id), provider),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.auth_oidc.provider.http.current_request"
|
||||||
|
) as current_request:
|
||||||
|
current_request.get.return_value = SimpleNamespace(remote="127.0.0.1")
|
||||||
|
assert provider.is_trusted_network_host() is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_provider_is_trusted_network_host_false_for_disallowed_ip(
|
||||||
|
hass: HomeAssistant, caplog
|
||||||
|
):
|
||||||
|
"""Provider should return False when trusted provider denies the current IP."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
|
||||||
|
class TrustedNetworksDenyProvider:
|
||||||
|
def async_validate_access(self, _ip_addr):
|
||||||
|
raise InvalidAuthError("Not in trusted_networks")
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
hass.auth._providers = OrderedDict(
|
||||||
|
[
|
||||||
|
(("trusted_networks", None), TrustedNetworksDenyProvider()),
|
||||||
|
((provider.type, provider.id), provider),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.auth_oidc.provider.http.current_request"
|
||||||
|
) as current_request:
|
||||||
|
current_request.get.return_value = SimpleNamespace(remote="127.0.0.1")
|
||||||
|
assert provider.is_trusted_network_host() is False
|
||||||
|
assert any(
|
||||||
|
level >= 0
|
||||||
|
and "is not in a trusted network, proceeding with OIDC flow" in message
|
||||||
|
for _, level, message in caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert not any(
|
||||||
|
level >= 0 and "Error while validating trusted network for IP" in message
|
||||||
|
for _, level, message in caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_provider_is_trusted_network_host_false_without_trusted_provider(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
):
|
||||||
|
"""Provider should return False when trusted_networks auth provider is absent."""
|
||||||
|
await setup(
|
||||||
|
hass,
|
||||||
|
DEFAULT_CONFIG,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
|
||||||
|
# Without actually getting the IP, should also be false
|
||||||
|
assert provider.is_trusted_network_host() is False
|
||||||
|
|
||||||
|
# With the IP, should be false
|
||||||
|
with patch(
|
||||||
|
"custom_components.auth_oidc.provider.http.current_request"
|
||||||
|
) as current_request:
|
||||||
|
current_request.get.return_value = SimpleNamespace(remote="127.0.0.1")
|
||||||
|
assert provider.is_trusted_network_host() is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_welcome_redirects_when_only_trusted_networks_and_not_in_trusted_network(
|
||||||
|
hass: HomeAssistant, hass_client
|
||||||
|
):
|
||||||
|
"""When only trusted_networks is present, welcome should redirect regardless of IP."""
|
||||||
|
|
||||||
|
class TrustedNetworksDenyProvider:
|
||||||
|
def async_validate_access(self, _ip_addr):
|
||||||
|
raise InvalidAuthError("Not in trusted_networks")
|
||||||
|
|
||||||
|
# Simulate that only trusted_networks is registered before OIDC provider setup
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
hass.auth._providers = OrderedDict(
|
||||||
|
[
|
||||||
|
(("trusted_networks", None), TrustedNetworksDenyProvider()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
# Now setup the OIDC provider which should detect trusted_networks as the only other provider
|
||||||
|
await setup(hass, DEFAULT_CONFIG, True)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
encoded_redirect_uri = base64.b64encode(FAKE_REDIR_URL.encode("utf-8")).decode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = await client.get(
|
||||||
|
f"/auth/oidc/welcome?redirect_uri={encoded_redirect_uri}",
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should redirect straight to the OIDC redirect endpoint
|
||||||
|
assert resp.status == 302
|
||||||
|
assert resp.headers["Location"].endswith("/auth/oidc/redirect")
|
||||||
|
|
||||||
|
|
||||||
async def login_user(hass: HomeAssistant, state_id: str):
|
async def login_user(hass: HomeAssistant, state_id: str):
|
||||||
"""Helper to login a user from the stored OIDC state."""
|
"""Helper to login a user from the stored OIDC state."""
|
||||||
|
|
||||||
@@ -167,8 +320,7 @@ async def test_full_login(hass: HomeAssistant, hass_client):
|
|||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
CLIENT_ID: "dummy",
|
**DEFAULT_CONFIG,
|
||||||
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING: False,
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
@@ -198,8 +350,7 @@ async def test_login_with_linking(hass: HomeAssistant, hass_client):
|
|||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
CLIENT_ID: "dummy",
|
**DEFAULT_CONFIG,
|
||||||
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING: True,
|
FEATURES_AUTOMATIC_USER_LINKING: True,
|
||||||
@@ -233,8 +384,7 @@ async def test_login_with_person_create(hass: HomeAssistant, hass_client):
|
|||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
CLIENT_ID: "dummy",
|
**DEFAULT_CONFIG,
|
||||||
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION: True,
|
FEATURES_AUTOMATIC_PERSON_CREATION: True,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING: False,
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
@@ -267,8 +417,7 @@ async def test_login_without_person_create_does_not_create_person(
|
|||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
CLIENT_ID: "dummy",
|
**DEFAULT_CONFIG,
|
||||||
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING: False,
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
@@ -295,8 +444,7 @@ async def test_login_shows_form(hass: HomeAssistant):
|
|||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
CLIENT_ID: "dummy",
|
**DEFAULT_CONFIG,
|
||||||
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING: False,
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
@@ -319,8 +467,7 @@ async def test_login_with_invalid_cookie_aborts(hass: HomeAssistant):
|
|||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
CLIENT_ID: "dummy",
|
**DEFAULT_CONFIG,
|
||||||
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING: False,
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
@@ -352,8 +499,7 @@ async def test_login_with_no_cookie_aborts(hass: HomeAssistant):
|
|||||||
await setup(
|
await setup(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
CLIENT_ID: "dummy",
|
**DEFAULT_CONFIG,
|
||||||
DISCOVERY_URL: MockOIDCServer.get_discovery_url(),
|
|
||||||
FEATURES: {
|
FEATURES: {
|
||||||
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
FEATURES_AUTOMATIC_PERSON_CREATION: False,
|
||||||
FEATURES_AUTOMATIC_USER_LINKING: False,
|
FEATURES_AUTOMATIC_USER_LINKING: False,
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
import base64
|
import base64
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
|
from types import SimpleNamespace
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
from urllib.parse import parse_qs, unquote, urlparse, urlencode
|
from urllib.parse import parse_qs, unquote, urlparse, urlencode
|
||||||
import pytest
|
import pytest
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from custom_components.auth_oidc import DOMAIN
|
from custom_components.auth_oidc import DOMAIN
|
||||||
|
from custom_components.auth_oidc.provider import COOKIE_NAME
|
||||||
from custom_components.auth_oidc.tools.oidc_client import (
|
from custom_components.auth_oidc.tools.oidc_client import (
|
||||||
OIDCDiscoveryClient,
|
OIDCDiscoveryClient,
|
||||||
OIDCDiscoveryInvalid,
|
OIDCDiscoveryInvalid,
|
||||||
@@ -248,6 +251,47 @@ async def test_full_oidc_flow(hass: HomeAssistant, hass_client):
|
|||||||
await verify_back_redirect(client, redirect_uri)
|
await verify_back_redirect(client, redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_login_flow_init_completes_with_state_cookie(
|
||||||
|
hass: HomeAssistant, hass_client
|
||||||
|
):
|
||||||
|
"""The provider login flow init step should finalize when the auth cookie is present."""
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
with mock_oidc_responses():
|
||||||
|
client = await hass_client()
|
||||||
|
redirect_uri = create_redirect_uri(WEB_CLIENT_ID)
|
||||||
|
|
||||||
|
state, _, status = await get_welcome_for_client(client, redirect_uri)
|
||||||
|
assert status == 200
|
||||||
|
|
||||||
|
authorization_url = await get_redirect_auth_url(client)
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
resp_auth = session.get(authorization_url, allow_redirects=False)
|
||||||
|
json_auth = await resp_auth.json()
|
||||||
|
|
||||||
|
resp = await client.get(
|
||||||
|
f"/auth/oidc/callback?code={json_auth['code']}&state={state}",
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
assert resp.status == 302
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
flow = await provider.async_login_flow({})
|
||||||
|
|
||||||
|
fake_request = SimpleNamespace(
|
||||||
|
cookies={COOKIE_NAME: state},
|
||||||
|
remote="127.0.0.1",
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"custom_components.auth_oidc.provider.http.current_request"
|
||||||
|
) as current_request:
|
||||||
|
current_request.get.return_value = fake_request
|
||||||
|
result = await flow.async_step_init({})
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
async def discovery_test_through_redirect(
|
async def discovery_test_through_redirect(
|
||||||
hass_client, caplog, scenario: str, match_log_line: str
|
hass_client, caplog, scenario: str, match_log_line: str
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -2,9 +2,17 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
|
from collections import OrderedDict
|
||||||
from urllib.parse import parse_qs, quote, unquote, urlparse, urlencode
|
from urllib.parse import parse_qs, quote, unquote, urlparse, urlencode
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from auth_oidc.config.const import DISCOVERY_URL, CLIENT_ID
|
from aiohttp import web
|
||||||
|
|
||||||
|
from auth_oidc.config.const import (
|
||||||
|
DISCOVERY_URL,
|
||||||
|
CLIENT_ID,
|
||||||
|
FEATURES,
|
||||||
|
FEATURES_DEFAULT_REDIRECT,
|
||||||
|
)
|
||||||
|
|
||||||
from pytest_homeassistant_custom_component.typing import ClientSessionGenerator
|
from pytest_homeassistant_custom_component.typing import ClientSessionGenerator
|
||||||
import pytest
|
import pytest
|
||||||
@@ -20,6 +28,22 @@ from custom_components.auth_oidc.endpoints.injected_auth_page import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
MOBILE_CLIENT_ID = "https://home-assistant.io/Android"
|
MOBILE_CLIENT_ID = "https://home-assistant.io/Android"
|
||||||
|
WELCOME_PATH = "/auth/oidc/welcome"
|
||||||
|
INJECTION_SCRIPT_MARKER = "<script src='/auth/oidc/static/injection.js"
|
||||||
|
|
||||||
|
|
||||||
|
def assert_redirects_to_welcome(resp) -> None:
|
||||||
|
"""Assert a response redirects to the OIDC welcome endpoint."""
|
||||||
|
assert resp.status == 302
|
||||||
|
location = resp.headers["Location"]
|
||||||
|
parsed_location = urlparse(location)
|
||||||
|
assert parsed_location.path == WELCOME_PATH
|
||||||
|
|
||||||
|
|
||||||
|
async def assert_normal_login_screen(resp) -> None:
|
||||||
|
"""Assert we stayed on the auth page and render the injected normal login HTML."""
|
||||||
|
assert resp.status == 200
|
||||||
|
assert INJECTION_SCRIPT_MARKER in await resp.text()
|
||||||
|
|
||||||
|
|
||||||
def create_redirect_uri(client_id: str) -> str:
|
def create_redirect_uri(client_id: str) -> str:
|
||||||
@@ -98,6 +122,28 @@ async def test_redirect_page_registration(
|
|||||||
assert resp2.status == 302
|
assert resp2.status == 302
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_welcome_page_default_redirect(
|
||||||
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
):
|
||||||
|
"""Test that the welcome page returns a redirect when default_redirect is preferred."""
|
||||||
|
|
||||||
|
mock_config = {
|
||||||
|
DOMAIN: {
|
||||||
|
CLIENT_ID: "dummy",
|
||||||
|
DISCOVERY_URL: "https://example.com/.well-known/openid-configuration",
|
||||||
|
FEATURES: {FEATURES_DEFAULT_REDIRECT: True},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await async_setup_component(hass, DOMAIN, mock_config)
|
||||||
|
assert result
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
resp = await client.get("/auth/oidc/welcome", allow_redirects=False)
|
||||||
|
assert resp.status == 302
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_welcome_rejects_invalid_encoded_redirect_uri(
|
async def test_welcome_rejects_invalid_encoded_redirect_uri(
|
||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
@@ -283,7 +329,9 @@ async def test_welcome_desktop_auto_redirects_without_other_providers(
|
|||||||
"""Welcome should auto-redirect desktop clients when no other providers exist."""
|
"""Welcome should auto-redirect desktop clients when no other providers exist."""
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
hass.auth._providers = [] # Clear initial providers out
|
hass.auth._providers = {} # Clear initial providers out
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
await setup(hass)
|
await setup(hass)
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
@@ -307,7 +355,7 @@ async def test_redirect_without_cookie_goes_to_welcome(
|
|||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
resp = await client.get("/auth/oidc/redirect", allow_redirects=False)
|
resp = await client.get("/auth/oidc/redirect", allow_redirects=False)
|
||||||
assert resp.status == 302
|
assert resp.status == 302
|
||||||
assert "/auth/oidc/welcome" in resp.headers["Location"]
|
assert_redirects_to_welcome(resp)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -703,10 +751,7 @@ async def test_frontend_injection(
|
|||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
resp = await client.get("/auth/authorize", allow_redirects=False)
|
resp = await client.get("/auth/authorize", allow_redirects=False)
|
||||||
assert resp.status == 200 # 200 because there is no redirect_uri
|
await assert_normal_login_screen(resp)
|
||||||
text = await resp.text()
|
|
||||||
|
|
||||||
assert "<script src='/auth/oidc/static/injection.js" in text
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -733,8 +778,12 @@ async def test_frontend_injection_logs_and_returns_when_route_handler_is_unexpec
|
|||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter([FakeRoute()])
|
return iter([FakeRoute()])
|
||||||
|
|
||||||
|
provider = MagicMock()
|
||||||
|
|
||||||
with patch.object(hass.http.app.router, "resources", return_value=[FakeResource()]):
|
with patch.object(hass.http.app.router, "resources", return_value=[FakeResource()]):
|
||||||
await frontend_injection(hass, force_https=False)
|
await frontend_injection(
|
||||||
|
hass, provider, force_https=False, has_trusted_networks_provider_first=False
|
||||||
|
)
|
||||||
|
|
||||||
assert "Unexpected route handler type" in caplog.text
|
assert "Unexpected route handler type" in caplog.text
|
||||||
assert (
|
assert (
|
||||||
@@ -753,7 +802,10 @@ async def test_injected_auth_page_inject_logs_errors(hass: HomeAssistant, caplog
|
|||||||
"custom_components.auth_oidc.endpoints.injected_auth_page.frontend_injection",
|
"custom_components.auth_oidc.endpoints.injected_auth_page.frontend_injection",
|
||||||
side_effect=RuntimeError("boom"),
|
side_effect=RuntimeError("boom"),
|
||||||
):
|
):
|
||||||
await OIDCInjectedAuthPage.inject(hass, force_https=False)
|
provider = MagicMock()
|
||||||
|
await OIDCInjectedAuthPage.inject(
|
||||||
|
hass, provider, force_https=False, has_trusted_networks_provider_first=False
|
||||||
|
)
|
||||||
|
|
||||||
assert "Failed to inject OIDC auth page: boom" in caplog.text
|
assert "Failed to inject OIDC auth page: boom" in caplog.text
|
||||||
|
|
||||||
@@ -809,5 +861,116 @@ async def test_injected_auth_page_returns_original_html_when_skipped(
|
|||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
response = await client.get(request_target, allow_redirects=False)
|
response = await client.get(request_target, allow_redirects=False)
|
||||||
|
|
||||||
assert response.status == 200
|
await assert_normal_login_screen(response)
|
||||||
assert "<script src='/auth/oidc/static/injection.js" in await response.text()
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_injected_auth_page_trusted_networks_bypass_skips_oidc_redirect(
|
||||||
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
):
|
||||||
|
"""Trusted network hosts should bypass OIDC redirect when trusted_networks is first."""
|
||||||
|
|
||||||
|
class TrustedNetworksAllowProvider:
|
||||||
|
def async_validate_access(self, _ip_addr):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
hass.auth._providers = OrderedDict(
|
||||||
|
[(("trusted_networks", None), TrustedNetworksAllowProvider())]
|
||||||
|
)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
await setup_mock_authorize_route(hass)
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
encoded_redirect_uri = quote(create_redirect_uri(client.make_url("/")), safe="")
|
||||||
|
|
||||||
|
resp = await client.get(
|
||||||
|
f"/auth/authorize?redirect_uri={encoded_redirect_uri}",
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
await assert_normal_login_screen(resp)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_injected_auth_page_ignores_trusted_networks_when_not_first(
|
||||||
|
hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
):
|
||||||
|
"""OIDC redirect should continue when trusted_networks is not the first provider."""
|
||||||
|
|
||||||
|
class DummyProvider:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TrustedNetworksAllowProvider:
|
||||||
|
def async_validate_access(self, _ip_addr):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Keep trusted_networks present but not first, so bypass should not apply.
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
hass.auth._providers = OrderedDict(
|
||||||
|
[
|
||||||
|
(("homeassistant", None), DummyProvider()),
|
||||||
|
(("trusted_networks", None), TrustedNetworksAllowProvider()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
await setup_mock_authorize_route(hass)
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
encoded_redirect_uri = quote(create_redirect_uri(client.make_url("/")), safe="")
|
||||||
|
|
||||||
|
resp = await client.get(
|
||||||
|
f"/auth/authorize?redirect_uri={encoded_redirect_uri}",
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_redirects_to_welcome(resp)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_injected_auth_page_converts_http_to_https_in_redirect(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
):
|
||||||
|
"""_get_welcome_redirect_location should convert HTTP to HTTPS when force_https is True."""
|
||||||
|
await setup(hass)
|
||||||
|
|
||||||
|
provider = hass.auth.get_auth_providers(DOMAIN)[0]
|
||||||
|
injected_page = OIDCInjectedAuthPage(
|
||||||
|
html="<html></html>",
|
||||||
|
provider=provider,
|
||||||
|
force_https=True,
|
||||||
|
has_trusted_networks_provider_first=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a mock request with HTTP URL
|
||||||
|
mock_req = MagicMock(spec=web.Request)
|
||||||
|
mock_req.url = "http://example.com/auth/authorize?redirect_uri=test"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"custom_components.auth_oidc.endpoints.injected_auth_page.get_url"
|
||||||
|
) as mock_get_url:
|
||||||
|
mock_get_url.return_value = "https://example.com/auth/oidc/welcome?redirect_uri=..."
|
||||||
|
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
injected_page._get_welcome_redirect_location(mock_req)
|
||||||
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
# Verify that the URL was converted from HTTP to HTTPS before being passed to get_url
|
||||||
|
call_args = mock_get_url.call_args
|
||||||
|
assert call_args is not None
|
||||||
|
welcome_path_with_redirect = call_args[0][0] # First positional argument to get_url
|
||||||
|
|
||||||
|
# Extract the redirect_uri parameter and decode it
|
||||||
|
parsed = urlparse(welcome_path_with_redirect)
|
||||||
|
query_params = parse_qs(parsed.query)
|
||||||
|
encoded_redirect_uri = query_params.get("redirect_uri", [None])[0]
|
||||||
|
|
||||||
|
# Decode the base64-encoded redirect_uri
|
||||||
|
if encoded_redirect_uri:
|
||||||
|
decoded_redirect_uri = base64.b64decode(unquote(encoded_redirect_uri)).decode("utf-8")
|
||||||
|
# Verify it contains https:// instead of http://
|
||||||
|
assert decoded_redirect_uri.startswith("https://example.com")
|
||||||
|
|||||||
197
uv.lock
generated
@@ -396,7 +396,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bleak"
|
name = "bleak"
|
||||||
version = "3.0.1"
|
version = "3.0.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "dbus-fast", marker = "sys_platform == 'linux'" },
|
{ name = "dbus-fast", marker = "sys_platform == 'linux'" },
|
||||||
@@ -413,9 +413,9 @@ dependencies = [
|
|||||||
{ name = "winrt-windows-foundation-collections", marker = "sys_platform == 'win32'" },
|
{ name = "winrt-windows-foundation-collections", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "winrt-windows-storage-streams", marker = "sys_platform == 'win32'" },
|
{ name = "winrt-windows-storage-streams", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/87/9f/dd19d92309e4a37823210827f0f42460e69603254309b99499622b511294/bleak-3.0.1.tar.gz", hash = "sha256:c8ff077519f8c30a972fd0d22f47a54b981184b2f2a0886d02e55acadbc1045d", size = 124162, upload-time = "2026-03-25T15:43:01.769Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/16/df/05a3f80ca8e3f7f5b0dba68a9e618147c909ccdba1468f07487dc8d72a9d/bleak-3.0.2.tar.gz", hash = "sha256:c2229cb8238d5876b4bd05c74bf7a1aea1f88da39d2e51ac9dfd5cc319d5265f", size = 125293, upload-time = "2026-05-02T23:01:04.066Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/9c/839e4ff0393070396c656fa6616d0d2512f60b571c1263183e709db1c365/bleak-3.0.1-py3-none-any.whl", hash = "sha256:49f93f24ce96610529842da2d9856e7f46597e25966c0f1cfc737f0191566de6", size = 144735, upload-time = "2026-03-25T15:43:00.285Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/54/05aceb9cd80073805b3ed8522e3196e8cb22f70e741873fa51406c31f4e7/bleak-3.0.2-py3-none-any.whl", hash = "sha256:39092feb9e83f1df5ad2f88e837723c7211c982ce9e9cda6235104bc2ebe0d0d", size = 146490, upload-time = "2026-05-02T23:01:02.592Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -505,30 +505,30 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3"
|
name = "boto3"
|
||||||
version = "1.42.88"
|
version = "1.43.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "botocore" },
|
{ name = "botocore" },
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "s3transfer" },
|
{ name = "s3transfer" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/da/bb/7d4435cca6fccf235dd40c891c731bcb9078e815917b57ebadd1e0ffabaf/boto3-1.42.88.tar.gz", hash = "sha256:2d22c70de5726918676a06f1a03acfb4d5d9ea92fc759354800b67b22aaeef19", size = 113238, upload-time = "2026-04-10T19:41:06.912Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/95/cd/bba36079f5d4bd63db7385e6b9dc1845db32407c3f18f56aaddafb75097f/boto3-1.43.2.tar.gz", hash = "sha256:be951cc22769fbcda73fac523b031ee38db45c3ae2b0d828c76b8f6e8e683073", size = 113108, upload-time = "2026-05-01T19:43:13.632Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/2b/8bfddb39a19f5fbc16a869f1a394771e6223f07160dbc0ff6b38e05ea0ae/boto3-1.42.88-py3-none-any.whl", hash = "sha256:2d0f52c971503377e4370d2a83edee6f077ddb8e684366ff38df4f13581d9cfc", size = 140557, upload-time = "2026-04-10T19:41:05.309Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/e5/c9cee72ef678dabcc27acaf8228a2d4157ad26b00e1cc5d48886f8a94c2c/boto3-1.43.2-py3-none-any.whl", hash = "sha256:796e859cfb5e93c55276ce746f8020f691eda6b68a0ec4ce4f6fd07a1cca6859", size = 140501, upload-time = "2026-05-01T19:43:10.5Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.42.88"
|
version = "1.43.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "python-dateutil" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/93/50/87966238f7aa3f7e5f87081185d5a407a95ede8b551e11bbe134ca3306dc/botocore-1.42.88.tar.gz", hash = "sha256:cbb59ee464662039b0c2c95a520cdf85b1e8ce00b72375ab9cd9f842cc001301", size = 15195331, upload-time = "2026-04-10T19:40:57.012Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2c/b0/65d4c85f16367fb6147d391652d0c386f24b029536f7026e7b98740166cd/botocore-1.43.2.tar.gz", hash = "sha256:7b2ec87b6d0720bff920451ce930e71c2a99cdea48d0eaa66ccf0b21ea747e03", size = 15301186, upload-time = "2026-05-01T19:42:59.748Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/46/ad14e41245adb8b0c83663ba13e822b68a0df08999dd250e75b0750fdf6c/botocore-1.42.88-py3-none-any.whl", hash = "sha256:032375b213305b6b81eedb269eaeefdf96f674620799bbf96117dca86052cc1a", size = 14876640, upload-time = "2026-04-10T19:40:53.663Z" },
|
{ url = "https://files.pythonhosted.org/packages/bb/52/f57ded73f1527a18e0712281eb49c4ae240038bb4dc7083fd288b4adc811/botocore-1.43.2-py3-none-any.whl", hash = "sha256:b823454d751a1c24bb403b5b07ab65007689654abb21787df923684e0743976c", size = 14982693, upload-time = "2026-05-01T19:42:54.602Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -542,11 +542,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.2.25"
|
version = "2026.4.22"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -890,7 +890,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "habluetooth"
|
name = "habluetooth"
|
||||||
version = "6.0.0"
|
version = "6.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "async-interrupt" },
|
{ name = "async-interrupt" },
|
||||||
@@ -902,26 +902,27 @@ dependencies = [
|
|||||||
{ name = "btsocket" },
|
{ name = "btsocket" },
|
||||||
{ name = "dbus-fast", marker = "sys_platform == 'linux'" },
|
{ name = "dbus-fast", marker = "sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/57/9f21b5614d984f5e311099f875182969363a3d0d25c73e0eb295ad99ec6c/habluetooth-6.0.0.tar.gz", hash = "sha256:e1e50a7e8009e54f7ec23d44d2959124bde1116e87e23aaebbc6cdce3205711c", size = 50463, upload-time = "2026-04-04T02:17:48.571Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/39/94/fcf28cd5bb9c427eb67feb0d3c0a31b7c2821be31e0d88406be9d53d4428/habluetooth-6.1.0.tar.gz", hash = "sha256:9b5ac9cb9a07bb9690f04e8587abdb5ffebb69e66163f55fbedf99d90fc554c9", size = 50852, upload-time = "2026-04-19T22:11:47.757Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/5d/8a5c1fe88470407f916764c0991f7b856bc1c4cea9ddafbff7114609d6d7/habluetooth-6.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:df52e45e3f827d09cda100c80201f86402a79efed1d9f634f52d5e74f16081ad", size = 577517, upload-time = "2026-04-04T02:34:36.924Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/10/fbe581101510d5c1d04f045d40d332e2294f1aaf85b75c8d90f363994216/habluetooth-6.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a7e03e19ca1406d031bf3569849a3a41ed0d99f109dd58eddf77f30c525a0890", size = 577925, upload-time = "2026-04-19T22:28:22.461Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/38/d8c32f0d16f8b924bc2d7cb8b402322bfc51d1396f914c077559783ed2bd/habluetooth-6.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a783c487bf75d9edd711048fcdc16ffc230a9e46afb795b2f9473277f591a8d", size = 679235, upload-time = "2026-04-04T02:34:38.406Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/5d/53ebb861788379f49c7f628f3400f39efbf7655c6d5c140d5956f4b20cd7/habluetooth-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81a6c739a4f31c89855ef41a555e3c1034e6bd3f85702a78469d32d350166d53", size = 679647, upload-time = "2026-04-19T22:28:24.003Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/dc/9f02ed6271d338562dd04863a972b47d7b81b19b0235ca962f0d716514e3/habluetooth-6.0.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4fbb7492309634296cd95911b54439e208c8d1c003b80db1c5d858c614b08639", size = 647563, upload-time = "2026-04-04T02:34:40.22Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/00/06affc9630c21da46d8aecafda55de05b55248b1a1ad7fa483b75251c7ec/habluetooth-6.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:729679ed08b1c589e89cb5d54d7579f0a484a377ca06ff028078e1607cbd8c98", size = 647972, upload-time = "2026-04-19T22:28:26.036Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/9c/35c8c2eacfd4ef81916908c571cae6ef1789e3a929c056d9ec2c13986966/habluetooth-6.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:719857e4a33538d6dc25a89a734ab86a694256a87e3e27acfc2e524c5416be86", size = 718619, upload-time = "2026-04-04T02:34:42.06Z" },
|
{ url = "https://files.pythonhosted.org/packages/ac/47/5b3c8ff9611bf6946828a1bf54112cbba2e491db9f837eb7f128d3d031b1/habluetooth-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76ebcc33b81d8990ff13d8fa7797402c422fa68618d70217136b5b6e4bd1697", size = 719028, upload-time = "2026-04-19T22:28:27.746Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/c2/e96630466133d2572bd9459d57739047c53f9290823ed9e90800591cf6c5/habluetooth-6.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf67ff07f8784308ad698a14562731b153087fdbb8eac9067640de2fb9e5599d", size = 688598, upload-time = "2026-04-04T02:34:44.002Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/73/da31a80d5c0f547a50ae1b55302eafbbd659b53bcb00dd8801609f805fe0/habluetooth-6.1.0-cp314-cp314-manylinux_2_41_x86_64.whl", hash = "sha256:7fd1aebff1f3888e731e6fa470cc13a8cf0450932f5202885793c8e5bcb9d17d", size = 717793, upload-time = "2026-04-19T22:11:46.296Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/11/b97d71ebe182fdd1e130d520e1e33d375e4b67db421b9ffda30345d4c68f/habluetooth-6.0.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90ae8d7f1d173a3dd872d006e410870d5b517b08caec80084adc879893ce3ef5", size = 652250, upload-time = "2026-04-04T02:34:45.748Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/f2/821ae8cbfd84a98ec5d2ddab9b6e35ab88a1a66e261527a376038b91aa2f/habluetooth-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5bcbf145957d39358ae99c07ee04db5b303f4dd4c9cb3e210b14597726da47", size = 689008, upload-time = "2026-04-19T22:28:29.579Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/53/46356f493a710f48428f81b9861f7679eb80cd23db92b9773a1de725af42/habluetooth-6.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:648e3aeb33e440f746cf5dd73ddd23ec58560431d55756a88cd91664463cfcd1", size = 724701, upload-time = "2026-04-04T02:34:47.585Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/51/22c96fffc0f8454d2ab79600c106e9fff00511241e71373c509d40f09e6f/habluetooth-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:e35ed0d81731f434ba0c8c8bdac73570ad5336b3f86b66a276338391da299da0", size = 652662, upload-time = "2026-04-19T22:28:31.446Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/ad/8d16720c114ce34166ce5edb63389e22e8e0f8305f3bac1d7043ebfee87e/habluetooth-6.0.0-cp314-cp314-win32.whl", hash = "sha256:2d030866124a0f56b9a2d3f6941685673ca39932e80dd150e1f389b6cc2abd34", size = 470304, upload-time = "2026-04-04T02:34:49.445Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/17/414ca5cf3972bca7c183a361725f7175cbfd113e7594725095a2d59189bb/habluetooth-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4e7766ce743e42eea2820fbacebc688cd3b9ed90cdb61676f37c5d92ec7b458e", size = 725110, upload-time = "2026-04-19T22:28:33.16Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/74/ccd0afa1e857d2098d7c9c8ddd742d8c330bbdb4593864e4c997b6ef0e16/habluetooth-6.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:f196dc23c1291db2b29a3137df39a182f89554e1c02f2355f44dd4b3a8ef9ce7", size = 544551, upload-time = "2026-04-04T02:34:51.042Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/ab/5d89e71c8abbb73f2e7cfde63381341fd802aad3d315e3b24c465f37e665/habluetooth-6.1.0-cp314-cp314-win32.whl", hash = "sha256:415e5ba996bfbffc571a751a6dc5a13f55846be24fc07f7d6ac310a0bd84fc05", size = 470707, upload-time = "2026-04-19T22:28:34.645Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/83/eaacde907945580e9fd50e613f170cd4c32a2607d0b1720ba58e0db53484/habluetooth-6.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f422e812fc969dbb236808341ce4c4c81616fd90dc6cfade62815a79f34743bd", size = 1145637, upload-time = "2026-04-04T02:34:52.555Z" },
|
{ url = "https://files.pythonhosted.org/packages/de/db/697c3e97aa0a298ecc12afc287d244001b019aef226e3a3781328ac33ce1/habluetooth-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4b31f6a827f12665d0ccd13b16b709740c66b060f4b7cdd294288a731b58eca1", size = 544957, upload-time = "2026-04-19T22:28:36.218Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/53/ebd308d8c57959def76d0c16dff2407e2baf5727c06e12365baf1f45bfeb/habluetooth-6.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdf8e23f58c0e816185db17221f33a9de1ffd5fae57e1c6e3eb5632732d9d7b1", size = 1300632, upload-time = "2026-04-04T02:34:54.276Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/a8/e6dc190a5008cd099961a2ded31e09061722b9d416ccfd4d185730da4ea3/habluetooth-6.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf41c1eb278968e73d00c4f229e3963f3595087f2568e7454645a4a34327442d", size = 1146040, upload-time = "2026-04-19T22:28:38.318Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/6c/371e21087113505d39d128da3629498c30305a5042951f263569cfc5c5db/habluetooth-6.0.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f71cd27ff431bb251d196d70be2593ec770239405c5450c96cf6d495341e9a4", size = 1219888, upload-time = "2026-04-04T02:34:56.053Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/91/deec7d3b9d32f3471e25b580f1182acde6799de599619de3e568f788f912/habluetooth-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b47141ca110f97c6f9d60673056faa8d49ee1d43700e81ce3cfc8a80f6414ff9", size = 1301043, upload-time = "2026-04-19T22:28:40.305Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/29/0d9de94ec3b8e6174d0b3ad4357c553e7de9edf3a4199ddafb0fd72b1585/habluetooth-6.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:157c4e9cef4a6b356af6cbe080ce360c6264d42d8d9369e755a924ca09e2458f", size = 1362404, upload-time = "2026-04-04T02:34:57.802Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/d5/d5df26aa19237a16be3df52a447e24ea949e5273394088e9f3bc2b7e0764/habluetooth-6.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:553cb8cbfdd50a42abf68b92b1625a9beb0b5d19d78480199c14440ca787288a", size = 1220296, upload-time = "2026-04-19T22:28:42.05Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/5e/aabd9f17fc366d52d0b3d0cf799d21114005cb3c218e5c02678ce519f698/habluetooth-6.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:764ea1bde89fe5b548338cf0ef7cd9a43e04b3a0f833f09a020b8d1cf5ce0472", size = 1320521, upload-time = "2026-04-04T02:34:59.463Z" },
|
{ url = "https://files.pythonhosted.org/packages/f9/20/907061cbe491dfb53ba1c23622460e317f50ad7935747045e69d690345b9/habluetooth-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a48b8a2fe8a903ffe656d94b6337b43dc1d3d3f0965550fff9a1294f0df3c0c0", size = 1362816, upload-time = "2026-04-19T22:28:43.808Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/3f/24ab240582eff51d9756f0cc1a8b3ccd318bf8bf9429ae024871aca057a1/habluetooth-6.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:6e0ea6a4b93eb7f528588190e8154b75bacd882b50f718dc08ffdadbb3d8724a", size = 1241857, upload-time = "2026-04-04T02:35:01.084Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/51/a433154052d282ac753a5f5146fa13a3cfbf677412733a276c3ffbf41199/habluetooth-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c62c552f6778ddbac0310a153a08e5059aa876bf4b73d66073d660e140286f6c", size = 1320933, upload-time = "2026-04-19T22:28:45.318Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/e5/3019d9cc58f0c2a77dc1fdce97202a2b008828900c00329e50036030c363/habluetooth-6.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:95076bfe54126e92afc5d61e82e7eddfeeef7bfac8fc6f126edf80dabc212f80", size = 1377691, upload-time = "2026-04-04T02:35:02.784Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/53/e2ff9e460f38f4bf95c45cc90ae5b8de874a8957ff6a28b9352e41198ed2/habluetooth-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3435d77d447cce135594cdde0d9579c7de507d3fb82d77d551eeb5ef9c8ee290", size = 1242264, upload-time = "2026-04-19T22:28:47.028Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/f9/a38ead8303706e9c5c0d6c5d8d444b6a54143f8c26582c5183c2b2efb54f/habluetooth-6.0.0-cp314-cp314t-win32.whl", hash = "sha256:b458186b5c80f3031d5b30b19882cd3eb68969fea5e1b358b288518a11fc14fc", size = 973476, upload-time = "2026-04-04T02:35:04.514Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/c2/220dd1ccca28e312f4ca83453c0d72de09d1a9cd1b3460d0b4373714a72e/habluetooth-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5682c7a269acd41ee78cc7abb8b0225c70e39c22c1db888bcd26629d0a3abff", size = 1378101, upload-time = "2026-04-19T22:28:48.708Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/57/e1d4be955c6b617a12fef18f988b3959c2afb26f214feef3a50e7b564dbd/habluetooth-6.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c7ef2e28e05f658c283700611a1faef95f09e6716596467939fdf4ed7cd4c413", size = 1147320, upload-time = "2026-04-04T02:35:06.497Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/1a/b28fc02284d3e0155843441fea353ec916b362e14d726a564b8fdea2a84e/habluetooth-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:aa5d791584572f08ff5e5141de0ed368909d62d34ce1c739e8d9ddaa346474fe", size = 973884, upload-time = "2026-04-19T22:28:50.634Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/ff/0a3dd553a7d9dc67f29b0ac4627a43aeb69703bf3ffb4d2476ea9605a51a/habluetooth-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c625f1e1afac4bd7f834a3b29c742789d1199029d84bff183799e06a0334eb9b", size = 1147734, upload-time = "2026-04-19T22:28:52.331Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1005,7 +1006,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2026.4.2"
|
version = "2026.4.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiodns" },
|
{ name = "aiodns" },
|
||||||
@@ -1059,9 +1060,9 @@ dependencies = [
|
|||||||
{ name = "yarl" },
|
{ name = "yarl" },
|
||||||
{ name = "zeroconf" },
|
{ name = "zeroconf" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/47/031d06d33af9545e68f33b808dedd823ba98e06e40ce59f77d804df177b4/homeassistant-2026.4.2.tar.gz", hash = "sha256:791770614fc3e008c66c87cd6ee9815017dbb2bfcfd22d64d0a45a0b9fb58df8", size = 32357303, upload-time = "2026-04-11T18:56:55.596Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/96/3d/041a66485642537286c4b1a3ee66ace7ce43cfb61737e224cf01e6b59c97/homeassistant-2026.4.4.tar.gz", hash = "sha256:10f997fb7c00b2f8abbe30f343469428665b68d5a1e665feee2b827d9d815212", size = 32333699, upload-time = "2026-04-24T18:58:32.073Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/e5/b29b6d65baf0012a008359f50b25bf15e0cfaed49088371115da052456cf/homeassistant-2026.4.2-py3-none-any.whl", hash = "sha256:7ecd6e5de22515596eee05ba69c6147a4c95ba846b2b5440b87f926c45e9ebd9", size = 53530765, upload-time = "2026-04-11T18:56:49.946Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/cb/120eb528c6eb5a21bc726aff1bc47898e5353a677ec18d33d65b11b8f3f8/homeassistant-2026.4.4-py3-none-any.whl", hash = "sha256:cd83240d320e9842822810b4c4aa13a132dbf7954d10576f406e66fca645467f", size = 53499095, upload-time = "2026-04-24T18:58:24.834Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1103,11 +1104,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.11"
|
version = "3.13"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1172,14 +1173,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "joserfc"
|
name = "joserfc"
|
||||||
version = "1.6.3"
|
version = "1.6.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/90/b8cc8635c4ce2e5e8104bf26ef147f6e599478f6329107283cdc53aae97f/joserfc-1.6.3.tar.gz", hash = "sha256:c00c2830db969b836cba197e830e738dd9dda0955f1794e55d3c636f17f5c9a6", size = 229090, upload-time = "2026-02-25T15:33:38.167Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/4f/124b3301067b752f44f292f0b9a74e837dd75ff863ee39500a082fc4c733/joserfc-1.6.3-py3-none-any.whl", hash = "sha256:6beab3635358cbc565cb94fb4c53d0557e6d10a15b933e2134939351590bda9a", size = 70465, upload-time = "2026-02-25T15:33:36.997Z" },
|
{ url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1357,11 +1358,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.0"
|
version = "26.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1408,11 +1409,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pip"
|
name = "pip"
|
||||||
version = "26.0.1"
|
version = "26.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/73/7e/d2b04004e1068ad4fdfa2f227b839b5d03e602e47cdbbf49de71137c9546/pip-26.1.tar.gz", hash = "sha256:81e13ebcca3ffa8cc85e4deff5c27e1ee26dea0aa7fc2f294a073ac208806ff3", size = 1840316, upload-time = "2026-04-26T21:00:05.406Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/7a/be4bd8bcbb24ea475856dd68159d78b03b2bb53dae369f69c9606b8888f5/pip-26.1-py3-none-any.whl", hash = "sha256:4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1", size = 1812804, upload-time = "2026-04-26T21:00:03.194Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1864,7 +1865,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-homeassistant-custom-component"
|
name = "pytest-homeassistant-custom-component"
|
||||||
version = "0.13.323"
|
version = "0.13.325"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohasupervisor" },
|
{ name = "aiohasupervisor" },
|
||||||
@@ -1895,9 +1896,9 @@ dependencies = [
|
|||||||
{ name = "syrupy" },
|
{ name = "syrupy" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/a7/ee5fe414ba6ada3ebf27852082e5c42acdd4bf7c7340787583c7118ddf11/pytest_homeassistant_custom_component-0.13.323.tar.gz", hash = "sha256:ff0cdbfd39c26afdf021363f1a600367a29e96ff3d0ee68f5fba8cab9afeb0b6", size = 69948, upload-time = "2026-04-12T05:41:56.511Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/40/e14846938decc2073b78d2c88a08444973aae82b7fb263ba0073aca89793/pytest_homeassistant_custom_component-0.13.325.tar.gz", hash = "sha256:12924ad407b6601748d3a3c7883423fd0d34b1f782f14b615d1b61d994f371fc", size = 69950, upload-time = "2026-04-25T05:37:23.337Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/55/798e8f648cd7efeb723aabf0fee33f620522459eba229496709952bb6c21/pytest_homeassistant_custom_component-0.13.323-py3-none-any.whl", hash = "sha256:0061c442358dc0ff94a5e027b07c8f47a14817f4c708343d67f1690784ea2f94", size = 75787, upload-time = "2026-04-12T05:41:55.202Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/c7/51cac710f1b6679550d04e162899dbdf4e7289ba715e5c4a9bff6bec7cfa/pytest_homeassistant_custom_component-0.13.325-py3-none-any.whl", hash = "sha256:9b758a092f74722e060d252ac80d71b6ae1ec2a712d55d0c9d378266e74e2c05", size = 75792, upload-time = "2026-04-25T05:37:21.529Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1987,11 +1988,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2026.1.post1"
|
version = "2026.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2101,39 +2102,39 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.10"
|
version = "0.15.12"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
|
{ url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
|
{ url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
|
{ url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "s3transfer"
|
name = "s3transfer"
|
||||||
version = "0.16.0"
|
version = "0.17.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "botocore" },
|
{ name = "botocore" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/ec/7c692cde9125b77e84b307354d4fb705f98b8ccad59a036d5957ca75bfc3/s3transfer-0.17.0.tar.gz", hash = "sha256:9edeb6d1c3c2f89d6050348548834ad8289610d886e5bf7b7207728bd43ce33a", size = 155337, upload-time = "2026-04-29T22:07:36.33Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/72/c6c32d2b657fa3dad1de340254e14390b1e334ce38268b7ad51abda3c8c2/s3transfer-0.17.0-py3-none-any.whl", hash = "sha256:ce3801712acf4ad3e89fb9990df97b4972e93f4b3b0004d214be5bce12814c20", size = 86811, upload-time = "2026-04-29T22:07:34.966Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2291,11 +2292,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2026.1"
|
version = "2026.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2354,28 +2355,28 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uv"
|
name = "uv"
|
||||||
version = "0.11.6"
|
version = "0.11.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c1/cd/4393fecb083897e956f016d4e66d0b8a496a08fe2e03cbda32a1e91da7ee/uv-0.11.8.tar.gz", hash = "sha256:bb2cf302b8503629aab6f0090a05551e6f8cfc2d687ca059cad7ec9e11214335", size = 4098020, upload-time = "2026-04-27T13:15:31.625Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" },
|
{ url = "https://files.pythonhosted.org/packages/99/84/dcb676a3e36a3a2b44dc2e4dfea471b8cd709025e27cce3e588b176fd899/uv-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:a53e704a780a9e78a50f5a880e99a690f84e6fb9e82610903ce26f47c271d74c", size = 23664296, upload-time = "2026-04-27T13:15:15.644Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/05/557aa070fda7b8460bbbe1e867e8e5b80602c5b30ed77d1d94fc5acae518/uv-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d414fc3795b6f56fb6b1fa359537930924fdfe857750a144d2aedf3077be3f1d", size = 23087321, upload-time = "2026-04-27T13:15:36.193Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/62/82953018801a250e16b091ef4b5e95e939b2f01224363d6fc80f600b7eff/uv-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f0d402e182ab581e934c159cc9edf25ec6e08d32f29aa797980e949afefc87cd", size = 21747142, upload-time = "2026-04-27T13:15:20.4Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/4c/477f2abe16f9a3d3c73077f15615878a303eef3760115ec946be58ecb9b2/uv-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:877c9af3b3955a35ef739e5b2ba79c56dae5c4d50420a7ed908c0901e1c8c807", size = 23425861, upload-time = "2026-04-27T13:15:10.374Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/63/19f46193e49f0c9bf33346a4d726313871864db16e7cdd1c0a63bc112000/uv-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:8278144df8d80a83f770c264a5e79ea50791316d2a0dda869e53b3c1174142a8", size = 23215551, upload-time = "2026-04-27T13:15:38.706Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/3e/5595b265df848a33cd060b10e8f763a46d67521ac9f6c314e8a4ad5329d7/uv-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3494ad32465f4e02259cfb104d24efe5bb8f7a782351f0354de9385415fb310", size = 23224170, upload-time = "2026-04-27T13:15:18.083Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" },
|
{ url = "https://files.pythonhosted.org/packages/a6/b3/6ca95e690b52542caa1dae10ede57732f90c629946ab5f027ff746f87deb/uv-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4421e27e81f85bce3bdb75986c38b5f9bfab9cdccaf3d977cf124b3f0f0b989", size = 24730048, upload-time = "2026-04-27T13:15:13.254Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/49/71b7322067c85a3736a22a300072b0566991fe3f95b81bed793508ff5315/uv-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91943e77fc962752d4f64ad5739219858395981078051c740b28b52963b366aa", size = 25585906, upload-time = "2026-04-27T13:15:41.455Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" },
|
{ url = "https://files.pythonhosted.org/packages/37/16/4e84cd5131327fe86d4784ebfc8a983149f4e6b811476ef271fc548b29e6/uv-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:41fbba287efcc9bc9505a60549b3a223220da720eacd03be8c23d9daaafa44f4", size = 24795740, upload-time = "2026-04-27T13:15:49.842Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" },
|
{ url = "https://files.pythonhosted.org/packages/5b/01/df175979018743cc5ba6e2fb9dcec916868271e8d88cf0b9df8fd805a0df/uv-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d97bb2920d6cddc07faa475013461294cc09b77ec8139278416c6e54b938d037", size = 24824980, upload-time = "2026-04-27T13:15:53.506Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" },
|
{ url = "https://files.pythonhosted.org/packages/1c/95/93c7f595f7136fb32807442860c55d0faed2cd3d7da4b7105ed3c2535d5f/uv-0.11.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:fb6a755305eb1e081dfe6a8bc007dbae2d26fe75e551656ca7c9cd08fba21d26", size = 23526790, upload-time = "2026-04-27T13:15:04.955Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/02/77430b89e172c20cc549b07a5b1dfda0c882c161b6d82781d3150a7063ac/uv-0.11.8-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:841ecbb38532698f73b14b49dc5f0c5e756194c7fcf6e5c6b7ed3859200fe91b", size = 24280498, upload-time = "2026-04-27T13:15:43.978Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/e3/23e4a2bb91e3880e017e6116886e2d0bde14ba6aa95ddc458160ee630e7c/uv-0.11.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b3ff2b20c1897105ebe7ed7f9b1b331c7171da029bc1e35970ce31dc086141c1", size = 24375233, upload-time = "2026-04-27T13:15:25.753Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" },
|
{ url = "https://files.pythonhosted.org/packages/d9/67/fb7dc17cea816a667d1be2632525aa1687566bfafd17bdac561a7a6c9484/uv-0.11.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ad381228b0170ef9646902c7e908d4a10a7ecc3da8139450506cf70c7e7f3e80", size = 23904818, upload-time = "2026-04-27T13:15:23.21Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" },
|
{ url = "https://files.pythonhosted.org/packages/4b/91/b920e35f54f8c6b51f2c639e8170bb80a47a739a1442fea33a479bc93a3d/uv-0.11.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0172b5215544844cd3db0fa3c73a2eb74999b3f00cd2527dde578725076d7b65", size = 25015448, upload-time = "2026-04-27T13:15:46.666Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/e8/3771956dc1c94b8484789bb8070d91872080d0af99332b8bdec7218c2bfd/uv-0.11.8-py3-none-win32.whl", hash = "sha256:e71c1dd23cbb480f3952c3a95b4fd00f96bd618e2a94583fc9388c500af3070d", size = 22823583, upload-time = "2026-04-27T13:15:33.674Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" },
|
{ url = "https://files.pythonhosted.org/packages/f9/9b/a91a9c60dcae0e1e3da06377d38f32118a523697d461fe41bc9f117ecf59/uv-0.11.8-py3-none-win_amd64.whl", hash = "sha256:306c624c68d95dd7ea3647675323d72c1abc25f91c3e92ae4cd6f0f11b508726", size = 25407438, upload-time = "2026-04-27T13:15:28.957Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" },
|
{ url = "https://files.pythonhosted.org/packages/61/5d/defa29fe617e6f07d4e514089e9d36fd9f44ede869e597e39ff7d69f6917/uv-0.11.8-py3-none-win_arm64.whl", hash = "sha256:a9853456696d579f206135c9dda7227a6ed8311b8a9a0b9b2008c4ae81950efe", size = 23914243, upload-time = "2026-04-27T13:15:07.717Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||