Skip to content

url_signer — HMAC-signed URLs for Mini Apps

⬅️ Back | 🏠 Docs Root

UrlSignerService

UrlSignerService

Generation of HMAC-signed URLs for Telegram Mini Apps.

Signs URLs using HMAC-SHA256 and a timestamp. This protects against forgery and limits the link's lifetime.

Parameters:

Name Type Description Default
secret_key str

Secret key for signing (string).

required
Example
signer = UrlSignerService(secret_key=settings.secret_key)
url = signer.generate_signed_url(
    base_url="https://myapp.example.com",
    request_id=42,
    action="reply",
)
# https://myapp.example.com/tma/reply/?req_id=42&ts=...&sig=...
Source code in src/codex_bot/url_signer/service.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class UrlSignerService:
    """Generation of HMAC-signed URLs for Telegram Mini Apps.

    Signs URLs using HMAC-SHA256 and a timestamp.
    This protects against forgery and limits the link's lifetime.

    Args:
        secret_key: Secret key for signing (string).

    Example:
        ```python
        signer = UrlSignerService(secret_key=settings.secret_key)
        url = signer.generate_signed_url(
            base_url="https://myapp.example.com",
            request_id=42,
            action="reply",
        )
        # https://myapp.example.com/tma/reply/?req_id=42&ts=...&sig=...
        ```
    """

    def __init__(self, secret_key: str) -> None:
        self._secret_key = secret_key.encode("utf-8")

    def generate_signed_url(
        self,
        base_url: str,
        request_id: str | int,
        action: str = "reply",
    ) -> str:
        """Generates a signed URL for a WebApp.

        Builds a URL of the form:
        ``{base_url}/tma/{action}/?req_id=...&ts=...&sig=...``

        The signature is calculated based on the string ``"{request_id}:{timestamp}"``.

        Args:
            base_url: Root URL of the site (e.g., ``"https://myapp.example.com"``).
            request_id: Request ID included in the signature.
            action: Action path (default is ``"reply"``).

        Returns:
            Full URL with signature parameters.

        Example:
            ```python
            url = signer.generate_signed_url(
                base_url="https://myapp.example.com",
                request_id=123,
                action="confirm",
            )
            ```
        """
        timestamp = str(int(time.time()))
        req_id_str = str(request_id)

        payload = f"{req_id_str}:{timestamp}".encode()
        signature = hmac.new(self._secret_key, payload, hashlib.sha256).hexdigest()

        params = {"req_id": req_id_str, "ts": timestamp, "sig": signature}
        clean_base_url = base_url.rstrip("/")

        return f"{clean_base_url}/tma/{action}/?{urlencode(params, quote_via=quote_plus)}"

    def verify_signed_url(
        self,
        req_id: str,
        timestamp: str,
        signature: str,
        max_age: int = 300,
    ) -> bool:
        """Verifies the URL signature.

        Args:
            req_id: Request ID from the ``req_id`` parameter.
            timestamp: Timestamp from the ``ts`` parameter.
            signature: Signature from the ``sig`` parameter.
            max_age: Maximum signature age in seconds (default is 300).

        Returns:
            ``True`` if the signature is valid and has not expired.

        Example:
            ```python
            is_valid = signer.verify_signed_url(
                req_id=request.query_params["req_id"],
                timestamp=request.query_params["ts"],
                signature=request.query_params["sig"],
            )
            ```
        """
        try:
            ts_int = int(timestamp)
        except ValueError:
            return False

        if int(time.time()) - ts_int > max_age:
            return False

        payload = f"{req_id}:{timestamp}".encode()
        expected = hmac.new(self._secret_key, payload, hashlib.sha256).hexdigest()

        return hmac.compare_digest(expected, signature)

Functions

generate_signed_url(base_url, request_id, action='reply')

Generates a signed URL for a WebApp.

Builds a URL of the form: {base_url}/tma/{action}/?req_id=...&ts=...&sig=...

The signature is calculated based on the string "{request_id}:{timestamp}".

Parameters:

Name Type Description Default
base_url str

Root URL of the site (e.g., "https://myapp.example.com").

required
request_id str | int

Request ID included in the signature.

required
action str

Action path (default is "reply").

'reply'

Returns:

Type Description
str

Full URL with signature parameters.

Example
url = signer.generate_signed_url(
    base_url="https://myapp.example.com",
    request_id=123,
    action="confirm",
)
Source code in src/codex_bot/url_signer/service.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def generate_signed_url(
    self,
    base_url: str,
    request_id: str | int,
    action: str = "reply",
) -> str:
    """Generates a signed URL for a WebApp.

    Builds a URL of the form:
    ``{base_url}/tma/{action}/?req_id=...&ts=...&sig=...``

    The signature is calculated based on the string ``"{request_id}:{timestamp}"``.

    Args:
        base_url: Root URL of the site (e.g., ``"https://myapp.example.com"``).
        request_id: Request ID included in the signature.
        action: Action path (default is ``"reply"``).

    Returns:
        Full URL with signature parameters.

    Example:
        ```python
        url = signer.generate_signed_url(
            base_url="https://myapp.example.com",
            request_id=123,
            action="confirm",
        )
        ```
    """
    timestamp = str(int(time.time()))
    req_id_str = str(request_id)

    payload = f"{req_id_str}:{timestamp}".encode()
    signature = hmac.new(self._secret_key, payload, hashlib.sha256).hexdigest()

    params = {"req_id": req_id_str, "ts": timestamp, "sig": signature}
    clean_base_url = base_url.rstrip("/")

    return f"{clean_base_url}/tma/{action}/?{urlencode(params, quote_via=quote_plus)}"

verify_signed_url(req_id, timestamp, signature, max_age=300)

Verifies the URL signature.

Parameters:

Name Type Description Default
req_id str

Request ID from the req_id parameter.

required
timestamp str

Timestamp from the ts parameter.

required
signature str

Signature from the sig parameter.

required
max_age int

Maximum signature age in seconds (default is 300).

300

Returns:

Type Description
bool

True if the signature is valid and has not expired.

Example
is_valid = signer.verify_signed_url(
    req_id=request.query_params["req_id"],
    timestamp=request.query_params["ts"],
    signature=request.query_params["sig"],
)
Source code in src/codex_bot/url_signer/service.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def verify_signed_url(
    self,
    req_id: str,
    timestamp: str,
    signature: str,
    max_age: int = 300,
) -> bool:
    """Verifies the URL signature.

    Args:
        req_id: Request ID from the ``req_id`` parameter.
        timestamp: Timestamp from the ``ts`` parameter.
        signature: Signature from the ``sig`` parameter.
        max_age: Maximum signature age in seconds (default is 300).

    Returns:
        ``True`` if the signature is valid and has not expired.

    Example:
        ```python
        is_valid = signer.verify_signed_url(
            req_id=request.query_params["req_id"],
            timestamp=request.query_params["ts"],
            signature=request.query_params["sig"],
        )
        ```
    """
    try:
        ts_int = int(timestamp)
    except ValueError:
        return False

    if int(time.time()) - ts_int > max_age:
        return False

    payload = f"{req_id}:{timestamp}".encode()
    expected = hmac.new(self._secret_key, payload, hashlib.sha256).hexdigest()

    return hmac.compare_digest(expected, signature)