Repository: opsdroid/opsdroid

Issue:
Token based authentication for Webhook Matcher
<!-- Before you post an issue or if you are unsure about something join our matrix channel https://riot.im/app/#/room/#opsdroid-general:matrix.org and ask away! We are more than happy to help you. -->
# Description

The webhook matcher allows you to trigger the skill by calling a specific URL endpoint.

Currently, a skill with a webhook matcher would be called if you send a `POST` to `http://localhost:8080/skill/exampleskill/examplewebhook`. 

This URL is public and does not have any form of authentication/authorization checks, which means it can be triggered by anyone. Adding an `Authorization` header with a token-based authentication would secure the webhook.

## Steps to Reproduce
1. Create a skill `exampleskill` with a webhook matcher `examplewebhook`.

2. Send a `POST` request to `http://localhost:8080/skill/exampleskill/examplewebhook` to trigger the bot.

## Expected Functionality
1. The webhook should check for the `Authorization` header and perform a token-based authentication.

2. The bot should be triggered based on the success/failure of the authentication.

## Experienced Functionality
The Bot gets triggered without any authentication/authorization.

## Versions
- **Opsdroid version:** master branch in git
- **Python version:** 3.6.8
- **OS/Docker version:** Ubuntu 18.04


<!-- Love opsdroid? Please consider supporting our collective:
 +👉  https://opencollective.com/opsdroid/donate -->



Discussion:
Yep this would be awesome! What are your thoughts on now we would configure this? Would there be one token for all endpoints, would we configure it in the skill?
My choice would be one token for all endpoints. Configuring in each skill would not be maintainable as the number of skills grows.
Sounds good to me! Maybe it should be in the `web` section of the config.

Resolving patch:
diff --git a/opsdroid/web.py b/opsdroid/web.py
--- a/opsdroid/web.py
+++ b/opsdroid/web.py
@@ -128,6 +128,22 @@ def register_skill(self, opsdroid, skill, webhook):
 
         async def wrapper(req, opsdroid=opsdroid, config=skill.config):
             """Wrap up the aiohttp handler."""
+            webhook_token = self.config.get("webhook-token", None)
+            authorization_header = []
+            if req is not None:
+                authorization_header = req.headers.get("Authorization", "").split()
+
+            if webhook_token is not None:
+                if not (
+                    len(authorization_header) == 2
+                    and authorization_header[0] == "Bearer"
+                    and authorization_header[1] == webhook_token
+                ):
+                    _LOGGER.error(
+                        _("Unauthorized to run skill %s via webhook"), webhook
+                    )
+                    return Web.build_response(403, {"called_skill": webhook})
+
             _LOGGER.info(_("Running skill %s via webhook"), webhook)
             opsdroid.stats["webhooks_called"] = opsdroid.stats["webhooks_called"] + 1
             resp = await opsdroid.run_skill(skill, config, req)


Test patch:
diff --git a/tests/test_matchers.py b/tests/test_matchers.py
--- a/tests/test_matchers.py
+++ b/tests/test_matchers.py
@@ -4,6 +4,8 @@
 import asyncio
 import aiohttp.web
 
+from aiohttp.test_utils import make_mocked_request
+
 from opsdroid.cli.start import configure_lang
 from opsdroid.core import OpsDroid
 from opsdroid.web import Web
@@ -143,6 +145,26 @@ async def test_match_webhook_response(self):
             webhookresponse = await wrapperfunc(None)
             self.assertEqual(type(webhookresponse), aiohttp.web.Response)
 
+    async def test_match_webhook_response_with_authorization_failure(self):
+        with OpsDroid() as opsdroid:
+            opsdroid.loader.current_import_config = {"name": "testhook"}
+            opsdroid.config["web"] = {"webhook-token": "aabbccddeeff"}
+            opsdroid.web_server = Web(opsdroid)
+            opsdroid.web_server.web_app = mock.Mock()
+            webhook = "test"
+            decorator = matchers.match_webhook(webhook)
+            opsdroid.skills.append(decorator(await self.getMockSkill()))
+            opsdroid.skills[0].config = {"name": "mockedskill"}
+            opsdroid.web_server.setup_webhooks(opsdroid.skills)
+            postcalls, _ = opsdroid.web_server.web_app.router.add_post.call_args_list[0]
+            wrapperfunc = postcalls[1]
+            webhookresponse = await wrapperfunc(
+                make_mocked_request(
+                    "POST", postcalls[0], headers={"Authorization": "Bearer wwxxyyzz"}
+                )
+            )
+            self.assertEqual(type(webhookresponse), aiohttp.web.Response)
+
     async def test_match_webhook_custom_response(self):
         with OpsDroid() as opsdroid:
             opsdroid.loader.current_import_config = {"name": "testhook"}