Сообщить об ошибке.

Авторизация на сайте через Telegram Passport

Настройка Тelegram Passport в python-telegram-bot

Начиная с Telegram API 4.0 добавлена поддержка того, что они называют Telegram Passport. Это позволяет разработчику бота получать личную информацию, такую ​​как документы, удостоверяющие личность, номер телефона, адрес электронной почты и многое другое, в безопасном зашифрованном виде. Этот материал научит большей части того, что нужно знать, чтобы начать работу с Telegram Passport в качестве разработчика ботов.

С версии 11.0.0 в python-telegram-bot добавлена поддержка Telegram Passport

Если получено предупреждение от бота о том, что закрытый ключ не настроен, то убедитесь, а нужно ли вообще использовать Telegram Passport. Если ключ не настроен, то обновления, отправленные боту с личными данными, просто игнорируются.

Прежде чем продолжить, посмотрите следующую статью, описывающую Telegram Passport с точки зрения пользователя: "Знакомство с Telegram Passport"

Шаг 1: Генерация ключей.

Telegram Passport требует ключи шифрования, чтобы данные передавались безопасно. Дополнительную информацию об асимметричном шифровании можно найти в Википедии.

Затем убедитесь, что установлен openssl, введя в консоли команду ниже:

$ openssl version
# OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)

Если ваш вывод не соответствует приведенному выше (обратите внимание, что более новая или более старая версия говорит, ЧТО в порядке), то необходимо установить openssl. Простой поиск в Google/Yandex для установки openssl на [вашу операционную систему] должен показать, как это сделать.

Теперь можно сгенерировать свой закрытый ключ.

$ openssl rsa -in private.key -pubout
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0B
# [snip]
# KwIDAQAB
# -----END PUBLIC KEY-----

Этот ключ понадобится для двух вещей. Первый - зарегистрировать его в @BotFather (следующий шаг). Другой - для вызова Telegram Passport API со своего веб-сайта.

Шаг 2: Регистрация ключа в @BotFather.

Затем необходимо вставить открытый ключ в чат с @BotFather, который был сгенерирован, после отправки ему команды /setpublickey.

На этом этапе также нужно добавить политику конфиденциальности для своего бота, если ее еще нет. Это можно сделать с помощью команды /setprivacypolicy. Примечание: эта команда ожидает URL-адрес, поэтому нужно будет разместить политику конфиденциальности где-нибудь в Интернете.

Шаг 3: Настройка кнопки "Войти через Telegram".

Теперь необходимо настроить кнопку, которую ваши пользователи будут нажимать, чтобы иметь возможность войти/зарегистрироваться в Telegram. Команда Telegram написала SDK (комплекты для разработки программного обеспечения), чтобы помочь быстро приступить к работе. В этом материале будет использован только Javascript SDK, но инструкции легко адаптировать к приложению iOS/macOS или Android.

Для начала понадобится простая веб-страница, на которой есть доступ к исходному коду HTML или аналогичному. Затем нужно будет включить Javascript SDK и вызвать функцию javascript Telegram.Passport.createAuthButton.

Если нужно быстро приступить к работе, то ниже представлен пример HTML-страницы, которую можно просто загрузить, отредактировать с помощью редактора, а затем открыть в предпочитаемом вами браузере. (Обратите внимание, что нужно будет загрузить фактический файл SDK и поместить его в ту же папку, что и файл HTML)

Затем необходимо заполнить идентификатор вашего бота bot_id (цифровая часть перед: в токене бота), область действия scope (какие данные необходимо запросить), открытый ключ public_key (осторожно с новыми строками), одноразовые данные nonce и URL-адрес обратного вызова callback_url (страница входа на сайт).

Telegram.Passport.createAuthButton('telegram_passport_auth', {
        bot_id: 1234567890, 
        // КАКИЕ ДАННЫЕ НЕОБХОДИМО ПОЛУЧИТЬ
        scope: {
            data: [{
                type: 'id_document',
                selfie: true
            }, 'address_document', 'phone_number', 'email'], v: 1
        }, 
        // ПУБЛИЧНЫЙ КЛЮЧ ЗАРЕГИСТРИРОВАННЫЙ в @BotFather.
        public_key: '-----BEGIN PUBLIC KEY-----\n', 
        // ВАШ БОТ ПОЛУЧИТ ЭТИ ДАННЫЕ ВМЕСТЕ С ЗАПРОСОМ
        nonce: 'thisisatest', 
        // TELEGRAM ОТПРАВИТ ВАШЕГО ПОЛЬЗОВАТЕЛЯ ОБРАТНО НА ЭТОТ URL
        callback_url: 'https://example.org' 
    });

Примечание: В целях безопасности необходимо генерировать случайный одноразовый номер nonce для каждого пользователя, который посещает ваш сайт, и ВСЕГДА проверять его с вашим ботом, когда получаем паспортные/личные данные. Если на сайте есть серверная часть Python, то может пригодиться что-то вроде itsdangerous - в противном случае другие методы подписи HMAC также должны быть безопасными.

Примечание. Для простого тестирования можно использовать https://example.org в качестве callback_url и это нормально, но на реальных сайтах он должен быть установлен на URL-адрес, по которому пользователи будут уведомлены об успешном входе в систему - после того конечно, как бот проверит паспортные/личные данные.

Примечание. В приведенном выше примере scope запрашивает документ (например, паспорт, водительские права и т. д.), который включает в себя фото и адрес, а также его номер телефона и электронная почта. Более подробно об использовании этого параметра смотрите документацию по scope.

Шаг 4: MessageHandler, который принимает элементы PassportData.

