Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login as iOS bodyweight app #2

Open
mkb79 opened this issue Apr 6, 2023 · 1 comment
Open

Login as iOS bodyweight app #2

mkb79 opened this issue Apr 6, 2023 · 1 comment
Labels
documentation Improvements or additions to documentation

Comments

@mkb79
Copy link
Owner

mkb79 commented Apr 6, 2023

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 mkb79 added the documentation Improvements or additions to documentation label Apr 6, 2023
@mkb79
Copy link
Owner Author

mkb79 commented Dec 25, 2023

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

1 participant