Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4d30fc3
Merge branch 'main' into develop
turtlesocks-bot Jun 5, 2026
f5ab32c
Merge branch 'main' into develop
turtlesocks-bot Jun 5, 2026
d6525bb
Merge branch 'main' into develop
turtlesocks-bot Jun 5, 2026
c91a2d0
Merge branch 'main' into develop
turtlesocks-bot Jun 5, 2026
2f953f7
Merge branch 'main' into develop
turtlesocks-bot Jun 5, 2026
dbab2b1
Merge branch 'main' into develop
turtlesocks-bot Jun 6, 2026
f1baa74
Merge branch 'main' into develop
turtlesocks-bot Jun 16, 2026
8dd025c
Merge branch 'main' into develop
turtlesocks-bot Jun 18, 2026
7abe4bb
Merge branch 'main' into develop
turtlesocks-bot Jun 18, 2026
c9da668
Merge branch 'main' into develop
turtlesocks-bot Jun 28, 2026
329b35a
Merge branch 'main' into develop
turtlesocks-bot Jul 5, 2026
f2d642b
fix(scanArea): prevent crash when area feature has no name/key (#1225)
pjorritsma Jul 5, 2026
f7ea5bd
Merge branch 'main' into develop
turtlesocks-bot Jul 5, 2026
d98bb94
Merge branch 'main' into develop
turtlesocks-bot Jul 5, 2026
9be91d6
Merge branch 'main' into develop
turtlesocks-bot Jul 11, 2026
a5e7d04
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
0104f35
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
95c59cd
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
9c02f0d
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
dafc035
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
bac18d9
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
3e28847
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
a021fd9
Merge branch 'main' into develop
turtlesocks-bot Jul 21, 2026
7fb077c
Merge branch 'main' into develop
turtlesocks-bot Jul 22, 2026
db3ee45
Merge branch 'main' into develop
turtlesocks-bot Jul 24, 2026
e897f1d
Merge branch 'main' into develop
turtlesocks-bot Jul 24, 2026
d1bb85f
Merge branch 'main' into develop
turtlesocks-bot Jul 24, 2026
278aaaf
Merge branch 'main' into develop
turtlesocks-bot Jul 25, 2026
df8daef
Merge branch 'main' into develop
turtlesocks-bot Jul 29, 2026
d80c8f4
Merge branch 'main' into develop
turtlesocks-bot Jul 29, 2026
c78d405
Merge branch 'main' into develop
turtlesocks-bot Jul 29, 2026
c645f8d
Merge branch 'main' into develop
turtlesocks-bot Jul 29, 2026
79f1116
Merge branch 'main' into develop
turtlesocks-bot Aug 10, 2026
d9955f2
Merge branch 'main' into develop
turtlesocks-bot Aug 11, 2026
26085b0
Merge branch 'main' into develop
turtlesocks-bot Aug 19, 2026
aab6069
Merge branch 'main' into develop
turtlesocks-bot Aug 19, 2026
f6fd7c5
Merge branch 'main' into develop
turtlesocks-bot Aug 20, 2026
d97d883
Merge branch 'main' into develop
turtlesocks-bot Aug 21, 2026
cb4f384
Merge branch 'main' into develop
turtlesocks-bot Aug 23, 2026
d5bb652
Merge branch 'main' into develop
turtlesocks-bot Aug 25, 2026
5a557a8
Merge branch 'main' into develop
turtlesocks-bot Aug 27, 2026
cd20b6f
Merge branch 'main' into develop
turtlesocks-bot Aug 27, 2026
c8ff7d8
feat(auth): support Telegram OAuth (OpenID Connect)
Fabio1988 Aug 30, 2026
80d6f26
fix(auth): resolve the Telegram flow per route, not per strategy list
Fabio1988 Aug 30, 2026
dad213c
fix(auth): resolve the Telegram flow per custom login page block
Fabio1988 Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,9 @@
"eventLogGroupId": "",
"enabled": false,
"botToken": "",
"clientId": "",
"clientSecret": "",
"redirectUri": "http://localhost:8080/auth/telegram/callback",
"groups": [],
"trialPeriod": {
"start": {
Expand Down
11 changes: 11 additions & 0 deletions config/local.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@
"blockedGuilds": [],
"allowedUsers": [],
"clientPrompt": "none"
},
{
"enabled": false,
"type": "telegram",
"name": "telegram",
"botToken": "",
"clientId": "",
"clientSecret": "",
"redirectUri": "http://localhost:8080/auth/telegram/callback",
"groups": [],
"allowedUsers": []
}
],
"areaRestrictions": [
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"i18next-browser-languagedetector": "8.0.0",
"i18next-fs-backend": "2.6.6",
"i18next-http-backend": "3.0.5",
"jose": "^5.9.6",
"knex": "3.1.0",
"leaflet": "1.9.4",
"leaflet-arrowheads": "^1.4.0",
Expand All @@ -166,6 +167,7 @@
"passport": "^0.6.0",
"passport-discord": "https://github.com/tonestrike/passport-discord.git",
"passport-local": "^1.0.0",
"passport-oauth2": "^1.8.0",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-ga4": "^1.4.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/.configref
Original file line number Diff line number Diff line change
@@ -1 +1 @@
26052
26175
1 change: 1 addition & 0 deletions packages/locales/lib/human/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@
"go_back": "Go Back",
"access": "Access",
"link_discord": "Link Discord",
"link_telegram": "Link Telegram",
"select_webhook_strategy": "Alert Manager",
"webhook_strategy_success_0": "Success! Refreshing to fetch alert settings...",
"register": "Register",
Expand Down
8 changes: 8 additions & 0 deletions packages/types/lib/augmentations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ declare module '@mui/material/styles' {
fuchsia: string
red: string
}
telegram: {
main: string
contrastText: string
}
}

interface PaletteOptions {
Expand All @@ -61,6 +65,10 @@ declare module '@mui/material/styles' {
fuchsia: string
red: string
}
telegram?: {
main: string
contrastText: string
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/types/lib/blocks.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ interface CustomTelegram extends BaseBlock {
type: 'telegram'
telegramBotName: string
telegramAuthUrl: string
/** Resolved server side from `telegramAuthUrl`, not set in config */
telegramOAuth?: boolean
}

interface CustomLocal extends BaseBlock {
Expand Down
15 changes: 9 additions & 6 deletions server/src/graphql/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { missing, readAndParseJson } = require('@rm/locales')

const { buildDefaultFilters } = require('../filters/builder/base')
const { filterComponents } = require('../utils/filterComponents')
const { annotateTelegramBlocks } = require('../utils/getTelegramStrategy')
const { validateSelectedWebhook } = require('../utils/validateSelectedWebhook')
const { PoracleAPI } = require('../services/Poracle')
const { geocoder } = require('../services/geocoder')
Expand Down Expand Up @@ -146,14 +147,16 @@ const resolvers = {
components = [],
...rest
} = config.getMapConfig(req)[component]
const strategies = config.getSafe('authentication.strategies')
const prepare = (blocks) =>
annotateTelegramBlocks(
filterComponents(blocks, !!username, perms.donor),
strategies,
)
return {
...rest,
footerButtons: filterComponents(
footerButtons,
!!username,
perms.donor,
),
components: filterComponents(components, !!username, perms.donor),
footerButtons: prepare(footerButtons),
components: prepare(components),
}
}
return null
Expand Down
15 changes: 15 additions & 0 deletions server/src/routes/authRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ const loadAuthStrategies = () => {
delete req.session.discordPromptRetry
}

// Telegram's OAuth flow reports a cancelled consent screen as an error
// param, which passport-oauth2 turns into a thrown AuthorizationError.
// Send those to /blocked, the same place a rejected login ends up.
if (
strategy.type === 'telegram' &&
typeof req.query.error === 'string'
) {
log.debug(TAGS.auth, 'Telegram auth was denied:', req.query.error)
return res.redirect(
`/blocked/${encodeURIComponent(
new URLSearchParams({ message: 'access_denied' }).toString(),
)}`,
)
}

return passport.authenticate(
name,
getAuthenticateOptions(req),
Expand Down
122 changes: 119 additions & 3 deletions server/src/services/TelegramClient.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// @ts-check
const { default: fetch } = require('node-fetch')
const { TelegramStrategy } = require('@rainb0w-clwn/passport-telegram-official')
const { createRemoteJWKSet, jwtVerify } = require('jose')
const passport = require('passport')
const OAuth2Strategy = require('passport-oauth2')

const config = require('@rm/config')

Expand All @@ -15,8 +17,38 @@ const { AuthClient } = require('./AuthClient')

/**
* @typedef {import('@rainb0w-clwn/passport-telegram-official/dist/types').PassportTelegramUser} TGUser
* @typedef {Parameters<import('@rainb0w-clwn/passport-telegram-official/dist/types').CallbackWithRequest>[0]} AuthRequest
*/

const TG_ISSUER = 'https://oauth.telegram.org'
const TG_AUTHORIZATION_URL = `${TG_ISSUER}/auth`
const TG_TOKEN_URL = `${TG_ISSUER}/token`
const TG_JWKS_URL = `${TG_ISSUER}/.well-known/jwks.json`

/**
* Telegram rotates its signing keys, so the set is fetched lazily and cached
* by `jose` rather than pinned at boot. Shared across every telegram strategy
* since the keys are not client specific.
*/
const getJwks = (() => {
/** @type {ReturnType<typeof createRemoteJWKSet>} */
let jwks
return () => {
if (!jwks) jwks = createRemoteJWKSet(new URL(TG_JWKS_URL))
return jwks
}
})()

/**
* JWT claims are `unknown` until narrowed, and the optional ones are simply
* absent when the user has not set them on their Telegram account.
*
* @param {unknown} claim
* @returns {string | undefined}
*/
const claimToString = (claim) =>
claim === undefined || claim === null ? undefined : String(claim)

class TelegramClient extends AuthClient {
/** @param {TGUser} user */
async getUserGroups(user) {
Expand Down Expand Up @@ -242,15 +274,99 @@ class TelegramClient extends AuthClient {
}
}

/**
* Telegram's OpenID Connect provider has no UserInfo endpoint - the profile
* is carried by the `id_token` returned from the token exchange, so it has to
* be verified against the JWKS before anything in it is trusted.
*
* The `sub` claim is an opaque, client specific identifier. The actual
* Telegram user id only arrives as the `id` claim under the `profile` scope,
* and that is what the rest of ReactMap keys off of (`users.telegramId`,
* `strategy.groups`, `strategy.allowedUsers`, the `getChatMember` lookup), so
* `sub` is deliberately ignored.
*
* @param {AuthRequest} req
* @param {Record<string, any>} params token endpoint response
* @param {(err: any, user?: any, info?: any) => void} done
*/
async oidcHandler(req, params, done) {
try {
if (!params?.id_token) {
throw new Error('No id_token was returned by Telegram')
}
const { payload } = await jwtVerify(params.id_token, getJwks(), {
issuer: TG_ISSUER,
audience: String(this.strategy.clientId),
})
if (!payload.id) {
throw new Error(
'The id_token has no `id` claim, the `profile` scope was not granted',
)
}
const firstName = claimToString(payload.given_name)
const lastName = claimToString(payload.family_name)

return this.authHandler(
req,
// The OIDC flow has no `hash` or `auth_date` - those belong to the
// legacy widget - so this is not a complete PassportTelegramUser
// @ts-ignore
{
// String, to match both the `telegramId` varchar column and the
// string role ids that `groups` / `allowedUsers` are compared against
id: String(payload.id),
username: claimToString(payload.preferred_username),
first_name: firstName,
last_name: lastName,
name: { givenName: firstName, familyName: lastName },
photo_url: claimToString(payload.picture),
provider: 'telegram',
},
done,
)
} catch (e) {
this.log.error('Unable to validate the Telegram id_token', e)
return done(null, false, { message: 'access_denied' })
}
}

initPassport() {
const { clientId, clientSecret } = this.strategy

if (!clientId || !clientSecret) {
// Legacy hash signed Login Widget, still supported by Telegram
passport.use(
this.rmStrategy,
new TelegramStrategy(
{
botToken: this.strategy.botToken,
passReqToCallback: true,
},
(req, profile, done) => this.authHandler(req, profile, done),
),
)
return
}

passport.use(
this.rmStrategy,
new TelegramStrategy(
new OAuth2Strategy(
{
botToken: this.strategy.botToken,
authorizationURL: TG_AUTHORIZATION_URL,
tokenURL: TG_TOKEN_URL,
clientID: clientId,
clientSecret,
callbackURL: this.strategy.redirectUri,
// `profile` is required, it is the only source of the Telegram user id
scope: ['openid', 'profile'],
state: true,
pkce: 'S256',
passReqToCallback: true,
},
(req, profile, done) => this.authHandler(req, profile, done),
// The 6 argument arity is what makes passport-oauth2 hand us `params`,
// which is where the id_token lives
(req, _accessToken, _refreshToken, params, _profile, done) =>
this.oidcHandler(req, params, done),
),
)
}
Expand Down
7 changes: 7 additions & 0 deletions server/src/utils/getServerSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const config = require('@rm/config')
const { clientOptions } = require('../ui/clientOptions')
const { advMenus } = require('../ui/advMenus')
const { drawer } = require('../ui/drawer')
const { isTelegramOAuth } = require('./getTelegramStrategy')

/**
*
Expand Down Expand Up @@ -52,6 +53,12 @@ function getServerSettings(req) {
loggedIn: !!req.user,
excludeList: authentication.excludeFromTutorial,
methods: authentication.methods,
// Resolved per domain, since customRoutes is part of the domain's map
// config and each domain can target a different telegram strategy
telegramOAuth: isTelegramOAuth(
mapConfig.customRoutes.telegramAuthUrl,
authentication.strategies,
),
},
database: {
settings: {
Expand Down
Loading
Loading