Section 2.5. TMA on KMP. User Authentication with DRF
This short article is a complement to the second one, but can be read independently if you only need to implement the server-side authentication.
Navigate through the article series:
Part 1. Writing a clicker web application in Kotlin
Part 2. Writing a clicker for Telegram in Kotlin
Section 2.5. User Authentication with DRF – Current Article
Part 3. Adding payment via Telegram Mini Apps on Kotlin – in development
Topics covered in the series
Web Application in Kotlin – Part 1
Integrating the App with Telegram Mini Apps – Part 2
Working with the interface elements of the TMA application. Theme,
MainButton
,BackButton
– part 2Share a link to an application via Telegram. Transferring data via a link – part 2
Authentication via TMA application – part 2 and 2.5
Telegram Payments API – Part 3
Technical task. Briefly
Authentication via TMA application
Processing of data used for the operation of the referral system
Authentication with Django Rest Framework
Because a custom title will be used tma-data,
where are the raw ones initData
from TMA API, and our own data validation algorithm, then we will use BaseAuthentication
The validation function itself tma-data
splits the sent data, removes the hash from it, sorts it and glues it back into a string with a newline separator. Then with the use of hmac
compares with hash
This check is performed based on documentation. 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
– does not change and is generated based on the bot token and a static key – string “WebAppData”
secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest()
Next we create a class, for example TMAAuthentication
heir BaseAuthentication
and we check the sent data
class TMAAuthentication(BaseAuthentication):
secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest()
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">authenticate</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):
tma_data = request.headers.get(<span class="hljs-string" style="box-sizing: border-box; color: var(--yfm-color-hljs-deletion);">'tma-data'</span>, <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>)
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">if</span> tma_data <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">or</span> <span class="hljs-built_in" style="box-sizing: border-box; color: var(--yfm-color-hljs-addition);">len</span>(tma_data) == <span class="hljs-number" style="box-sizing: border-box; color: var(--yfm-color-hljs-deletion);">0</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">or</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">not</span> validate_data(tma_data, secret_key=self.secret_key):
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>
data = parse_qs(tma_data)
user_data = get_user_data(data)
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">if</span> user_data <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>:
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>
user, is_new = get_user_or_create(user_data)
user.is_authenticated = <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">True</span>
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> user, tma_data
To get or create (at first login), we create a user in the database. Here the backend decides how best to save users, an article about how to validate data from the client's 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 ''
},
)
Now let's connect to ours APIView
created TMAAuthentication
.
class ScoreAPIView(APIView):
authentication_classes = [TMAAuthentication]
permission_classes = [IsAuthenticated]
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">get</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):
user = request.user
<span class="hljs-comment" style="box-sizing: border-box; color: var(--yfm-color-hljs-comment);"># ...</span>
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">post</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):
user = request.user
<span class="hljs-comment" style="box-sizing: border-box; color: var(--yfm-color-hljs-comment);"># ...</span>
Start param. Direct link. Referral link
Now let's add referral link processing. We'll only look at how you can get this data. Where and when to get it is up to you.
The link structure is simple: https://t.me/botusername/appname?startapp=command. How to get the opportunity to follow such links was shown in the second part of the series
startapp
– this is the parameter that we can pass through the link and process in the client, since it becomes part of WebAppInitData
with a key start_param
.
In our application we will create a link where startapp=ref_{user_id}
. And extracting it on the server side will look like
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)
From the function we return a numeric value user_id
which we can work with further.
For example, add a person to your friends list if they are not there yet
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(<span class="hljs-built_in" style="box-sizing: border-box; color: var(--yfm-color-hljs-addition);">id</span>=ref)
rel_1 = get_or_none(FriendRelationship, first=first_user, second=user)
rel_2 = get_or_none(FriendRelationship, first=user, second=first_user)
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">if</span> rel_1 <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">not</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">or</span> rel_2 <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">not</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>:
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span>
rel = FriendRelationship(first=first_user, second=user, is_invitation=user.is_new)
rel.save()
For simplicity, in our clicker the place of calling will be user authorization. In fact, it is not worth doing this, an extra operation for authorization.
class TMAAuthentication(BaseAuthentication):
secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest()
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">authenticate</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):
<span class="hljs-comment" style="box-sizing: border-box; color: var(--yfm-color-hljs-comment);"># ...</span>
user, is_new = get_user_or_create(user_data)
user.is_authenticated = <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">True</span>
user.is_new = is_new
create_friend_rel(user, request)
<span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> user, tma_data
Results
Now the clicker can log in using his Telegram account and transfer various data via the application login link, namely the referral code. There is no point in showing the rest of the implementation of the application in the article, there are many other articles with information about working with Django and DRF, our goal is to show how to process data specifically from the TMA application. And, interestingly, we still have not launched our bot.