Skip to content

Commit

Permalink
Implement initial flow (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
christiaangoossens authored Dec 24, 2024
1 parent 1c8c7ed commit 8ba494c
Show file tree
Hide file tree
Showing 15 changed files with 886 additions and 1,808 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2022 Christiaan Goossens
Copyright 2024 Christiaan Goossens

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
64 changes: 53 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,70 @@
# OIDC Auth for Home Assistant

Status: in progress, but very slowly.
> [!CAUTION]
> This is a pre-alpha release. I give no guarantees about code quality, error handling or security at this stage. Please treat this repo as a proof of concept for now and only use it on development HA installs.
Current roadblocks:
Provides an OIDC implementation for Home Assistant.

- [ ] Find a way to do a redirect within the login step in Home Assistant, we should not use window.open
- [ ] Find out how to make this redirect work on all platforms (including mobile)
### Background
If you would like to read the background/open letter that lead to this component, please see 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.

If this is solved, implementing OIDC itself is doable.
## How to use
### Installation

If you have any tips or would like to contribute, send me a message.
Add this repository to [HACS](https://hacs.xyz/).

## Installation
Update your `configuration.yaml` file with

Add this repository to [HACS](https://hacs.xyz/).
```yaml
auth_oidc:
client_id: ""
discovery_url: ""
```
Update your configuration.yaml file with
Register your client with your OIDC Provider (e.g. Authentik/Authelia) as a public client and get the client_id. Then, use the obtained client_id and discovery URLs to fill the fields in `configuration.yaml`.

For example:
```yaml
auth_oidc:
client_id: "someValueForTheClientId"
discovery_url: "https://example.com/application/o/application/.well-known/openid-configuration"
```

Afterwards, restart Home Assistant.

### Login
You should now be able to see a second option on your login screen ("OpenID Connect (SSO)"). It provides you with a single input field.

Sadly, the user experience is pretty poor right now. Go to `/auth/oidc/welcome` (for example `https://hass.io/auth/oidc/welcome`, replace the URL with your Home Assistant URL) and follow the prompts provided to login, then copy the code into the input field from before. You should now login automatically with your username from SSO.

> [!TIP]
> You can use a different device to login instead. Open the `/auth/oidc/welcome` link on device A and then type the obtained code into the normal HA login on device B (can also be the mobile app) to login.

## Development
This package uses poetry: https://github.com/python-poetry/poetry. Use `poetry install` to install.
You can force the venv within the project with `poetry config virtualenvs.in-project true`.
This project uses the Rye package manager for development. You can find installation instructions here: https://rye.astral.sh/guide/installation/.
Start by installing the dependencies using `rye sync` and then point your editor towards the environment created in the `.venv` directory.

### Help wanted
If you have any tips or would like to contribute, send me a message. You are also welcome to contribute a PR to fix any of the TODOs.

Currently, this is a pre-alpha, so I welcome issues but I cannot guarantee I can fix them (at least within a reasonable time). Please turn on watch for this repository to remain updated. When the component is in a beta stage, issues will likely get fixed more frequently.

### TODOs

- [X] Basic flow
- [ ] Improve welcome screen UI, should render a simple centered Tailwind UI instructing users that you should login externally to obtain a code.
- [ ] Improve finish screen UI, showing the code clearly with a copy button and instructions to paste it into Home Assistant.
- [ ] Implement error handling on top of this proof of concept (discovery, JWKS, OIDC)
- [ ] Make id_token claim used for the group (admin/user) configurable
- [ ] Make id_token claim used for the username configurable
- [ ] Make id_token claim used for the name configurable
- [ ] Add instructions on how to deploy this with Authentik & Authelia
- [ ] Configure Github Actions to automatically lint and build the package
- [ ] Configure Dependabot for automatic updates

Currently impossible TODOs (waiting for assistance from HA devs, not possible without forking HA frontend & apps right now):

- [ ] Update the HA frontend code to allow a redirection to be requested from an auth provider instead of manually opening welcome page
- [ ] Implement this redirection logic to open a new tab on desktop
- [ ] Implement this redirection logic to open a Android Custom Tab (Android) / SFSafariViewController (iOS), instead of opening the link in the HA webview
- [ ] Implement a final redirect back to the main page with the code as a query param instead of showing the finalize page
26 changes: 24 additions & 2 deletions custom_components/auth_oidc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
import voluptuous as vol
from homeassistant.core import HomeAssistant

from .endpoints.welcome import OIDCWelcomeView
from .endpoints.redirect import OIDCRedirectView
from .endpoints.finish import OIDCFinishView
from .endpoints.callback import OIDCCallbackView

from .oidc_client import OIDCClient

DOMAIN = "auth_oidc"
_LOGGER = logging.getLogger(__name__)

Expand All @@ -13,7 +20,9 @@
{
DOMAIN: vol.Schema(
{

vol.Required("client_id"): vol.Coerce(str),
vol.Optional("client_secret"): vol.Coerce(str),
vol.Required("discovery_url"): vol.Url(),
}
)
},
Expand All @@ -34,5 +43,18 @@ async def async_setup(hass: HomeAssistant, config):
providers.update(hass.auth._providers)
hass.auth._providers = providers

_LOGGER.debug("Added OIDC provider")
_LOGGER.debug("Added OIDC provider for Home Assistant")

# Define some fields
discovery_url = config[DOMAIN]["discovery_url"]
client_id = config[DOMAIN]["client_id"]
scope = "openid profile email"

oidc_client = oidc_client = OIDCClient(discovery_url, client_id, scope)

hass.http.register_view(OIDCWelcomeView())
hass.http.register_view(OIDCRedirectView(oidc_client))
hass.http.register_view(OIDCCallbackView(oidc_client, provider))
hass.http.register_view(OIDCFinishView())

return True
41 changes: 0 additions & 41 deletions custom_components/auth_oidc/callback.py

This file was deleted.

49 changes: 49 additions & 0 deletions custom_components/auth_oidc/endpoints/callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
import logging
from ..oidc_client import OIDCClient
from ..provider import OpenIDAuthProvider

PATH = "/auth/oidc/callback"

_LOGGER = logging.getLogger(__name__)

class OIDCCallbackView(HomeAssistantView):
"""OIDC Plugin Callback View."""

requires_auth = False
url = PATH
name = "auth:oidc:callback"

def __init__(
self, oidc_client: OIDCClient, oidc_provider: OpenIDAuthProvider
) -> None:
self.oidc_client = oidc_client
self.oidc_provider = oidc_provider

async def get(self, request: web.Request) -> web.Response:
"""Receive response."""

_LOGGER.debug("Callback view accessed")

params = request.rel_url.query
code = params.get("code")
state = params.get("state")
base_uri = str(request.url).split('/auth', 2)[0]

if not (code and state):
return web.Response(
headers={"content-type": "text/html"},
text="<h1>Error</h1><p>Missing code or state parameter</p>",
)

user_details = await self.oidc_client.complete_token_flow(base_uri, code, state)
if user_details is None:
return web.Response(
headers={"content-type": "text/html"},
text="<h1>Error</h1><p>Failed to get user details, see console.</p>",
)

code = await self.oidc_provider.save_user_info(user_details)

return web.HTTPFound(base_uri + "/auth/oidc/finish?code=" + code)
24 changes: 24 additions & 0 deletions custom_components/auth_oidc/endpoints/finish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
import logging

PATH = "/auth/oidc/finish"

_LOGGER = logging.getLogger(__name__)

class OIDCFinishView(HomeAssistantView):
"""OIDC Plugin Finish View."""

requires_auth = False
url = PATH
name = "auth:oidc:finish"

async def get(self, request: web.Request) -> web.Response:
"""Receive response."""

code = request.query.get("code", "FAIL")

return web.Response(
headers={"content-type": "text/html"},
text=f"<h1>Done!</h1><p>Your code is: <b>{code}</b></p><p>Please return to the Home Assistant login screen (or your mobile app) and fill in this code into the single login field. It should be visible if you select 'Login with OpenID Connect (SSO)'.</p>",
)
46 changes: 46 additions & 0 deletions custom_components/auth_oidc/endpoints/redirect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
import logging

from ..oidc_client import OIDCClient

PATH = "/auth/oidc/redirect"

_LOGGER = logging.getLogger(__name__)

class OIDCRedirectView(HomeAssistantView):
"""OIDC Plugin Redirect View."""

requires_auth = False
url = PATH
name = "auth:oidc:redirect"

def __init__(
self, oidc_client: OIDCClient
) -> None:
self.oidc_client = oidc_client

async def get(self, request: web.Request) -> web.Response:
"""Receive response."""

_LOGGER.debug("Redirect view accessed")

base_uri = str(request.url).split('/auth', 2)[0]
_LOGGER.debug("Base URI: %s", base_uri)

auth_url = await self.oidc_client.get_authorization_url(base_uri)
_LOGGER.debug("Auth URL: %s", auth_url)

if auth_url:
return web.HTTPFound(auth_url)
else:
return web.Response(
headers={"content-type": "text/html"},
text="<h1>Plugin is misconfigured, discovery could not be obtained</h1>",
)

async def post(self, request: web.Request) -> web.Response:
"""POST"""

_LOGGER.debug("Redirect POST view accessed")
return await self.get(request)
24 changes: 24 additions & 0 deletions custom_components/auth_oidc/endpoints/welcome.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
import logging

PATH = "/auth/oidc/welcome"

_LOGGER = logging.getLogger(__name__)

class OIDCWelcomeView(HomeAssistantView):
"""OIDC Plugin Welcome View."""

requires_auth = False
url = PATH
name = "auth:oidc:welcome"

async def get(self, request: web.Request) -> web.Response:
"""Receive response."""

_LOGGER.debug("Welcome view accessed")

return web.Response(
headers={"content-type": "text/html"},
text="<h1>OIDC Login (beta)</h1><p><a href='/auth/oidc/redirect'>Login with OIDC</a></p>",
)
Loading

0 comments on commit 8ba494c

Please sign in to comment.