Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1355653
Initial commit
erwindouna Feb 19, 2026
7080ac9
Feedback
erwindouna Feb 19, 2026
8e6c9c7
Update docs/core/platform/oauth2.md
erwindouna Feb 24, 2026
0921e4b
Update docs/core/platform/oauth2.md
erwindouna Feb 24, 2026
e27a9bb
Update docs/core/platform/oauth2.md
erwindouna Feb 24, 2026
6a82715
Update docs/core/platform/oauth2.md
erwindouna Feb 24, 2026
a2e8b3b
Update docs/core/platform/oauth2.md
erwindouna Feb 24, 2026
ec55f36
Update docs/core/platform/oauth2.md
erwindouna Feb 24, 2026
50c5ec9
Add token comment
erwindouna Feb 24, 2026
86a9636
Merge branch 'master' into oauth-docs
erwindouna Feb 24, 2026
ebede11
Update docs/core/platform/oauth2.md
erwindouna Feb 25, 2026
b67aadf
Replace
erwindouna Feb 25, 2026
6522124
delete
erwindouna Feb 25, 2026
b82f6ff
Add to sidebar
erwindouna Feb 25, 2026
4037d43
Refer to library
erwindouna Feb 25, 2026
11288b9
Merge branch 'master' into oauth-docs
erwindouna Feb 25, 2026
2284532
Update docs/oauth2.md
erwindouna Mar 11, 2026
9bd89b0
Update docs/oauth2.md
erwindouna Mar 11, 2026
ccd1937
Update docs/oauth2.md
erwindouna Mar 11, 2026
0f77422
Update docs/oauth2.md
erwindouna Mar 11, 2026
88298b9
Update docs/oauth2.md
erwindouna Mar 11, 2026
ea745b6
Update docs/oauth2.md
erwindouna Mar 11, 2026
63334fb
Update docs/oauth2.md
erwindouna Mar 11, 2026
f6f8bfe
Apply suggestions from code review
erwindouna Mar 11, 2026
d420a37
Merge branch 'master' into oauth-docs
erwindouna Mar 11, 2026
9449aef
Feedback
erwindouna Apr 9, 2026
f296d88
Merge branch 'master' into oauth-docs
erwindouna Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions docs/oauth2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
---
title: "OAuth 2.0 support"
---

Integrations that connect to cloud services often need to authenticate users via OAuth 2.0. Home Assistant provides a set of helpers in `homeassistant.helpers.config_entry_oauth2_flow` that handle the OAuth 2.0 flow, token storage, and token refresh lifecycle — so integrations don't have to implement this themselves.

This page covers how to implement OAuth 2.0 in an integration, how to handle errors and best practices.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we could mention the library guide:

https://developers.home-assistant.io/docs/api_lib_auth#oauth2

Ie that this page covers implementing OAuth2 in an integration while the library guide page covers implementing Oauth2 in a 3rd party library that will be used for an integration.


Before reading this page, make sure you are familiar with [Application credentials](/docs/core/platform/application_credentials) and [Config entries](/docs/config_entries_index).

## Overview

Home Assistant's OAuth 2.0 helper provides:

- A built-in Authorization Code flow via `config_entry_oauth2_flow`.
- Automatic token refresh when an access token expires.
- A session helper (`OAuth2Session`) for making authenticated API requests.
- Error handling via a set of semantic exceptions

The helper supports two credential approaches, both of which require `application_credentials` support.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should mention what two credential approaches we mean here.


It's encouraged to use the built-in `config_entry_oauth2_flow` for standard Authorization Code flows. Use the existing template flows that inherit from `AbstractOAuth2FlowHandler`. As a last resort, only build own child flows if it's needed.
Comment thread
erwindouna marked this conversation as resolved.
Outdated
Comment thread
erwindouna marked this conversation as resolved.
Outdated

## Supported OAuth 2.0 flows

| Flow | Class | When to use |
| ---------------------------- | ----------------------------------- | ------------------------------- |
| Authorization code | `LocalOAuth2Implementation` | Standard browser-based flow |
| Authorization code with PKCE | `LocalOAuth2ImplementationWithPkce` | When the provider requires PKCE |
| Custom | `AbstractOAuth2Implementation` | Any non-standard flow |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not sure if we should talk about custom flows, since we don't know that we can support them in a good way.


## Implementing the config flow

The integration's config flow must extend `AbstractOAuth2FlowHandler`:

```python
from homeassistant.helpers import config_entry_oauth2_flow


class OAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Handle the OAuth2 config flow."""

DOMAIN = DOMAIN

@property
def logger(self) -> logging.Logger:
"""Return the logger."""
return logging.getLogger(__name__)

@property
def extra_authorize_data(self) -> dict[str, Any]:
"""Return extra authorization parameters."""
return {
"scope": "access:offline",
}
```

