Skip to content

Telegram OAuth (OpenID Connect) - #1251

Open
Fabio1988 wants to merge 1 commit into
WatWowMap:mainfrom
Fabio1988:feat/telegram-oauth
Open

Telegram OAuth (OpenID Connect)#1251
Fabio1988 wants to merge 1 commit into
WatWowMap:mainfrom
Fabio1988:feat/telegram-oauth

Conversation

@Fabio1988

@Fabio1988 Fabio1988 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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 telegram strategy in place rather than adding a new type. If a strategy has both clientId and clientSecret, TelegramClient registers passport-oauth2 against 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

  • The id_token is verified against Telegram's JWKS with jose, covering all four algorithms BotFather offers (RS256, ES256, EdDSA, ES256K) and enforcing signature, issuer and audience in one call.
  • Identity comes from the id claim, not sub. sub is opaque per-client; 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.
  • Telegram has no UserInfo endpoint, so the profile is read from the token claims and handed to the existing authHandler. Groups, perms, trials and account linking are untouched.
  • A derived authentication.telegramOAuth flag tells the client which flow to render, since authentication.methods only carries strategy types.
  • A cancelled consent screen now lands on /blocked instead 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/config/lib/mutations.js Outdated
@Fabio1988
Fabio1988 force-pushed the feat/telegram-oauth branch from 7274488 to c8ff7d8 Compare August 30, 2026 12:37
Fabio1988 added a commit to Fabio1988/ReactMap that referenced this pull request Aug 30, 2026
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
@Mygod
Mygod requested a balanced review from Copilot August 30, 2026 17:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread server/src/services/TelegramClient.js Outdated
Comment thread server/src/services/TelegramClient.js Outdated
@Mygod

Mygod commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

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:

  • [P2] Preserve OAuth when unresolved strategies agree — server/src/utils/getTelegramStrategy.js:44-48
    When a custom or proxied telegramAuthUrl does not contain /auth/<name> and multiple enabled Telegram strategies are all OAuth, this count-based fallback returns null. isTelegramOAuth() then reports false and renders the legacy widget even though no legacy strategy exists, so login cannot start; infer the common flow when all candidates agree, reserving the legacy fallback for mixed modes.

  • [P2] Propagate non-denial OAuth errors — server/src/routes/authRouter.js:76-79
    When Telegram returns an OAuth error other than cancellation, such as server_error, temporarily_unavailable, or invalid_scope, this condition still rewrites it to access_denied before Passport or the error middleware sees it. Users receive a misleading permission denial and the actual provider/configuration failure is reduced to a debug log; normalize only genuine denial codes and propagate other errors.

  • [P2] Namespace PKCE state by strategy — server/src/services/TelegramClient.js:362-364
    When two Telegram OAuth strategies are started concurrently in the same map session, such as two custom login controls opened in separate tabs, passport-oauth2 gives both instances the default session key derived only from oauth.telegram.org. The second request overwrites the first strategy's state and verifier, so the first callback fails validation and consumes the second flow's state as well; provide a strategy-specific sessionKey.

@Mygod
Mygod changed the base branch from develop to main August 30, 2026 18:16
Fabio1988 added a commit to Fabio1988/ReactMap that referenced this pull request Sep 7, 2026
Two review findings on WatWowMap#1251:

`profile` is documented as returning `name`; the `given_name`/`family_name`
pair only shows up in the example payload. Mapping the pair alone meant a
user with no @username was displayed as their numeric id even though the
token carried their name, so fall back to splitting `name`.

`redirectUri` is not inherited from default.json when a config declares its
own `strategies` array, since node-config replaces arrays rather than merging
them. Adding just the two advertised credentials therefore left the flow
without a `redirect_uri`. Fold it into a single `isOAuthStrategy` predicate
shared by the server and the client so a half configured strategy stays on
the widget instead of rendering a link for a flow the server cannot start,
and log what to add.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QB1uHoTq84BLq1ikVBFheY
@Mygod

Mygod commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Two reproducible edge cases break OAuth state isolation and login-control selection. The 13 focused tests and formatting checks passed, but lint failed because jose is missing locally; a full build and live provider login were not run.

Full review comments:

  • [P2] Give each Telegram strategy its own OAuth session key — server/src/services/TelegramClient.js:378-380
    With multiple OAuth Telegram strategies, starting logins for two strategies in separate tabs overwrites the first flow's state. passport-oauth2 defaults both strategies to the session key oauth2:oauth.telegram.org. The first callback then fails state validation and deletes the second flow's state, causing both logins to fail. Set a strategy-specific sessionKey, derived from this.rmStrategy, to keep these independent flows isolated.

  • [P2] Parse the pathname of relative authentication URLs — server/src/utils/getTelegramStrategy.js:17-22
    With multiple enabled Telegram strategies, /auth/tg?source=login resolves to the name tg?source=login instead of tg, although Express correctly routes it to the tg strategy. Lookup consequently falls back to the legacy widget even when that route uses OAuth. OAuth configurations leaving telegramBotName at its empty default then lose their usable login button. Parse relative URLs through URL with a base and use .pathname, just as for absolute URLs.

