Allow Alexa Skill to access home assistant only by using a client certificate

Hey,

i built my own Alexa skill and would like to allow access to homeassistant by using client certificates.

I use cloudflare to make my homeassistant docker container public accessible.
Cloudflare has a built in function to limit access to client which has the right client certificate.

My actual issue is to modify the lambda.py code to send the client.pem und key.pem content with the request.

Here is my actual lambda code:

# -*- coding: utf-8 -*-
import os
import json
import logging
import urllib3
from urllib3.connectionpool import HTTPSConnectionPool

_debug = bool(os.environ.get('DEBUG'))

_logger = logging.getLogger('HomeAssistant-Intents')
_logger.setLevel(logging.DEBUG if _debug else logging.INFO)


def lambda_handler(event, context):
    """Handle incoming Alexa directive."""

    _logger.debug('Event: %s', event)

    base_url = "https://homeassistant.myurl.com"
    assert base_url is not None, 'Please set BASE_URL environment variable'

    try:
        token = event.get('session', {}).get('user', {}).get('accessToken')
    except AttributeError:
        token = None

    if token is None and _debug:
        token = os.environ.get('LONG_LIVED_ACCESS_TOKEN')

    assert token, 'Could not get access token'

    verify_ssl = not bool(os.environ.get('NOT_VERIFY_SSL'))

    http = HTTPSConnectionPool(base_url, cert_file='client.pem', key_file='key.pem', cert_reqs='CERT_REQUIRED')
    #http = urllib3.PoolManager(
    #    cert_reqs='CERT_REQUIRED',
    #    timeout=urllib3.Timeout(connect=2.0, read=10.0),
    #    key_file='./key.pem',
    #    cert_file='./client.pem'
    #)

    response = http.request(
        'POST',
        '{}/api/alexa'.format(base_url),
        headers={
            'Authorization': 'Bearer {}'.format(token),
            'Content-Type': 'application/json',
        },
        body=json.dumps(event).encode('utf-8'),
    )
    if response.status >= 400:
        return {
            'event': {
                'payload': {
                    'type': 'INVALID_AUTHORIZATION_CREDENTIAL'
                    if response.status in (401, 403) else 'INTERNAL_ERROR',
                    'message': response.data.decode("utf-8"),
                }
            }
        }
    return json.loads(response.data.decode('utf-8'))

Has someone a idea how i can fix it?

My log shows that the Alexa skill tries always to come without the cert.

Greetings

Dany

In your code, the certificate part is commented out, did you notice?

I extended the script myself and added the certificate side by side to the script. So this script is working as expected:

import os
import json
import logging
import urllib3

_debug = bool(os.environ.get('DEBUG'))

_logger = logging.getLogger('HomeAssistant-SmartHome')
_logger.setLevel(logging.DEBUG if _debug else logging.INFO)


def lambda_handler(event, context):
    """Handle incoming Alexa directive."""
    
    _logger.debug('Event: %s', event)

    base_url = os.environ.get('BASE_URL')
    assert base_url is not None, 'Please set BASE_URL environment variable'

    directive = event.get('directive')
    assert directive is not None, 'Malformatted request - missing directive'
    assert directive.get('header', {}).get('payloadVersion') == '3', \
        'Only support payloadVersion == 3'
    
    scope = directive.get('endpoint', {}).get('scope')
    if scope is None:
        # token is in grantee for Linking directive 
        scope = directive.get('payload', {}).get('grantee')
    if scope is None:
        # token is in payload for Discovery directive 
        scope = directive.get('payload', {}).get('scope')
    assert scope is not None, 'Malformatted request - missing endpoint.scope'
    assert scope.get('type') == 'BearerToken', 'Only support BearerToken'

    token = scope.get('token')
    if token is None and _debug:
        token = os.environ.get('LONG_LIVED_ACCESS_TOKEN')  # only for debug purpose
    
    verify_ssl = not bool(os.environ.get('NOT_VERIFY_SSL'))
    
    http = urllib3.PoolManager(
        cert_file="./client.crt",
        cert_reqs='CERT_REQUIRED' if verify_ssl else 'CERT_NONE',
        timeout=urllib3.Timeout(connect=2.0, read=10.0)
    )
    
    response = http.request(
        'POST', 
        '{}/api/alexa/smart_home'.format(base_url),
        headers={
            'Authorization': 'Bearer {}'.format(token),
            'Content-Type': 'application/json',
        },
        body=json.dumps(event).encode('utf-8'),
    )
    resp_data=response.data.decode('utf-8')
    _logger.debug(f"response: {resp_data}")
    hass = json.loads(resp_data)
    if response.status >= 400:
        return {
            'event': {
                'payload': {
                    'type': 'INVALID_AUTHORIZATION_CREDENTIAL' 
                            if response.status in (401, 403) else 'INTERNAL_ERROR',
                    'message': resp_data,
                }
            }
        }

    return hass

client.crt content

-----BEGIN RSA PRIVATE KEY-----
[...]
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----

The /auth/token-URI specified in the Skill needs to be accessible without a client cert, otherweise account linking will not work.