Telegram OAuth (OpenID Connect) - #1251
Conversation
…wMap#1225) * fix(scanArea): prevent crash when area feature has no name/key Guard the scan area search filter against features missing a properties.key (which happens when a scan area polygon has no name set), instead of throwing TypeError: Cannot read properties of undefined (reading 'toLowerCase'). Also fixes a longstanding typo (geoJsonFilName / geoJsonFilname -> geoJsonFileName) in the multi-domain example config and docs. * fix: copilot comments --------- Co-authored-by: Mygod <contact-git@mygod.be>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7274488688
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Telegram now runs an OIDC provider at oauth.telegram.org, replacing the hash-signed Login Widget with an authorization-code + PKCE flow. The existing `telegram` strategy is upgraded in place rather than adding a new type: when a strategy has both `clientId` and `clientSecret`, TelegramClient registers a passport-oauth2 strategy against Telegram's endpoints; without them it keeps registering the legacy widget strategy. No config rename, no DB migration, no re-linking. The `id_token` is verified against Telegram's JWKS with `jose`, which covers all four signing algorithms BotFather offers (RS256, ES256, EdDSA, ES256K) and enforces signature, issuer and audience in one call. Telegram has no UserInfo endpoint, so the profile is read from the token claims and handed to the existing authHandler, leaving groups, perms, trials and account linking untouched. Identity comes from the `id` claim, not `sub`. `sub` is an opaque per-client identifier; `id` (profile scope) is the real Telegram user id that users.telegramId, strategy.groups, strategy.allowedUsers and the getChatMember lookup all key off, so existing accounts carry over. Client-side, a derived `authentication.telegramOAuth` flag tells the app which flow to render, since `authentication.methods` only carries strategy types. TelegramLogin picks the button or the widget from it, and the three call sites (login page, profile linking, login-page builder) share it. Also sends a cancelled Telegram consent screen to /blocked instead of letting passport's AuthorizationError surface as a 500. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QB1uHoTq84BLq1ikVBFheY
7274488 to
c8ff7d8
Compare
The `telegramOAuth` flag was derived with `some()` over every enabled telegram strategy, so a config running two of them - one legacy widget, one OAuth - reported OAuth for both. The login page renders a single control pointed at `map.customRoutes.telegramAuthUrl`, so the legacy route got a redirect link instead of the widget script and login failed. Which flow a control needs is a property of the one strategy behind its route, so resolve it from the auth URL instead. That also makes it correct for multiDomain, where customRoutes is per domain and each domain can target a different telegram strategy - hence the move out of the global config mutations and into getServerSettings, which has the per-request map config. An auth URL that does not resolve by name (a custom or proxied path) falls back to the only enabled telegram strategy when there is exactly one, and to the legacy widget when it is ambiguous. Reported by chatgpt-codex-connector on WatWowMap#1251. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QB1uHoTq84BLq1ikVBFheY
Custom login page blocks carry their own `telegramAuthUrl`, which can point at a different strategy than the domain's `customRoutes` default, so they cannot inherit the page level flag either. The customComponent resolver now annotates each telegram block with the flow resolved from that block's own route, recursing into parent blocks, and Generator passes it down. TelegramLogin prefers an explicitly resolved flow and falls back to the domain default when a caller does not supply one, so the login page and profile linking are unchanged. The block list is copied rather than mutated, since it comes straight off the shared config object. customComponent returns a JSON scalar, so the added field needs no schema change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QB1uHoTq84BLq1ikVBFheY
There was a problem hiding this comment.
Pull request overview
Adds Telegram OIDC with PKCE while preserving the legacy Login Widget and existing account identity mapping.
Changes:
- Adds conditional OAuth2 strategy registration and ID-token verification.
- Exposes the resolved flow to login, account-linking, and custom-page components.
- Adds dependencies, configuration, types, localization, and resolution tests.
Reviewed changes
Copilot reviewed 18 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
yarn.lock |
Locks new authentication dependencies. |
package.json |
Adds JOSE and OAuth2 packages. |
config/default.json |
Adds Telegram OAuth defaults. |
config/local.example.json |
Documents a Telegram strategy example. |
packages/config/.configref |
Updates generated config reference. |
packages/locales/lib/human/en.json |
Adds account-linking text. |
packages/types/lib/augmentations.d.ts |
Types the Telegram theme palette. |
packages/types/lib/blocks.d.ts |
Types the resolved flow flag. |
server/src/graphql/resolvers.js |
Annotates custom Telegram blocks. |
server/src/routes/authRouter.js |
Handles cancelled Telegram consent. |
server/src/services/TelegramClient.js |
Implements OAuth2 and token validation. |
server/src/utils/getServerSettings.js |
Exposes the domain flow flag. |
server/src/utils/getTelegramStrategy.js |
Resolves strategy flow from auth URLs. |
server/test/telegramStrategyResolution.test.js |
Tests strategy and block resolution. |
src/assets/theme.js |
Adds Telegram branding colors. |
src/components/auth/Telegram.jsx |
Selects OAuth button or legacy widget. |
src/components/Config.jsx |
Loads the flow flag into state. |
src/features/builder/components/Generator.jsx |
Supports custom-block flow selection. |
src/features/profile/LinkAccounts.jsx |
Updates Telegram account linking. |
src/pages/login/Methods.jsx |
Uses the unified Telegram control. |
src/store/useMemory.js |
Initializes the flow flag. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| 'The id_token has no `id` claim, the `profile` scope was not granted', | ||
| ) | ||
| } | ||
| const firstName = claimToString(payload.given_name) |
| tokenURL: TG_TOKEN_URL, | ||
| clientID: clientId, | ||
| clientSecret, | ||
| callbackURL: this.strategy.redirectUri, |
|
The primary OAuth flow is wired coherently, but proxied homogeneous configurations can select the wrong frontend flow, non-denial provider failures are misclassified, and multiple configured OAuth strategies share PKCE state. These cases can prevent login or obscure actionable failures. Full review comments:
|
Telegram now runs an OIDC provider at
oauth.telegram.org, replacing the hash-signed Login Widget with an authorization-code + PKCE flow.This upgrades the existing
telegramstrategy in place rather than adding a new type. If a strategy has bothclientIdandclientSecret,TelegramClientregisterspassport-oauth2against Telegram's endpoints; without them it keeps the legacy widget. No config rename, no DB migration, no re-linking — admins opt in by adding two keys from @Botfather → your bot → Login Widget.Notes
id_tokenis verified against Telegram's JWKS withjose, covering all four algorithms BotFather offers (RS256, ES256, EdDSA, ES256K) and enforcing signature, issuer and audience in one call.idclaim, notsub.subis opaque per-client;id(profile scope) is the real Telegram user id thatusers.telegramId,strategy.groups,strategy.allowedUsersand thegetChatMemberlookup all key off — so existing accounts carry over.authHandler. Groups, perms, trials and account linking are untouched.authentication.telegramOAuthflag tells the client which flow to render, sinceauthentication.methodsonly carries strategy types./blockedinstead of surfacing as a 500.Config
{ "name": "telegram", "type": "telegram", "enabled": true, "botToken": "123:ABC", "clientId": "123456789", "clientSecret": "...", "redirectUri": "https://your.map/auth/telegram/callback", "groups": [] }The redirect URI must also be registered under Allowed URLs in BotFather.
Testing
Tests, build, lint and prettier all pass; tsc gains no new errors. Verified the OAuth2 wiring (endpoints, S256, session-backed state, the verify arity that delivers id_token), token validation against a locally-signed JWT (id vs sub; wrong audience, wrong issuer and forged signature all rejected), and the flag derivation across config permutations.
Not yet tested against Telegram's live servers — that needs a real bot with Allowed URLs registered.