The `extra_authorize_data` property is where you define the OAuth scopes and any other provider specific parameters required during the authorization request.
Comment thread
erwindouna marked this conversation as resolved.

### Reauthentication

Home Assistant will refresh the access token when `async_ensure_token_valid` is called. However, if a token becomes permanently invalid (for example, if the user revokes access from the provider's website), Home Assistant will trigger a reauthentication flow. To support this, add `async_step_reauth` in your config flow:

```python
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication."""
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication."""
if user_input is None:
return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the same example as here (or link to that example):

https://developers.home-assistant.io/docs/config_entries_config_flow_handler#reauthentication

```
Comment on lines +88 to +99
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for examples of async_oauth_create_entry in the Home Assistant codebase to find correct pattern

rg -nP -A10 'async_oauth_create_entry' --type=py -g '!test*' -C3

Repository: home-assistant/developers.home-assistant

Length of output: 232


🏁 Script executed:

cat -n docs/oauth2.md | sed -n '80,110p'

Repository: home-assistant/developers.home-assistant

Length of output: 1470


🏁 Script executed:

cat -n docs/oauth2.md | sed -n '60,99p'

Repository: home-assistant/developers.home-assistant

Length of output: 2038


🏁 Script executed:

cat -n docs/oauth2.md | sed -n '99,130p'

Repository: home-assistant/developers.home-assistant

Length of output: 1889


🏁 Script executed:

rg -n 'user_id' docs/oauth2.md -B3 -A3

Repository: home-assistant/developers.home-assistant

Length of output: 437


🏁 Script executed:

# Search for actual working oauth2 implementations or references in the documentation
rg -n 'async_set_unique_id' docs/oauth2.md -B5 -A5

Repository: home-assistant/developers.home-assistant

Length of output: 584


🏁 Script executed:

# Search for examples of how other methods extract data or set unique_id in the file
rg -n 'async_step_user\|data\[' docs/oauth2.md -B2 -A2 | head -50

Repository: home-assistant/developers.home-assistant

Length of output: 66


🏁 Script executed:

# Check the beginning of the file for context and other examples
cat -n docs/oauth2.md | head -60

Repository: home-assistant/developers.home-assistant

Length of output: 3444


🏁 Script executed:

# Look for any comments or docstrings that might explain what data contains or where user_id comes from
cat -n docs/oauth2.md | sed -n '30,100p' | grep -i 'user\|data\|unique'

Repository: home-assistant/developers.home-assistant

Length of output: 1306


Extract user_id from the data parameter on line 90.

The code example references an undefined user_id variable. Since the function receives data: dict, extract the user identifier from it. Change line 90 to: self.async_set_unique_id(data['user_id']) or use data.get('user_id') depending on whether user_id is guaranteed to be present.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/oauth2.md` around lines 88 - 99, In async_oauth_create_entry, the code
calls self.async_set_unique_id(user_id) but user_id is undefined; extract the id
from the incoming data dict (e.g., obtain user_id = data.get('user_id') or
data['user_id'] depending on guarantees) and pass that to
self.async_set_unique_id, and if using get() add a guard to handle a missing
user_id (raise/abort with a clear error) before continuing; update the reference
in async_oauth_create_entry accordingly.


## Making authenticated API requests

Use `OAuth2Session` to make authenticated requests. It automatically injects a valid access token into each request and handles token refresh transparently.

```python
from homeassistant.helpers import config_entry_oauth2_flow

session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)

# The session handles token refresh, inside the library, when `async_ensure_token_valid` is called. This must be called before every request.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should end the code block here before continuing and write a new paragraph explaining that we want integrations to create the session and then pass that session to the client library so that the library uses the same session for the requests, including token refresh. Link to the library guide in that paragraph.

Then after the paragraph we can make a new code example block for the two lines below showing how the library will use the session. It's important to make two different code blocks since the first is for the integration and the second is for the library.