Теперь необходимо добавить в диспетчер - обработчик MessageHandler, чтобы можно было получать элементы объекта Message, а PassportData будет присутствовать как атрибут этого объекта (passport_data). Если нужно, чтобы обработчик сообщений получал только Telegram Passport (рекомендуется), то необходимо использовать фильтр filter.PASSPORT_DATA.

Подробнее смотрите ниже "Пример скрипта бота passportbot.py".

Шаг 5: Тестирование.

Последний шаг - попробовать авторизироваться на сайте через синюю кнопку "Войти через Telegram". После настройки пароля и загрузки соответствующих документов можно будет увидеть данные, напечатанные в консоли.

Пример HTML-страницы кнопки "Войти через Telegram".

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Telegram passport test!</title>
    <meta charset="utf-8">
    <meta content="IE=edge" http-equiv="X-UA-Compatible">
    <meta content="width=device-width, initial-scale=1" name="viewport">
</head>
<body>
<h1>Telegram passport test</h1>

<div id="telegram_passport_auth"></div>
</body>

<!--- Нужен файл из https://github.com/TelegramMessenger/TGPassportJsSDK --->
<script src="telegram-passport.js"></script>
<script>
    "use strict";
    Telegram.Passport.createAuthButton('telegram_passport_auth', {
        // ИДЕНТИФИКАТОР БОТА
        bot_id: 1234567890, 
        // КАКИЕ ДАННЫЕ НУЖНО ПОЛУЧИТЬ
        scope: { 
            data: [{
                type: 'id_document',
                selfie: true
            }, 'address_document', 'phone_number', 'email'], v: 1
        },
        // ВАШ ОТКРЫТЫЙ КЛЮЧ
        public_key: '-----BEGIN PUBLIC KEY-----\n',
        // ВАШ БОТ ПОЛУЧИТ ЭТИ ДАННЫЕ ВМЕСТЕ С ЗАПРОСОМ
        nonce: 'thisisatest', 
        // TELEGRAM ОТПРАВИТ ВАШЕГО ПОЛЬЗОВАТЕЛЯ ОБРАТНО НА ЭТОТ URL
        callback_url: 'https://example.org' 
    });

</script>
</html>

Пример скрипта бота passportbot.py.

Этот скрипт просто расшифрует и распечатает все полученные данные Telegram Passport. Он также загрузит все найденные PassportFiles в текущий каталог. Чтобы начать работу, замените TOKEN на токен вашего бота и поместите свой private.key в тот же каталог, что и скрипт.

## passportbot.py
import logging
from pathlib import Path

from telegram import __version__ as TG_VER

try:
    from telegram import __version_info__
except ImportError:
    __version_info__ = (0, 0, 0, 0, 0)  # type: ignore[assignment]

if __version_info__ < (20, 0, 0, "alpha", 5):
    raise RuntimeError(
        f"Этот пример несовместим с текущей версией PTB. {TG_VER}."
    )
from telegram import Update
from telegram.ext import Application, ContextTypes, MessageHandler, filters

# Включить ведение журнала
logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)

logger = logging.getLogger(__name__)

async def msg(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Загружает и распечатывает полученные паспортные данные."""
    # Извлечение данных
    passport_data = update.message.passport_data
    # Если `nonce` не соответствует тому, что сгенерировано серверной частью, 
    # то это обновление исходит не от нас. В идеале необходимо рандомизировать `nonce` на сервере.
    if passport_data.decrypted_credentials.nonce != "thisisatest":
        return

    # Распечатать расшифрованные данные
    # Файлы будут загружены в текущий каталог
    for data in passport_data.decrypted_data:  # здесь данные расшифровываются
        if data.type == "phone_number":
            print("Phone: ", data.phone_number)
        elif data.type == "email":
            print("Email: ", data.email)
        if data.type in (
            "personal_details",
            "passport",
            "driver_license",
            "identity_card",
            "internal_passport",
            "address",
        ):
            print(data.type, data.data)
        if data.type in (
            "utility_bill",
            "bank_statement",
            "rental_agreement",
            "passport_registration",
            "temporary_registration",
        ):
            print(data.type, len(data.files), "files")
            for file in data.files:
                actual_file = await file.get_file()
                print(actual_file)
                await actual_file.download_to_drive()
        if (
            data.type in ("passport", "driver_license", "identity_card", "internal_passport")
            and data.front_side
        ):
            front_file = await data.front_side.get_file()
            print(data.type, front_file)
            await front_file.download_to_drive()
        if data.type in ("driver_license" and "identity_card") and data.reverse_side:
            reverse_file = await data.reverse_side.get_file()
            print(data.type, reverse_file)
            await reverse_file.download_to_drive()
        if (
            data.type in ("passport", "driver_license", "identity_card", "internal_passport")
            and data.selfie
        ):
            selfie_file = await data.selfie.get_file()
            print(data.type, selfie_file)
            await selfie_file.download_to_drive()
        if data.translation and data.type in (
            "passport",
            "driver_license",
            "identity_card",
            "internal_passport",
            "utility_bill",
            "bank_statement",
            "rental_agreement",
            "passport_registration",
            "temporary_registration",
        ):
            print(data.type, len(data.translation), "translation")
            for file in data.translation:
                actual_file = await file.get_file()
                print(actual_file)
                await actual_file.download_to_drive()


def main() -> None:
    """Запуск бота."""
    # Создание приложения (передайте ему свой токен и закрытый ключ)
    private_key = Path("private.key")
    application = (
        Application.builder().token("TOKEN").private_key(private_key.read_bytes()).build()
    )

    # В сообщениях, содержащих личные данные, нужно вызвать функцию `msg()`
    application.add_handler(MessageHandler(filters.PASSPORT_DATA, msg))
    application.run_polling()

if __name__ == "__main__":
    main()