mkb79 / freeletics-python

Python client for the non-publicly Freeletics API
GNU Affero General Public License v3.0
5 stars 0 forks source link

Login as iOS bodyweight app #2

Open mkb79 opened 1 year ago

mkb79 commented 1 year ago

Login to Freeletics bodyweight app requires a X-Authorization and X-Authorization-Timestamp header. To prevent these, I used the User-Agent from the Nutrion app in the past, which don’t require these.

Now I have found out how to create these required headers.

import base64
import hashlib
import hmac
from datetime import datetime, timezone
from typing import Dict

import httpx

MAC_KEY = b'2f8562ec236faf401289537f8a8d53921f3aaab8b56533b5ba2c9006c41e48ea316b65212d59b12e3338c090d2a1f19645a11a2be931bf013188f2da47caecec'
MSG_PREFIX = b"e@*GE(eHj(!+XHlUShWpCDxct0}c=4"
LOGIN_URL = "https://api.freeletics.com/user/v2/password/authentication"
LOGOUT_URL = "https://api.freeletics.com/user/v1/auth/logout"
USER_AGENT = "bodyweight-ios-23100000 (iPhone; iOS 16.3.1; Freeletics 23.10.0; com.Freeletics.Freeletics-Lite; de_DE; MESZ; release)"

def timestamp_ms_now() -> int:
    return int(datetime.now(tz=timezone.utc).timestamp() * 1000)

class MessageSigner:
    def __init__(self) -> None:
        self._signer = hmac.new(MAC_KEY, msg=MSG_PREFIX, digestmod=hashlib.sha256)

    def sign(self, data: bytes, timestamp: int) -> bytes:
        signer = self._signer.copy()

        encoded_timestamp = str(timestamp).encode()
        encoded_data = base64.b64encode(data)

        signer.update(encoded_data)
        signer.update(encoded_timestamp)
        msg_digest = signer.digest()

        return base64.b64encode(msg_digest)

    def get_request_headers(self, data: bytes, timestamp: int) -> Dict[str, str]:
        signature = self.sign(data, timestamp)
        return {
            "X-Authorization-Timestamp": str(timestamp),
            "X-Authorization": signature.decode()
        }

class LoginAuthorizer(httpx.Auth):
    """Login request authenticator for the httpx package."""

    requires_request_body = True
    signer = MessageSigner()

    def auth_flow(self, request: httpx.Request):
        timestamp = timestamp_ms_now()
        headers = self.signer.get_request_headers(request.content, timestamp)
        request.headers.update(headers)
        yield request

login_authorizer = LoginAuthorizer()

async def login(username: str, password: str):
    async with httpx.AsyncClient(auth=login_authorizer) as client:
        body = {"authentication": {"email": username, "password": password}}
        headers = {"User-Agent": USER_AGENT}
        resp = await client.post(LOGIN_URL, json=body, headers=headers)

        if resp.status_code != 201:
            raise Exception()

        return resp.json()

if __name__ == "__main__":
    import asyncio

    user = "USERMAIL"
    pw = "USERPASS"

    login_response = asyncio.run(login(user, pw))
    print(login_response)
mkb79 commented 9 months ago

To make the code above working again, the User Agent must be updated to USER_AGENT = "bodyweight-ios-23491000 (iPhone; iOS 17.2.1; Freeletics 23.49.1; com.Freeletics.Freeletics-Lite; de_DE; MESZ; release)".

The complete code now is:

import base64
import hashlib
import hmac
from datetime import datetime, timezone
from typing import Dict

import httpx

MAC_KEY = b'2f8562ec236faf401289537f8a8d53921f3aaab8b56533b5ba2c9006c41e48ea316b65212d59b12e3338c090d2a1f19645a11a2be931bf013188f2da47caecec'
MSG_PREFIX = b"e@*GE(eHj(!+XHlUShWpCDxct0}c=4"
LOGIN_URL = "https://api.freeletics.com/user/v2/password/authentication"
LOGOUT_URL = "https://api.freeletics.com/user/v1/auth/logout"
USER_AGENT = "bodyweight-ios-23491000 (iPhone; iOS 17.2.1; Freeletics 23.49.1; com.Freeletics.Freeletics-Lite; de_DE; MESZ; release)"

def timestamp_ms_now() -> int:
    return int(datetime.now(tz=timezone.utc).timestamp() * 1000)

class MessageSigner:
    def __init__(self) -> None:
        self._signer = hmac.new(MAC_KEY, msg=MSG_PREFIX, digestmod=hashlib.sha256)

    def sign(self, data: bytes, timestamp: int) -> bytes:
        signer = self._signer.copy()

        encoded_timestamp = str(timestamp).encode()
        encoded_data = base64.b64encode(data)

        signer.update(encoded_data)
        signer.update(encoded_timestamp)
        msg_digest = signer.digest()

        return base64.b64encode(msg_digest)

    def get_request_headers(self, data: bytes, timestamp: int) -> Dict[str, str]:
        signature = self.sign(data, timestamp)
        return {
            "X-Authorization-Timestamp": str(timestamp),
            "X-Authorization": signature.decode()
        }

class LoginAuthorizer(httpx.Auth):
    """Login request authenticator for the httpx package."""

    requires_request_body = True
    signer = MessageSigner()

    def auth_flow(self, request: httpx.Request):
        timestamp = timestamp_ms_now()
        headers = self.signer.get_request_headers(request.content, timestamp)
        request.headers.update(headers)
        yield request

login_authorizer = LoginAuthorizer()

async def login(username: str, password: str):
    async with httpx.AsyncClient(auth=login_authorizer) as client:
        body = {"authentication": {"email": username, "password": password}}
        headers = {"User-Agent": USER_AGENT}
        resp = await client.post(LOGIN_URL, json=body, headers=headers)

        if resp.status_code != 201:
            raise Exception()

        return resp.json()

if __name__ == "__main__":
    import asyncio

    user = "USERMAIL"
    pw = "USERPASS"

    login_response = asyncio.run(login(user, pw))
    print(login_response)