await session.async_ensure_token_valid()
response = await session.async_request("GET", "https://api.example.com/data")
```


- `async_ensure_token_valid` - refreshes the token if needed but does **not** return the token. This needs to done before every request to ensure there's a valid token. The token can be obtained from the `OAuth2Session.token` property.
Comment thread
erwindouna marked this conversation as resolved.
Outdated
Comment thread
erwindouna marked this conversation as resolved.
Outdated

See [Error handling](#error-handling) below for how to handle errors during token requests.

## Error handling

When a token request or refresh fails, the OAuth 2.0 helper raises one of three exceptions defined in `homeassistant.helpers.config_entry_oauth2_flow`:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exceptions are defined in exceptions.py in the root, ie not in the helper.


| Exception | HTTP status | Meaning |
| ---------------------------------- | -------------------- | -------------------------------------------------------------------------------- |
| `OAuth2TokenRequestReauthError` | 400–499 (except 429) | Non-recoverable. The token is invalid and the user must reauthenticate. |
| `OAuth2TokenRequestTransientError` | 500+ and 429 | Transient. The server is temporarily unavailable or rate-limited. Safe to retry. |
| `OAuth2TokenRequestError` | Base class | Catch-all for token request failures not covered by the above two. |

All three exceptions inherit from `aiohttp.ClientResponseError` for backwards compatibility, but integrations should migrate to catching the new exceptions directly.

### Integrations using the Data Update Coordinator

If your integration uses the [Data Update Coordinator](/docs/integration_fetching_data/#coordinated-single-api-poll-for-data-for-all-entities), no special error handling is required. The coordinator automatically maps the new exceptions to the correct behavior:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add: "Make sure to do a first coordinator refresh during config entry setup, to ensure the access token is refreshed."


- `OAuth2TokenRequestReauthError` raises `ConfigEntryAuthFailed`, triggering a reauthentication flow
- `OAuth2TokenRequestTransientError` treated as `UpdateFailed`, triggering the coordinator's retry mechanism
Comment thread
erwindouna marked this conversation as resolved.

### Integrations without a Data Update Coordinator

If your integration does **not** use a coordinator, you must handle the exceptions explicitly wherever you call `async_get_access_token()` or `async_ensure_token_valid()`:
Comment thread
erwindouna marked this conversation as resolved.
Outdated

```python
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady

try:
await session.async_get_access_token()
Comment thread
erwindouna marked this conversation as resolved.
Outdated
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="reauth_required",
) from err
except (OAuth2TokenRequestTransientError, OAuth2TokenRequestError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="auth_server_error",
) from err
```

## Complete examples

### With Data Update Coordinator

Look at the [library guide](/docs/api_lib_auth) on authentication for more information on building guidelines. The following examples act merely as an example how to interlink a library with OAuth2.0 in the Data Update Coordinator.
Comment thread
erwindouna marked this conversation as resolved.
Outdated
Comment thread
erwindouna marked this conversation as resolved.
Outdated

```python
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed


class ExampleCoordinator(DataUpdateCoordinator[MyData]):
"""Coordinator for the Example integration."""

def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
session: config_entry_oauth2_flow.OAuth2Session,
client: LibraryClient
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
logger=logging.getLogger(__name__),
name=DOMAIN,
update_interval=timedelta(minutes=30),
)
self.session = session
self.client = client

async def _async_setup(self) -> None:
"""Setup the coordinator."""
try:
self.client = ExampleApiClient(session=session)
except ApiClientAuthenticationError as err:
raise ConfigEntryAuthFailed(f"Error authenticating with library: {err}") from err

Comment thread
erwindouna marked this conversation as resolved.
Outdated
async def _async_update_data(self) -> MyData:
"""Fetch data from the API."""
try:
return await client.async_get_data()
except ApiClientError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
Comment thread
erwindouna marked this conversation as resolved.
```

### Without Data Update Coordinator

```python
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Example from a config entry."""
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
session = OAuth2Session(hass, entry, implementation)

try:
await session.async_ensure_token_valid()
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="reauth_required",
) from err
except (OAuth2TokenRequestTransientError, OAuth2TokenRequestError) as err:
Comment thread
erwindouna marked this conversation as resolved.
Outdated
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="auth_server_error",
) from err

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = session
Comment thread
erwindouna marked this conversation as resolved.
Outdated
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
```

## Best practices

- Never catch `aiohttp.ClientResponseError` directly. Use the new OAuth exception hierarchy instead. The compatibility shim will eventually be removed.
- Use the Data Update Coordinator where possible. It handles token refresh errors automatically and reduces the amount of boilerplate in each integration.
- Don't put token logic in entity classes. Token management belongs in `async_setup_entry` or the coordinator, not in individual entity `async_update` methods.
- Always handle `OAuth2TokenRequestReauthError` explicitly in integrations that don't use a coordinator. Failing to do so means the user will never be prompted to reauthenticate.
- Raise `ConfigEntryNotReady` for transient errors. Transient errors are temporary and should be retried. Raise `ConfgEntryAuthFailed` for non-recoverable errors.
Comment thread
erwindouna marked this conversation as resolved.
- Always implement reauthentication (`async_step_reauth`) in your config flow so Home Assistant can prompt the user to re-link their account.
- Use `extra_authorize_data` to specify scopes and parameters required by the provider during authorization. This keeps your implementation clean and focused on the provider's requirements.

## Reference

- [Blog post: Changes in OAuth 2.0 helper error handling](https://developers.home-assistant.io/blog/2026/02/19/oauth-token-request-error-handling)
1 change: 1 addition & 0 deletions sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ module.exports = {
"core/integration_diagnostics",
"core/integration_system_health",
"configuration_yaml_index",
"oauth2",
"dev_101_services",
"creating_platform_index",
"creating_component_generic_discovery",
Expand Down