OAuth2 token issuer for the Telegram Mini App
net-new: v1 had no Mini App. /oauth2/token with three grants (Telegram initData, the login widget, refresh_token), the same RS256 keys and JWKS /login already publishes, plus scopes, a short access-token life and rotating refresh tokens stored as hashes (migration 0010). A replayed refresh token revokes its whole family. /login is untouched and its scope-less token stays read-only
- v1 scenarios
- —
- Ported
- —
- Green
- —
- Failing
- —
net-new: v1 had no Mini App. /oauth2/token with three grants (Telegram initData, the login widget, refresh_token), the same RS256 keys and JWKS /login already publishes, plus scopes, a short access-token life and rotating refresh tokens stored as hashes (migration 0010). A replayed refresh token revokes its whole family. /login is untouched and its scope-less token stays read-only
What it does
A Mini App opens inside Telegram with initData — a signed string naming the
user who opened it. POST /oauth2/token exchanges that for a short-lived RS256
access token and a refresh token, so every later call is an ordinary bearer
request. The same endpoint takes the web console's login-widget payload, and a
refresh token, as two more grants; the access token is the one /login already
mints, signed by the same keys and published by the same JWKS, so a resource
server verifies both without learning a second format.
The full flow, the request and response shapes and the scopes are on Mini App API.
Behaviour that must not change
/login is untouched. It is v1's endpoint, the shipped web console posts to
it, and its token still has no scope claim — which cb_api.security reads as
read-only. A console that could only read before this feature cannot write
because of it.
The discovery document only grew. /.well-known/openid-configuration
carries v1's five keys with v1's values; the token endpoint, the grant list and
the scopes are additions beside them.
One answer for every failed grant. A bad signature, a stale auth_date and
a payload with no user in it all return invalid_grant. A client that could
tell them apart could probe which forgery got closest.
How to verify it
packages/cb-api/tests/test_miniapp_auth.py covers Telegram's algorithm — the
WebAppData key derivation, the two fields outside the signature, the replay
window — and test_oauth_endpoints.py covers the three grants over HTTP.
Rotation, replay detection and expiry are proved against real rows in
qa/integration/test_refresh_tokens.py, including the two-concurrent-refreshes
race the used_at IS NULL predicate exists for.
Group settings over HTTP, admin-gated
every /config setting plus the rules and welcome text, readable and writable by a group's admins from the Mini App. groups:read / groups:write scopes, 404 (not 403) for a group you do not administer, PATCH semantics so two admins do not overwrite each other, and the same cb_core.group_texts upsert the Telegram handlers use
Telegram-login JWT for the web console
D7 fixed: the RSA key is configured or generated once into signing_keys (migration 0008), so it survives a restart and every replica shares it - v1 generated one per gunicorn worker per start and published only the answering worker's in its JWKS. Also D-WL-2: v1's pop('hash') meant only the first of its five bot tokens could ever sign anyone in. auth_date enforcement is written but off by default (the WebHub renews by replaying the payload) - see docs/contracts/x_webhub_login.md. No QA scenario: the feature has no Telegram surface