Реализация аутентификации в telegram-боте через OAuth2 на примере Box API
Возникла идея простого бота, который будет отправлять переданные ему файлы в облако. Однако для аутентификации сейчас практически везде используется OAuth2. Если в web-приложениях пользоваться OAuth2 все уже научились, то с telegram у меня возникли вопросы.
В этой заметке будет минимальный пример telegram-бота, который работает с box.com через API с аутентификацией через OAuth2.
disclaimer #1: В статье не будет подробного описания работы OAuth2 — только необходимый минимум информации для понимания процесса.
disclaimer #2: Для примера использую облачное хранилище box.com, так как именно в нём лежит моя библиотека электронных книг. При реализации аналогичной функциональности для других облаков проблем возникнуть не должно.
Какой сценарий с точки зрения пользователя получим в конце текущей статьи:
- Пользователь добавляет бота в telegram
- По команде
/login
пользователь получает ссылку на аутентификацию - Пользователь переходит по ссылке в браузере на box.com
- Пользователь аутентифицируется на box.com
- Открывается страница бота
https://t.me/{BOT_NAME}?start={code}
с предложением отправить ему сообщение по кнопке Send message - Пользователь нажимает на кнопку Send message
- В telegram открывается диалог с ботом с активной кнопкой Start
- Пользователь нажимает на кнопку Start
- Бот отправляет пользователю его login в box.com
Добавить остальную функциональность — дело техники.
Необходимый минимум об OAuth2
Для реализация аутентификации через OAuth2 необходимо выполнить несколько пунктов:
- Получить
client_id
иclient_secret
в консоли разработчика box.com. Подробно процесс описан в приложении в конце статьи - Сформировать ссылку для аутентификации пользователя
https://account.box.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_url}&response_type=code
.client_id
иredirect_url
необходимо скопировать из настроек созданного ранее приложения в консоли box.com - На адресе
redirect_url
(по-умолчанию в этой статье — этоlocalhost:8000
) поднять веб-сервер, который сможет получить код для аутентификации в box.com - С полученным в п.3 кодом и параметрами
client_id
иclient_secret
из п.1 обратиться к endpoint box.com API для полученияaccess_token
иrefresh_token
.access_token
в дальнейшем необходимо добавлять во все запросы к API.
Реализация
Шаг 1. Подготовка
Для работы понадобятся три внешних библиотеки:
python-telegram-bot
— для работы с Telegram Bot APIrequests
— для выполнения запросов к APIboxsdk
— для работы с API box.com
Библиотеки и их версии прописаны в requirements.txt. Установить всё через pip install -r requirements.txt
— ничего необычного. Но лучше для этого создать отдельное виртуальное окружение.
Шаг 2. Продолжаем подготовку
Теперь надо получить токен для бота. Я уже писал об этом ранее.
Кратко повторю. Получить токен для нового бота возможно через служебный аккаунт @BotFatherBot. Для этого отправить ему команду /newbot
и пройти небольшой визард. В процессе надо сохранить токен, который потребуется при создании бота, и имя бота.
Шаг 3. Веб-сервис для получения OAuth2-кода
Нужен простой веб-сервис. На URI этого веб-сервиса будет происходить перенаправление со страницы аутентификации box.com. Адрес перенаправления будет содержать параметр code
в строке запроса. Этот код используется для получения access_token
и refresh_token
дальше.
Веб-сервис будет без использования внешних зависимостей. Реализован на встроенном simple http server. Код позаимоствован из gist, но немного модифицирован:
#!/usr/bin/env python3
"""
Very simple HTTP server in python for logging requests
Usage::
./server.py [<port>]
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import logging
# Имя созданного ранее в @BotFather бота
bot_name = 'BOT_NAME'
class S(BaseHTTPRequestHandler):
def do_GET(self):
logging.info("GET request,\nPath: %s\nHeaders:\n%s\n", str(self.path), str(self.headers))
# Необходимо перенаправить запрос на адрес твоего бота.
# Полученный из box.com API код передаётся созданному боту через механизм deep linking (https://core.telegram.org/bots#deep-linking).
self.send_response(301)
self.send_header('Location',f'https://t.me/{bot_name}?start={self.path.split("=")[-1]}')
self.end_headers()
def run(server_class=HTTPServer, handler_class=S, port=8000):
logging.basicConfig(level=logging.INFO)
server_address = ('', port)
httpd = server_class(server_address, handler_class)
logging.info('Starting httpd...\n')
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
logging.info('Stopping httpd...\n')
if __name__ == '__main__':
from sys import argv
if len(argv) == 2:
run(port=int(argv[1]))
else:
run()
Использовать код выше в настоящем боевом production я бы, конечно, не стал, но для демонстрации идеи подойдёт.
Шаг 4. Минимально работающий бот
Простейший бот с использованием библиотеки python-telegram-bot
выглядит так:
from telegram.ext import Updater
from telegram.update import Update
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext import CommandHandler
token = 'TG_BOT_TOKEN'
updater = Updater(token=token, use_context=True)
dispatcher = updater.dispatcher
def start(update: Update, context: CallbackContext):
context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")
start_handler = CommandHandler('start', start)
dispatcher.add_handler(start_handler)
updater.start_polling()
где TG_BOT_TOKEN
— токен, который выдал @BotFather.
Этот бот не делает ничего полезного — только отправляет сообщение "I'm a bot, please talk to me!"
по команде /start
.
Теперь надо добавить самое важное — аутентификацию через OAuth2.
Шаг 4.1. OAuth2-аутентификация
Для аутентификации необходимо перейти по сформированной особым образом ссылке: https://account.box.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_url}&response_type=code
, где client_id
и redirect_url
параметры из настроек созданного ранее приложения в консоли разработчика box.com.
На этой странице будет стандартная форма логина в box.com .
После ввода логина и пароля произойдёт перенаправление на redirect_url
. На этом адресе сервис, получив GET-запрос, вычленит из строки запроса code
. После чего перенаправит запрос на страницу старта работы с telegram-ботом. В адрес будет добавлен полученный код. Далее должен открыться диалог с ботом с активной кнопкой Start
. По клику на кнопку в обработчике команды /start
можно будет получить переданный код.
Простейший код как proof of concept:
from telegram.ext import Updater
from telegram.update import Update
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext import CommandHandler
import requests
import json
import boxsdk
client_secret: str = 'CLIENT_SECRET'
client_id: str = 'CLIENT_ID'
redirect_url: str = 'REDIRECT_URL'
token = 'TG_BOT_TOKEN'
updater = Updater(token=token, use_context=True)
dispatcher = updater.dispatcher
# Обработчик команды /start
def start(update: Update, context: CallbackContext):
message_text: str = update.message.text
# В deep linking параметры из url передаются в сообщении в виде строки "/start code",
# где code - строка из url https://t.me/{BOT_NAME}?start=code.
message_parts: list[str] = message_text.split(' ')
if len(message_parts) == 2:
# Во втором элементе будет содержаться переданный код,
# который необходим для получения access_token и refresh_token
code: str = message_parts[1]
# Формируем запрос к API box.com для получения access_token и refresh_token
access_token_url = 'https://api.box.com/oauth2/token'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
params = {
'client_id': client_id,
'client_secret': client_secret,
'code': code,
'grant_type': 'authorization_code'
}
req = requests.post(access_token_url, data=params, headers=headers)
data = json.loads(req.text)
# Полученный JSON содержит необходимые данные.
# Используя их можно инициализировать клиент boxsdk
# и начать обращаться к его API.
oauth: boxsdk.OAuth2 = boxsdk.OAuth2(
client_id,
client_secret,
access_token=data['access_token'],
refresh_token=data['refresh_token'])
box_client = boxsdk.Client(oauth)
# Например, можно получить информацию об аутентифицированном пользователе.
user = box_client.user().get()
context.bot.send_message(chat_id=update.effective_chat.id, text=user.login)
else:
show_welcome_message(update, context)
# Обработчик команды /login
def login(update: Update, context: CallbackContext):
context.bot.send_message(chat_id=update.effective_chat.id, text=f'<a href="https://account.box.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_url}&response_type=code">Connect to Box.com</a>', parse_mode='HTML')
def show_welcome_message(update: Update, context: CallbackContext):
context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")
start_handler = CommandHandler('start', start)
login_handler = CommandHandler('login', login)
dispatcher.add_handler(start_handler)
dispatcher.add_handler(login_handler)
updater.start_polling()
Что дальше
- Привести код бота в порядок: выделить классы, методы,
- Сделать конфигурирование бота более удобным,
- Реализовать загрузку файлов в облако.
О чём-то из этого постараюсь написать позже.
Исходники для этой заметки можно найти на github.
Приложение
Получить client_id
и client_secret
необходимо в консоли разработчика box.com:
- Cоздать новое приложение с типом Custom app
- В настройках приложения выбрать User Authentication (OAuth2) и указывать App Name
- В разделе Configuration созданного приложения:
- в секции OAuth 2.0 Credentials забрать
client_id
иclient_secret
, - в секции OAuth 2.0 Redirect URI указать
localhost:8000
(или другой адрес, если есть понимание, что это и зачем) - поставить галочку в чекбоксе Write all files and folders stored in Box (в этой статье про это говорить не буду)
- в секции OAuth 2.0 Credentials забрать