Генерация ссылок Zoom через Server-to-Server OAuth на Python

Зачем переходить на Server-to-Server OAuth
Zoom объявил об устаревании JWT-приложений. Если ваше приложение зависит от JWT для доступа к API Zoom, вам нужно переключиться до отключения сервиса. Есть два основных варианта:
- OAuth (user-level): требует, чтобы пользователи авторизовали ваше приложение через их Zoom-аккаунты. Подходит, если вы действуете от имени конкретного пользователя.
- Server-to-Server OAuth (account-level): даёт приложению доступ к API от имени аккаунта Zoom без вмешательства пользователя. Подходит для автоматической генерации ссылок, планировщика встреч и интеграций на серверной стороне.
Важно: S2S — лучший выбор, когда вы создаёте ссылки на встречи централизованно и не требуете явного согласия каждого пользователя.
Создание Server-to-Server OAuth приложения в Zoom Marketplace
- Перейдите в Zoom Marketplace и нажмите создать новое приложение.
- Пролистайте до баннера Server-to-Server OAuth и нажмите Создать.
- Введите название приложения и подтвердите создание. Вы попадёте на панель управления приложением.
- Скопируйте все учётные данные: Account ID, Client ID и Client Secret. Сохраните их в безопасном месте (например, в переменных окружения или менеджере секретов).
- Заполните «Основную информацию» (Basic information) и продолжите.
- На странице функций нажмите Продолжить и затем Add Scopes. Для создания встреч добавьте минимально необходимые области (scopes): «Get a meeting’s encoded SIP URI» и «View and manage all user meetings» (эквивалент прав на просмотр и управление встречами).
- Нажмите Continue, затем Activate your app. После активации приложение сможет запрашивать токены для аккаунта.
Important: храните Client Secret и Account ID в защищённом хранилище. Не публикуйте их в репозиториях.
Python: реализация создания встречи через S2S OAuth
Ниже — более аккуратная и надёжная версия функции для запроса токена и создания встречи. Скрипт использует requests и стандартный модуль datetime.
import os
import requests
from datetime import datetime, timezone
# Получите эти значения из защищённых переменных окружения
CLIENT_ID = os.environ.get("ZOOM_CLIENT_ID")
CLIENT_SECRET = os.environ.get("ZOOM_CLIENT_SECRET")
ACCOUNT_ID = os.environ.get("ZOOM_ACCOUNT_ID")
AUTH_TOKEN_URL = "https://zoom.us/oauth/token"
API_BASE_URL = "https://api.zoom.us/v2"
class ZoomError(Exception):
pass
def get_s2s_access_token(client_id: str, client_secret: str, account_id: str) -> str:
"""Получить токен доступа для Server-to-Server OAuth."""
data = {
"grant_type": "account_credentials",
"account_id": account_id
}
# Basic auth с client_id:client_secret
resp = requests.post(AUTH_TOKEN_URL, auth=(client_id, client_secret), data=data, timeout=10)
if resp.status_code != 200:
raise ZoomError(f"Не удалось получить токен: {resp.status_code} {resp.text}")
token_data = resp.json()
return token_data.get("access_token")
def create_meeting(topic: str, duration_minutes: int, start_dt: datetime) -> dict:
"""Создать встречу и вернуть ответ API как dict.
start_dt должен быть timezone-aware в UTC или с указанным часовым поясом.
"""
if start_dt.tzinfo is None:
# Предполагаем, что переданное время локальное — переводим в UTC
start_dt = start_dt.replace(tzinfo=timezone.utc)
access_token = get_s2s_access_token(CLIENT_ID, CLIENT_SECRET, ACCOUNT_ID)
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
payload = {
"topic": topic,
"type": 2, # Запланированная встреча
"start_time": start_dt.astimezone(timezone.utc).isoformat(),
"duration": int(duration_minutes)
}
resp = requests.post(f"{API_BASE_URL}/users/me/meetings", headers=headers, json=payload, timeout=10)
if resp.status_code != 201:
raise ZoomError(f"Не удалось создать встречу: {resp.status_code} {resp.text}")
return resp.json()Примеры использования:
from datetime import datetime, timezone, timedelta
# Спланируем встречу на 23 августа 2023, 18:24 (UTC+3)
local_ts = datetime(2023, 8, 23, 18, 24)
# Явно задаём часовой пояс, если нужно: UTC+3
# local_ts = local_ts.replace(tzinfo=timezone(timedelta(hours=3)))
# В демонстрации используем UTC
local_ts = local_ts.replace(tzinfo=timezone.utc)
try:
result = create_meeting("Test Zoom Meeting", 60, local_ts)
print("Ссылка:", result.get("join_url"))
print("Пароль:", result.get("password"))
except Exception as e:
print("Ошибка:", e)Notes: Zoom ожидает start_time в ISO 8601. В примере мы приводим время к UTC и используем .isoformat(). Это снижает ошибки, связанные с часовыми поясами.
Что возвращает API
API вернёт JSON с множеством полей: join_url, start_time, duration, password, id и т. п. Выберите только те поля, которые вам нужны. Пример полезных полей:
- join_url — публичная ссылка для присоединения
- password — пароль к встрече (если есть)
- start_time — время старта в ISO 8601
- id — идентификатор встречи
Интеграция в приложение
Вы можете поместить функцию create_meeting в слой сервиса вашего приложения (Django/Flask/FastAPI). Если создаёте API-эндпоинт, обеспечьте авторизацию клиентов, логирование и лимитирование запросов.
Пример маршрута (FastAPI):
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.post('/meetings')
async def meetings_create(payload: dict):
try:
meeting = create_meeting(payload['topic'], payload['duration'], payload['start_dt'])
return meeting
except ZoomError as e:
raise HTTPException(status_code=500, detail=str(e))Когда этот подход НЕ подходит
- Вам нужно действовать от имени конкретного пользователя и запрашивать его доступ — используйте OAuth с авторизацией пользователя.
- Необходимо управлять личными настройками пользователя, доступ к которым требует явного согласия.
- Если вы раздаёте права сторонним приложениям, S2S даёт доступ на уровне аккаунта — оцените риски.
Альтернативные подходы
- Полноценный OAuth (консенсус пользователя) — когда нужна явная авторизация пользователя.
- Прямые интеграции через Zoom SDK для клиентских приложений — если нужно полностью встраивать UI/AV функциональность.
Чек-лист ролей
Для ускоренной проверки развертывания:
Админ Zoom:
- Создал S2S-приложение и активировал его.
- Ограничил scopes до необходимых.
- Сохранил Account ID, Client ID и Client Secret в безопасном хранилище.
Разработчик:
- Внёс секреты в переменные окружения или секрет-менеджер.
- Реализовал обработку ошибок и таймауты.
- Привёл start_time к ISO 8601 (UTC) и проверил часовые пояса.
DevOps / SRE:
- Настроил сеть/файрволы и HTTPS.
- Настроил ротацию секретов и мониторинг ошибок.
Критерии приёмки
- Эндпоинт создаёт встречу и возвращает 201/успешный JSON с join_url.
- Секреты не хранятся в коде и доступны только через защищённое хранилище.
- Логи не содержат Client Secret или полных токенов доступа.
Безопасность и конфиденциальность
- Храните Client Secret и Account ID в менеджере секретов (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager).
- Ограничьте права S2S приложения только необходимыми scopes.
- Логи: маскируйте чувствительные поля и храните только минимальные сведения.
- Ротация ключей: периодически обновляйте Client Secret.
- Для соответствия локальным требованиям безопасности (например, GDPR) минимизируйте передачу персональных данных и описывайте назначение хранения данных в политике конфиденциальности.
Уязвимости и способы их уменьшения
- Утечка секретов — используйте менеджер секретов и IAM-права.
- Компрометация токенов — сократите время жизни сессий и контролируйте доступ.
- Ошибки часовых поясов — всегда храните и передавайте время в ISO 8601 (UTC).
Краткое резюме
Server-to-Server OAuth — правильный выбор, если вы хотите централизованно создавать встречи Zoom без участия пользователей. Создайте S2S-приложение в Zoom Marketplace, сохраните учётные данные в безопасном месте и используйте приведённую функцию на Python для получения токена и создания встречи. Обязательно реализуйте обработку ошибок, логирование без секретов и корректную работу с часовыми поясами.
Important: перед развёртыванием в проде протестируйте создание встреч в тестовом аккаунте и проверьте ограничения вашего тарифного плана Zoom.