Fabio1988 added a commit to Fabio1988/ReactMap that referenced this pull request Sep 7, 2026
Addresses the remaining review findings on WatWowMap#1251:

passport-oauth2 derives its session key from the authorization URL host, so
every Telegram strategy shared `oauth2:oauth.telegram.org`. Two OAuth
strategies started at once overwrote each other's state and PKCE verifier,
and the first callback deleted the second flow's state on its way to failing,
breaking both logins. Give each strategy its own `sessionKey`.

The auth url was matched as a raw string, so `/auth/tg?source=login` resolved
to the name `tg?source=login` and fell back to the widget for a route express
routes to `tg`. Parse it through `URL` and match on the pathname.

An unresolved auth url fell back to the widget whenever more than one
Telegram strategy was enabled, even when all of them ran OAuth - an OAuth
only config has no reason to set `telegramBotName`, so that left it with no
usable login control. Only fall back when the candidates actually disagree.

Drop the callback error guard: passport-oauth2 already fails rather than
errors on `access_denied`, so the denial reached /blocked without it, while
the guard rewrote every other error - `server_error`, `invalid_scope` - into
a permission denial and hid the real failure in a debug log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QB1uHoTq84BLq1ikVBFheY
@Fabio1988

Copy link
Copy Markdown
Contributor Author

All review findings are addressed. The branch is now a single squashed commit (ab892c6e), so the older inline threads point at commits that no longer exist — summarising here instead.

Fixed as suggested

  • Flow resolved from the strategy behind the specific route, not aggregated across the strategy list. The mutations.js thread is stale — that file is no longer touched; resolution moved to getServerSettings (per domain) and the customComponent resolver (per custom-login-page block).
  • profile name claims: fall back to splitting name when given_name/family_name aren't sent, so a user with no @username is no longer shown as their numeric id.
  • Per-strategy sessionKey. Reproduced against PKCESessionStore first: with the shared key both concurrent logins fail (Invalid authorization request state. / Unable to verify authorization request state.), since verify deletes the single slot before comparing handles. Both pass with the key scoped to the strategy name.
  • Auth URL parsed through URL and matched on .pathname, so /auth/tg?source=login resolves to tg.
  • Unresolved URL only falls back to the widget when the enabled strategies actually disagree.

Two places I went a different way

  • redirectUri: validated rather than derived. There's no reliable public origin at boot, and a derived URL would fail at Telegram anyway unless it matches a registered Allowed URL. It's folded into one isOAuthStrategy predicate used by both TelegramClient and the login control, so a half-configured strategy can't render an OAuth link for a route running the widget; startup logs what to add.
  • Non-denial errors: removed the callback guard instead of narrowing it. passport-oauth2 already calls fail() (not error()) on access_denied, so denials reached /blocked without it — the guard was unnecessary from the start and its only real effect was rewriting server_error/invalid_scope into a permission denial. Gone, so genuine provider failures propagate as errors.

Two things worth knowing, both outside this PR's scope

  • The sessionKey collision isn't Telegram-specific: passport-oauth2 defaults it to the authorization URL host, so multiple Discord strategies share oauth2:discord.com and hit the same bug today. Left alone here.
  • Telegram restricts EdDSA and ES256K to the openid scope, rejecting profile. Since profile is the only source of the id claim, those two algorithms can't work with ReactMap at all — only RS256 and ES256 are viable.

Still untested against Telegram's live servers; that needs a real bot with Allowed URLs registered in BotFather.

Telegram now runs an OpenID Connect provider at https://oauth.telegram.org,
replacing the hash signed Login Widget with an authorization code + PKCE
flow. The widget still works and remains the default, so nothing changes for
existing installs - a telegram strategy switches to OAuth only once it has a
`clientId`, `clientSecret` and `redirectUri`, all from @Botfather.

The `profile` scope is required: `sub` is an opaque per client identifier,
and the real Telegram user id only arrives as the `id` claim. That is what
`users.telegramId`, `strategy.groups` and `strategy.allowedUsers` key off, so
existing accounts keep working with no migration and no re-linking.

Which flow a login control renders is resolved from the strategy behind its
own route, since a config can enable several telegram strategies and each
domain, or each custom login page block, can point at a different one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QB1uHoTq84BLq1ikVBFheY
@Mygod

Mygod commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

A valid mixed-strategy configuration can render an unusable login control. All 14 helper tests and focused formatting checks passed; missing dependencies blocked complete lint/build verification, and network restrictions prevented live Telegram verification.

Review comment:

  • [P2] Preserve auth when it is the configured strategy name — server/src/utils/getTelegramStrategy.js:17-18
    With an OAuth strategy named auth alongside a legacy Telegram strategy, /auth/auth/callback resolves to callback instead of auth. Consequently, isTelegramOAuth() returns false and renders the legacy widget, leaving OAuth configurations without telegramBotName unable to log in. Match the registered route shape rather than the last occurrence of auth, and cover this mixed-strategy case in the tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants