We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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 -->


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `opsdroid/web.py`
Content:
```
1 """Submodule to handle web requests in opsdroid."""
2 
3 import json
4 import logging
5 import ssl
6 
7 from aiohttp import web
8 
9 from opsdroid import __version__
10 
11 
12 _LOGGER = logging.getLogger(__name__)
13 
14 
15 class Web:
16     """Create class for opsdroid Web server."""
17 
18     def __init__(self, opsdroid):
19         """Create web object."""
20         self.opsdroid = opsdroid
21         try:
22             self.config = self.opsdroid.config["web"]
23         except KeyError:
24             self.config = {}
25         self.web_app = web.Application()
26         self.runner = web.AppRunner(self.web_app)
27         self.site = None
28         self.web_app.router.add_get("/", self.web_index_handler)
29         self.web_app.router.add_get("", self.web_index_handler)
30         self.web_app.router.add_get("/stats", self.web_stats_handler)
31         self.web_app.router.add_get("/stats/", self.web_stats_handler)
32 
33     @property
34     def get_port(self):
35         """Return port from config or the default.
36 
37         Args:
38             self: instance method
39 
40         Returns:
41             int: returns value of port being used, config or default
42 
43         """
44         try:
45             port = self.config["port"]
46         except KeyError:
47             if self.get_ssl_context is not None:
48                 port = 8443
49             else:
50                 port = 8080
51         return port
52 
53     @property
54     def get_host(self):
55         """Return host from config or the default.
56 
57         Args:
58             self: instance method
59 
60         Returns:
61             string: returns address of host being used, config or default
62 
63         """
64         try:
65             host = self.config["host"]
66         except KeyError:
67             host = "0.0.0.0"
68         return host
69 
70     @property
71     def get_ssl_context(self):
72         """Return the ssl context or None.
73 
74         Args:
75             self: instance method
76 
77         Returns:
78             string (or NoneType): returns ssl context of None.
79 
80         """
81         try:
82             ssl_config = self.config["ssl"]
83             sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
84             sslcontext.load_cert_chain(ssl_config["cert"], ssl_config["key"])
85             return sslcontext
86         except FileNotFoundError:
87             _LOGGER.error(_("Cannot find ssl cert or key."))
88             return None
89         except KeyError:
90             return None
91 
92     async def start(self):
93         """Start web servers."""
94         _LOGGER.info(
95             _("Started web server on %s://%s%s"),
96             "http" if self.get_ssl_context is None else "https",
97             self.get_host,
98             ":{}".format(self.get_port) if self.get_port not in (80, 443) else "",
99         )
100         await self.runner.setup()
101         self.site = web.TCPSite(
102             self.runner,
103             host=self.get_host,
104             port=self.get_port,
105             ssl_context=self.get_ssl_context,
106         )
107         await self.site.start()
108 
109     async def stop(self):
110         """Stop the web server."""
111         await self.runner.cleanup()
112 
113     @staticmethod
114     def build_response(status, result):
115         """Build a json response object to power the bot reponses.
116 
117         Args:
118             result: serialize obj as a JSON formated stream
119 
120         Returns:
121             json: returns json object with list of responses for the bot
122 
123         """
124         return web.Response(text=json.dumps(result), status=status)
125 
126     def register_skill(self, opsdroid, skill, webhook):
127         """Register a new skill in the web app router."""
128 
129         async def wrapper(req, opsdroid=opsdroid, config=skill.config):
130             """Wrap up the aiohttp handler."""
131             _LOGGER.info(_("Running skill %s via webhook"), webhook)
132             opsdroid.stats["webhooks_called"] = opsdroid.stats["webhooks_called"] + 1
133             resp = await opsdroid.run_skill(skill, config, req)
134             if isinstance(resp, web.Response):
135                 return resp
136             return Web.build_response(200, {"called_skill": webhook})
137 
138         self.web_app.router.add_post(
139             "/skill/{}/{}".format(skill.config["name"], webhook), wrapper
140         )
141         self.web_app.router.add_post(
142             "/skill/{}/{}/".format(skill.config["name"], webhook), wrapper
143         )
144 
145     def setup_webhooks(self, skills):
146         """Add the webhooks for the webhook skills to the router."""
147         for skill in skills:
148             for matcher in skill.matchers:
149                 if "webhook" in matcher:
150                     self.register_skill(self.opsdroid, skill, matcher["webhook"])
151 
152     async def web_index_handler(self, request):
153         """Handle root web request to opsdroid API.
154 
155         Args:
156             request: web request to the root (index)
157 
158         Returns:
159             dict: returns successful status code and greeting for the root page
160 
161         """
162         return self.build_response(200, {"message": "Welcome to the opsdroid API"})
163 
164     async def web_stats_handler(self, request):
165         """Handle stats request.
166 
167         Args:
168             request: web request to render opsdroid stats
169 
170         Returns:
171             dict: returns successful status code and dictionary with
172                   stats requested
173 
174         """
175         stats = self.opsdroid.stats
176         try:
177             stats["average_response_time"] = (
178                 stats["total_response_time"] / stats["total_responses"]
179             )
180         except ZeroDivisionError:
181             stats["average_response_time"] = 0
182 
183         return self.build_response(
184             200,
185             {
186                 "version": __version__,
187                 "messages": {
188                     "total_parsed": stats["messages_parsed"],
189                     "webhooks_called": stats["webhooks_called"],
190                     "total_response_time": stats["total_response_time"],
191                     "total_responses": stats["total_responses"],
192                     "average_response_time": stats["average_response_time"],
193                 },
194                 "modules": {
195                     "skills": len(self.opsdroid.skills),
196                     "connectors": len(self.opsdroid.connectors),
197                     "databases": len(self.opsdroid.memory.databases),
198                 },
199             },
200         )
201
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/opsdroid/web.py b/opsdroid/web.py
--- a/opsdroid/web.py
+++ b/opsdroid/web.py
@@ -128,6 +128,22 @@
 
         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)

{"golden_diff": "diff --git a/opsdroid/web.py b/opsdroid/web.py\n--- a/opsdroid/web.py\n+++ b/opsdroid/web.py\n@@ -128,6 +128,22 @@\n \n         async def wrapper(req, opsdroid=opsdroid, config=skill.config):\n             \"\"\"Wrap up the aiohttp handler.\"\"\"\n+            webhook_token = self.config.get(\"webhook-token\", None)\n+            authorization_header = []\n+            if req is not None:\n+                authorization_header = req.headers.get(\"Authorization\", \"\").split()\n+\n+            if webhook_token is not None:\n+                if not (\n+                    len(authorization_header) == 2\n+                    and authorization_header[0] == \"Bearer\"\n+                    and authorization_header[1] == webhook_token\n+                ):\n+                    _LOGGER.error(\n+                        _(\"Unauthorized to run skill %s via webhook\"), webhook\n+                    )\n+                    return Web.build_response(403, {\"called_skill\": webhook})\n+\n             _LOGGER.info(_(\"Running skill %s via webhook\"), webhook)\n             opsdroid.stats[\"webhooks_called\"] = opsdroid.stats[\"webhooks_called\"] + 1\n             resp = await opsdroid.run_skill(skill, config, req)\n", "issue": "Token based authentication for Webhook Matcher\n<!-- 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. -->\r\n# Description\r\n\r\nThe webhook matcher allows you to trigger the skill by calling a specific URL endpoint.\r\n\r\nCurrently, a skill with a webhook matcher would be called if you send a `POST` to `http://localhost:8080/skill/exampleskill/examplewebhook`. \r\n\r\nThis 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.\r\n\r\n## Steps to Reproduce\r\n1. Create a skill `exampleskill` with a webhook matcher `examplewebhook`.\r\n\r\n2. Send a `POST` request to `http://localhost:8080/skill/exampleskill/examplewebhook` to trigger the bot.\r\n\r\n## Expected Functionality\r\n1. The webhook should check for the `Authorization` header and perform a token-based authentication.\r\n\r\n2. The bot should be triggered based on the success/failure of the authentication.\r\n\r\n## Experienced Functionality\r\nThe Bot gets triggered without any authentication/authorization.\r\n\r\n## Versions\r\n- **Opsdroid version:** master branch in git\r\n- **Python version:** 3.6.8\r\n- **OS/Docker version:** Ubuntu 18.04\r\n\r\n\r\n<!-- Love opsdroid? Please consider supporting our collective:\r\n +\ud83d\udc49  https://opencollective.com/opsdroid/donate -->\r\n\n", "before_files": [{"content": "\"\"\"Submodule to handle web requests in opsdroid.\"\"\"\n\nimport json\nimport logging\nimport ssl\n\nfrom aiohttp import web\n\nfrom opsdroid import __version__\n\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass Web:\n    \"\"\"Create class for opsdroid Web server.\"\"\"\n\n    def __init__(self, opsdroid):\n        \"\"\"Create web object.\"\"\"\n        self.opsdroid = opsdroid\n        try:\n            self.config = self.opsdroid.config[\"web\"]\n        except KeyError:\n            self.config = {}\n        self.web_app = web.Application()\n        self.runner = web.AppRunner(self.web_app)\n        self.site = None\n        self.web_app.router.add_get(\"/\", self.web_index_handler)\n        self.web_app.router.add_get(\"\", self.web_index_handler)\n        self.web_app.router.add_get(\"/stats\", self.web_stats_handler)\n        self.web_app.router.add_get(\"/stats/\", self.web_stats_handler)\n\n    @property\n    def get_port(self):\n        \"\"\"Return port from config or the default.\n\n        Args:\n            self: instance method\n\n        Returns:\n            int: returns value of port being used, config or default\n\n        \"\"\"\n        try:\n            port = self.config[\"port\"]\n        except KeyError:\n            if self.get_ssl_context is not None:\n                port = 8443\n            else:\n                port = 8080\n        return port\n\n    @property\n    def get_host(self):\n        \"\"\"Return host from config or the default.\n\n        Args:\n            self: instance method\n\n        Returns:\n            string: returns address of host being used, config or default\n\n        \"\"\"\n        try:\n            host = self.config[\"host\"]\n        except KeyError:\n            host = \"0.0.0.0\"\n        return host\n\n    @property\n    def get_ssl_context(self):\n        \"\"\"Return the ssl context or None.\n\n        Args:\n            self: instance method\n\n        Returns:\n            string (or NoneType): returns ssl context of None.\n\n        \"\"\"\n        try:\n            ssl_config = self.config[\"ssl\"]\n            sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)\n            sslcontext.load_cert_chain(ssl_config[\"cert\"], ssl_config[\"key\"])\n            return sslcontext\n        except FileNotFoundError:\n            _LOGGER.error(_(\"Cannot find ssl cert or key.\"))\n            return None\n        except KeyError:\n            return None\n\n    async def start(self):\n        \"\"\"Start web servers.\"\"\"\n        _LOGGER.info(\n            _(\"Started web server on %s://%s%s\"),\n            \"http\" if self.get_ssl_context is None else \"https\",\n            self.get_host,\n            \":{}\".format(self.get_port) if self.get_port not in (80, 443) else \"\",\n        )\n        await self.runner.setup()\n        self.site = web.TCPSite(\n            self.runner,\n            host=self.get_host,\n            port=self.get_port,\n            ssl_context=self.get_ssl_context,\n        )\n        await self.site.start()\n\n    async def stop(self):\n        \"\"\"Stop the web server.\"\"\"\n        await self.runner.cleanup()\n\n    @staticmethod\n    def build_response(status, result):\n        \"\"\"Build a json response object to power the bot reponses.\n\n        Args:\n            result: serialize obj as a JSON formated stream\n\n        Returns:\n            json: returns json object with list of responses for the bot\n\n        \"\"\"\n        return web.Response(text=json.dumps(result), status=status)\n\n    def register_skill(self, opsdroid, skill, webhook):\n        \"\"\"Register a new skill in the web app router.\"\"\"\n\n        async def wrapper(req, opsdroid=opsdroid, config=skill.config):\n            \"\"\"Wrap up the aiohttp handler.\"\"\"\n            _LOGGER.info(_(\"Running skill %s via webhook\"), webhook)\n            opsdroid.stats[\"webhooks_called\"] = opsdroid.stats[\"webhooks_called\"] + 1\n            resp = await opsdroid.run_skill(skill, config, req)\n            if isinstance(resp, web.Response):\n                return resp\n            return Web.build_response(200, {\"called_skill\": webhook})\n\n        self.web_app.router.add_post(\n            \"/skill/{}/{}\".format(skill.config[\"name\"], webhook), wrapper\n        )\n        self.web_app.router.add_post(\n            \"/skill/{}/{}/\".format(skill.config[\"name\"], webhook), wrapper\n        )\n\n    def setup_webhooks(self, skills):\n        \"\"\"Add the webhooks for the webhook skills to the router.\"\"\"\n        for skill in skills:\n            for matcher in skill.matchers:\n                if \"webhook\" in matcher:\n                    self.register_skill(self.opsdroid, skill, matcher[\"webhook\"])\n\n    async def web_index_handler(self, request):\n        \"\"\"Handle root web request to opsdroid API.\n\n        Args:\n            request: web request to the root (index)\n\n        Returns:\n            dict: returns successful status code and greeting for the root page\n\n        \"\"\"\n        return self.build_response(200, {\"message\": \"Welcome to the opsdroid API\"})\n\n    async def web_stats_handler(self, request):\n        \"\"\"Handle stats request.\n\n        Args:\n            request: web request to render opsdroid stats\n\n        Returns:\n            dict: returns successful status code and dictionary with\n                  stats requested\n\n        \"\"\"\n        stats = self.opsdroid.stats\n        try:\n            stats[\"average_response_time\"] = (\n                stats[\"total_response_time\"] / stats[\"total_responses\"]\n            )\n        except ZeroDivisionError:\n            stats[\"average_response_time\"] = 0\n\n        return self.build_response(\n            200,\n            {\n                \"version\": __version__,\n                \"messages\": {\n                    \"total_parsed\": stats[\"messages_parsed\"],\n                    \"webhooks_called\": stats[\"webhooks_called\"],\n                    \"total_response_time\": stats[\"total_response_time\"],\n                    \"total_responses\": stats[\"total_responses\"],\n                    \"average_response_time\": stats[\"average_response_time\"],\n                },\n                \"modules\": {\n                    \"skills\": len(self.opsdroid.skills),\n                    \"connectors\": len(self.opsdroid.connectors),\n                    \"databases\": len(self.opsdroid.memory.databases),\n                },\n            },\n        )\n", "path": "opsdroid/web.py"}], "after_files": [{"content": "\"\"\"Submodule to handle web requests in opsdroid.\"\"\"\n\nimport json\nimport logging\nimport ssl\n\nfrom aiohttp import web\n\nfrom opsdroid import __version__\n\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass Web:\n    \"\"\"Create class for opsdroid Web server.\"\"\"\n\n    def __init__(self, opsdroid):\n        \"\"\"Create web object.\"\"\"\n        self.opsdroid = opsdroid\n        try:\n            self.config = self.opsdroid.config[\"web\"]\n        except KeyError:\n            self.config = {}\n        self.web_app = web.Application()\n        self.runner = web.AppRunner(self.web_app)\n        self.site = None\n        self.web_app.router.add_get(\"/\", self.web_index_handler)\n        self.web_app.router.add_get(\"\", self.web_index_handler)\n        self.web_app.router.add_get(\"/stats\", self.web_stats_handler)\n        self.web_app.router.add_get(\"/stats/\", self.web_stats_handler)\n\n    @property\n    def get_port(self):\n        \"\"\"Return port from config or the default.\n\n        Args:\n            self: instance method\n\n        Returns:\n            int: returns value of port being used, config or default\n\n        \"\"\"\n        try:\n            port = self.config[\"port\"]\n        except KeyError:\n            if self.get_ssl_context is not None:\n                port = 8443\n            else:\n                port = 8080\n        return port\n\n    @property\n    def get_host(self):\n        \"\"\"Return host from config or the default.\n\n        Args:\n            self: instance method\n\n        Returns:\n            string: returns address of host being used, config or default\n\n        \"\"\"\n        try:\n            host = self.config[\"host\"]\n        except KeyError:\n            host = \"0.0.0.0\"\n        return host\n\n    @property\n    def get_ssl_context(self):\n        \"\"\"Return the ssl context or None.\n\n        Args:\n            self: instance method\n\n        Returns:\n            string (or NoneType): returns ssl context of None.\n\n        \"\"\"\n        try:\n            ssl_config = self.config[\"ssl\"]\n            sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)\n            sslcontext.load_cert_chain(ssl_config[\"cert\"], ssl_config[\"key\"])\n            return sslcontext\n        except FileNotFoundError:\n            _LOGGER.error(_(\"Cannot find ssl cert or key.\"))\n            return None\n        except KeyError:\n            return None\n\n    async def start(self):\n        \"\"\"Start web servers.\"\"\"\n        _LOGGER.info(\n            _(\"Started web server on %s://%s%s\"),\n            \"http\" if self.get_ssl_context is None else \"https\",\n            self.get_host,\n            \":{}\".format(self.get_port) if self.get_port not in (80, 443) else \"\",\n        )\n        await self.runner.setup()\n        self.site = web.TCPSite(\n            self.runner,\n            host=self.get_host,\n            port=self.get_port,\n            ssl_context=self.get_ssl_context,\n        )\n        await self.site.start()\n\n    async def stop(self):\n        \"\"\"Stop the web server.\"\"\"\n        await self.runner.cleanup()\n\n    @staticmethod\n    def build_response(status, result):\n        \"\"\"Build a json response object to power the bot reponses.\n\n        Args:\n            result: serialize obj as a JSON formated stream\n\n        Returns:\n            json: returns json object with list of responses for the bot\n\n        \"\"\"\n        return web.Response(text=json.dumps(result), status=status)\n\n    def register_skill(self, opsdroid, skill, webhook):\n        \"\"\"Register a new skill in the web app router.\"\"\"\n\n        async def wrapper(req, opsdroid=opsdroid, config=skill.config):\n            \"\"\"Wrap up the aiohttp handler.\"\"\"\n            webhook_token = self.config.get(\"webhook-token\", None)\n            authorization_header = []\n            if req is not None:\n                authorization_header = req.headers.get(\"Authorization\", \"\").split()\n\n            if webhook_token is not None:\n                if not (\n                    len(authorization_header) == 2\n                    and authorization_header[0] == \"Bearer\"\n                    and authorization_header[1] == webhook_token\n                ):\n                    _LOGGER.error(\n                        _(\"Unauthorized to run skill %s via webhook\"), webhook\n                    )\n                    return Web.build_response(403, {\"called_skill\": webhook})\n\n            _LOGGER.info(_(\"Running skill %s via webhook\"), webhook)\n            opsdroid.stats[\"webhooks_called\"] = opsdroid.stats[\"webhooks_called\"] + 1\n            resp = await opsdroid.run_skill(skill, config, req)\n            if isinstance(resp, web.Response):\n                return resp\n            return Web.build_response(200, {\"called_skill\": webhook})\n\n        self.web_app.router.add_post(\n            \"/skill/{}/{}\".format(skill.config[\"name\"], webhook), wrapper\n        )\n        self.web_app.router.add_post(\n            \"/skill/{}/{}/\".format(skill.config[\"name\"], webhook), wrapper\n        )\n\n    def setup_webhooks(self, skills):\n        \"\"\"Add the webhooks for the webhook skills to the router.\"\"\"\n        for skill in skills:\n            for matcher in skill.matchers:\n                if \"webhook\" in matcher:\n                    self.register_skill(self.opsdroid, skill, matcher[\"webhook\"])\n\n    async def web_index_handler(self, request):\n        \"\"\"Handle root web request to opsdroid API.\n\n        Args:\n            request: web request to the root (index)\n\n        Returns:\n            dict: returns successful status code and greeting for the root page\n\n        \"\"\"\n        return self.build_response(200, {\"message\": \"Welcome to the opsdroid API\"})\n\n    async def web_stats_handler(self, request):\n        \"\"\"Handle stats request.\n\n        Args:\n            request: web request to render opsdroid stats\n\n        Returns:\n            dict: returns successful status code and dictionary with\n                  stats requested\n\n        \"\"\"\n        stats = self.opsdroid.stats\n        try:\n            stats[\"average_response_time\"] = (\n                stats[\"total_response_time\"] / stats[\"total_responses\"]\n            )\n        except ZeroDivisionError:\n            stats[\"average_response_time\"] = 0\n\n        return self.build_response(\n            200,\n            {\n                \"version\": __version__,\n                \"messages\": {\n                    \"total_parsed\": stats[\"messages_parsed\"],\n                    \"webhooks_called\": stats[\"webhooks_called\"],\n                    \"total_response_time\": stats[\"total_response_time\"],\n                    \"total_responses\": stats[\"total_responses\"],\n                    \"average_response_time\": stats[\"average_response_time\"],\n                },\n                \"modules\": {\n                    \"skills\": len(self.opsdroid.skills),\n                    \"connectors\": len(self.opsdroid.connectors),\n                    \"databases\": len(self.opsdroid.memory.databases),\n                },\n            },\n        )\n", "path": "opsdroid/web.py"}]}