Skip to content

Instantly share code, notes, and snippets.

@ffenix113
Created February 1, 2023 16:59
Show Gist options
  • Save ffenix113/9f58aee7697a1d0756125ac93b5a27e8 to your computer and use it in GitHub Desktop.
Save ffenix113/9f58aee7697a1d0756125ac93b5a27e8 to your computer and use it in GitHub Desktop.
Home Assistant Keycloak authentication using Direct access grant

Simple auth CLI script to authenticate with Keycloak with username and password.


How to use this:

  1. In direct_auth.py modify configuration, and optionally get_meta_attributes and login_hook functions to your needs.
  2. Place direct_auth.py script into Home assistant. I.e. pass it to Docker container through Volume. Remember where it is located
  3. Merge authenticator configuration from configuration.yml to Home assistant config file and change command to path to direct_auth.py
  4. Make sure that direct_auth.py can be executed(chmod +x direct_auth.py)
  5. Restart Home assistant and choose Command Line Authentication below main log in form.

By default script will only allow authentication of users that have home_assistant realm role.

Aquired token from Keycloak will not be used except in get_meta_attributes and login_hook functions. As such I would suggest to make expiry as quick as possible, just to be safe.


  • If when trying to log in with this script Home assistant writes in logs somethings like /path/to/direct_auth.py cannot be found - please make sure that it can be executed.
homeassistant:
auth_providers:
- type: homeassistant
- type: command_line
meta: true
command: /config/direct_auth.py
#!/usr/bin/env python3
import os
import sys
import requests
###
### Configuration starts here
CLIENT_ID = 'client_id'
CLIENT_SECRET = 'client_secret'
# This should be in form of
# Keycloak <17: https://example.com/auth/realms/<realm_name>
# Keycloak >=17: https://example.com/realms/<realm_name>
REALM_URL = 'https://auth.mega.pp.ua/realms/mega'
### Configuration ends here
###
###
### Some functions to modify behavior
def get_meta_attributes(token: dict) -> dict:
"""
See https://www.home-assistant.io/docs/authentication/providers/#command-line
"""
name = token['name']
if not name:
name = token['preferred_username']
return {
'name': name
}
def login_hook(username: str, body: dict, token: dict) -> bool:
"""
This function will decide if user can access HA or not.
"""
return 'home_assistant' in token['realm_access']['roles']
### End functions to modify behavior
###
# Do not change this
AUTH_URL = REALM_URL + '/protocol/openid-connect/token'
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def decode_token(token: str) -> dict:
import json
from base64 import b64decode
access_token = token.split('.')[1]
# Some nice fix for invalid padding
# https://gist.github.com/perrygeo/ee7c65bb1541ff6ac770
decoded = b64decode(access_token + "===").decode('utf8')
return json.loads(decoded)
def auth(username: str, password: str) -> bool:
payload = {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'username': username,
'password': password,
'grant_type': 'password',
}
resp = requests.post(AUTH_URL, data=payload)
if resp.status_code != 200:
eprint(f'wrong status code: {resp.status_code}, response: {resp.content}')
return False
data = resp.json()
decoded_token = decode_token(data['access_token'])
if not login_hook(username, data, decoded_token):
return False
attributes = get_meta_attributes(decoded_token)
for attribute in attributes:
print(f'{attribute} = {attributes[attribute]}')
return True
if __name__ == '__main__':
username = os.getenv('username')
password = os.getenv('password')
if not auth(username, password):
exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment