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 2

  • Share 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 hashThis 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 TMAAuthenticationheir BaseAuthenticationand 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_idwhich 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *