VC/Zoom: Add webhook validation

This commit is contained in:
Adrian Moennich 2023-09-28 12:47:49 +02:00
parent d948366ccc
commit b994a0be5c
4 changed files with 47 additions and 10 deletions

View File

@ -11,6 +11,10 @@
## Changelog
### 3.2.5
- Handle webhook validation request and use zoom webhook secret token for request verification
### 3.2.4
- Adapt to Indico 3.2.6 changes
@ -109,7 +113,9 @@
**URL:** `https://yourserver/api/plugin/zoom/webhook`
(write down the "Verification Token", as you will need it in the plugin configuration below)
Copy the "Secret Token", as you will need it in the plugin configuration below. Note that in order
to actually create the webhook you need to validate the URL, which requires the token to saved in
the Indico plugin configuration.
Select the following "Event types":
* `Meeting has been updated`
@ -124,7 +130,7 @@ These are the most relevant configuration options:
* **Notification email addresses** - Additional e-mails which will receive notifications
* **E-mail domains** - List of e-mail domains which can be used for the Zoom API (e.g. `cern.ch`)
* **Webhook token** (optional) - the token which Zoom requests will authenticate with (get it from Zoom Marketplace)
* **Webhook Secret Token** (optional) - the token which Zoom requests will authenticate with (get it from Zoom Marketplace)
### Zoom Server-to-Server OAuth

View File

@ -5,13 +5,16 @@
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
import hashlib
import hmac
from flask import flash, jsonify, request, session
from flask_pluginengine import current_plugin
from marshmallow import EXCLUDE
from sqlalchemy.orm.attributes import flag_modified
from webargs import fields
from webargs.flaskparser import use_kwargs
from werkzeug.exceptions import Forbidden
from werkzeug.exceptions import Forbidden, ServiceUnavailable
from indico.core.db import db
from indico.core.errors import UserValueError
@ -48,17 +51,44 @@ class RHRoomAlternativeHost(RHVCSystemEventBase):
class RHWebhook(RH):
CSRF_ENABLED = False
def _is_validation_event(self, event):
return event == 'endpoint.url_validation'
def _get_hmac(self, data):
webhook_token = current_plugin.settings.get('webhook_token')
if not webhook_token:
current_plugin.logger.warning('Tried to validate Zoom webhook, but no secret token has been configured')
raise ServiceUnavailable('No Zoom Webhook Secret Token configured')
return hmac.new(webhook_token.encode(), data, hashlib.sha256).hexdigest()
def _check_access(self):
token = request.headers.get('Authorization')
expected_token = current_plugin.settings.get('webhook_token')
if not expected_token or not token or token != expected_token:
raise Forbidden
timestamp = request.headers.get('x-zm-request-timestamp')
zoom_signature_header = request.headers.get('x-zm-signature')
signature = self._get_hmac(b'v0:%s:%s' % (timestamp.encode(), request.data))
expected_header = f'v0={signature}'
if zoom_signature_header != expected_header:
current_plugin.logger.warning('Received request with invalid signature: Expected %s, got %s, payload %s',
expected_header, zoom_signature_header, request.data.decode())
raise Forbidden('Zoom signature verification failed')
@use_kwargs({
'event': fields.String(required=True),
'payload': fields.Dict(required=True)
}, unknown=EXCLUDE)
def _process(self, event, payload):
if self._is_validation_event(event):
return self._handle_validation(payload)
return self._handle_zoom_event(event, payload)
def _handle_validation(self, payload):
plain_token = payload['plainToken']
signed_token = self._get_hmac(plain_token.encode())
return jsonify({
'plainToken': plain_token,
'encryptedToken': signed_token
})
def _handle_zoom_event(self, event, payload):
meeting_id = payload['object']['id']
vc_room = VCRoom.query.filter(VCRoom.data.contains({'zoom_id': meeting_id})).first()

View File

@ -58,8 +58,9 @@ class PluginSettingsForm(VCPluginSettingsFormBase):
client_id = StringField(_('Client ID'), [])
client_secret = IndicoPasswordField(_('Client Secret'), [], toggle=True)
webhook_token = IndicoPasswordField(_('Webhook Token'), toggle=True,
description=_("Specify Zoom's webhook token if you want live updates"))
webhook_token = IndicoPasswordField(_('Webhook Secret Token'), toggle=True,
description=_("Specify the \"Secret Token\" of your Zoom Webhook if you want "
"live updates in case of modified/deleted Zoom meetings."))
user_lookup_mode = IndicoEnumSelectField(_('User lookup mode'), [DataRequired()], enum=UserLookupMode,
description=_('Specify how Indico should look up the zoom user that '

View File

@ -1,6 +1,6 @@
[metadata]
name = indico-plugin-vc-zoom
version = 3.2.4
version = 3.2.5
description = Zoom video-conferencing plugin for Indico
long_description = file: README.md
long_description_content_type = text/markdown; charset=UTF-8; variant=GFM