Часть 2.5. TMA на KMP. Аутентификации пользователя с DRF
Эта короткая статья является дополнением ко второй, но можно прочить независимо, если требуется только реализация серверной части аутентификации.
Навигация по циклу статей:
Часть 1. Пишем веб-приложение кликер на Kotlin
Часть 2. Пишем кликер для Telegram на Kotlin
Часть 2.5. Аутентификация пользователя с DRF — текущая статья
Часть 3. Добавляем оплату через Telegram Mini Apps на Kotlin — в разработке
Раскрытые темы в цикле
Web приложение на Kotlin — часть 1
Интеграция приложения с Telegram Mini Apps — часть 2
Работа с элементами интерфейса TMA приложения. Тема,
MainButton
,BackButton
— часть 2Поделиться ссылкой на приложение через Telegram. Передача данных через ссылку — часть 2
Аутентификации через TMA приложение — часть 2 и 2.5
Telegram Payments API– часть 3
Техническое задание. Кратко
Аутентификация через TMA приложение
Обработка данных, используемых для работы реферальной системы
Аутентификация с Django Rest Framework
Поскольку будет использоваться собственный заголовок tma-data,
где лежат сырые initData
от TMA API, и собственный алгоритм валидации данных, то будем использовать BaseAuthentication
Сама функция валидации tma-data
разделяет отправленные данные, удаляет из них hash, сортирует и склеивает обратно в строку с разделителем начала новой строки. Дальше с использование hmac
сравнивает с hash
. Эта проверка выполняется на основе документации Telegram.
from urllib.parse import unquote_plus
import hashlib
import hmac
def validate_data(data: str, secret_key):
decoded = unquote_plus(data).split('&')
filtered = filter(lambda a: not a.startswith('hash='), decoded)
data_hash = ''.join(list(filter(lambda a: a.startswith('hash='), decoded))[0][5:])
sorted_data = sorted(filtered)
data_check = '\n'.join(sorted_data)
return hmac.new(secret_key, data_check.encode(), hashlib.sha256).hexdigest() == data_hash
secret_key
— не изменяется и генерируется на основе токена бота и статического ключа — строки "WebAppData”
secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest()
Далее создаём класс, к примеру TMAAuthentication
, наследника BaseAuthentication
, и выполняем проверку отправленных данных
class TMAAuthentication(BaseAuthentication):
secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest()
def authenticate(self, request: Request):
tma_data = request.headers.get('tma-data', None)
if tma_data is None or len(tma_data) == 0 or not validate_data(tma_data, secret_key=self.secret_key):
return None
data = parse_qs(tma_data)
user_data = get_user_data(data)
if user_data is None:
return None
user, is_new = get_user_or_create(user_data)
user.is_authenticated = True
return user, tma_data
Для получения или создания (при первом входе) создаём в базе даных пользователя. Здесь уже бекендер решает, как лучше сохранять пользователей, статья про то, как валидировать данные с TMA клиента.
def get_user_or_create(user):
return TelegramUser.objects.get_or_create(
pk=user['id'],
defaults={
"username": user['username'],
"first_name": user['first_name'] if user['first_name'] is not None else '',
"last_name": user['last_name'] if user['last_name'] is not None else ''
},
)
Теперь подключим к нашим APIView
созданный TMAAuthentication
.
class ScoreAPIView(APIView):
authentication_classes = [TMAAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request: Request):
user = request.user
# ...
def post(self, request: Request):
user = request.user
# ...
Start param. Direct link. Реферальная ссылка
Теперь добавим обработку реферальных ссылок. Разберём только то, как можно получить эти данные. Где и в какой момент их получать решать вам.
Структура ссылки проста: https://t.me/botusername/appname? startapp=command. Как получить возможнось перехода по таким ссылкам показывалось во второй части цикла
startapp
— это и есть параметр, который мы можем передать через ссылку и обработать в клиенте, поскольку он становится частью WebAppInitData
с ключём start_param
.
В нашем приложении мы будем создавать ссылку, где startapp=ref_{user_id}
. А извлечение её на стороне сервера будет выглядеть
def get_ref_user_id(request: Request) -> int | None:
tma_data = request.headers.get('tma-data', None)
data = parse_qs(tma_data)
query_parameter = data.get('start_param', None)
if query_parameter is None or len(query_parameter) == 0:
return None
start_param = query_parameter[0]
if not start_param.startswith('ref_'):
return None
id = start_param[4:]
if not id.isnumeric():
return None
return int(id)
Из функции мы возвращаем числовое значение user_id
, с которым мы можем работать далее.
К примеру, добавить человека в список друзей, если его ещё там нет
def create_friend_rel(user: TelegramUser, request: Request):
ref = get_ref_user_id(request)
if ref is None or ref == user.id:
return
first_user = TelegramUser.objects.get(id=ref)
rel_1 = get_or_none(FriendRelationship, first=first_user, second=user)
rel_2 = get_or_none(FriendRelationship, first=user, second=first_user)
if rel_1 is not None or rel_2 is not None:
return
rel = FriendRelationship(first=first_user, second=user, is_invitation=user.is_new)
rel.save()
Для простоты в нашем кликере местом вызова будет авторизация пользователя. Так делать на самом не стоит, лишняя операция на авторизацию.
class TMAAuthentication(BaseAuthentication):
secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest()
def authenticate(self, request: Request):
# ...
user, is_new = get_user_or_create(user_data)
user.is_authenticated = True
user.is_new = is_new
create_friend_rel(user, request)
return user, tma_data
Итоги
Теперь кликер может авторизовываться, используя свой аккаунт в Telegram и передавать различные данные по через ссылку входа в приложение, а именно реферальный код. Остальную часть реализации работы приложения показывать в статье смысла показывать нет, много других статей с информацией о работе с Django и DRF, наша цель же показать как можно обрабатывать данные конкретно из TMA приложения. И, что интересно, мы всё ещё ни разу не запустили нашего бота