generated from cheqd/.github
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Add replace Callback URI Domain parameter (#78)
* chore(release): 1.1.1 [skip ci] ## [1.1.1](1.1.0...1.1.1) (2023-05-24) * chore(release): 1.1.2 [skip ci] ## [1.1.2](1.1.1...1.1.2) (2023-05-30) * fix: Telegram domain checks * Add replace callback URI param * fix: Linting & deps * Update index.ts * Update index.ts --------- Co-authored-by: semantic-release-bot <[email protected]> Co-authored-by: jay-dee7 <[email protected]>
- Loading branch information
1 parent
c540371
commit 8e49758
Showing
5 changed files
with
147 additions
and
126 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,53 @@ | ||
import type { ConnectorMetadata } from "@logto/connector-kit"; | ||
import { ConnectorPlatform, ConnectorConfigFormItemType } from "@logto/connector-kit"; | ||
import type { ConnectorMetadata } from '@logto/connector-kit'; | ||
import { ConnectorPlatform, ConnectorConfigFormItemType } from '@logto/connector-kit'; | ||
|
||
export const authorizationEndpoint = "https://oauth.telegram.org/auth"; | ||
export const authorizationEndpoint = 'https://oauth.telegram.org/auth'; | ||
// Scope is set to write because we might want to send the Bot a message at some point? | ||
export const scope = "write"; | ||
export const scope = 'write'; | ||
|
||
export const defaultMetadata: ConnectorMetadata = { | ||
id: "telegram-web", | ||
target: "telegram", | ||
platform: ConnectorPlatform.Web, | ||
name: { | ||
en: "Telegram", | ||
"zh-CN": "Telegram", | ||
"tr-TR": "Telegram", | ||
ko: "Telegram", | ||
id: 'telegram-web', | ||
target: 'telegram', | ||
platform: ConnectorPlatform.Web, | ||
name: { | ||
en: 'Telegram', | ||
'zh-CN': 'Telegram', | ||
'tr-TR': 'Telegram', | ||
ko: 'Telegram', | ||
}, | ||
logo: './logo.svg', | ||
logoDark: './logo-dark.svg', | ||
description: { | ||
en: 'Telegram is a cloud-based mobile and desktop messaging app with a focus on security and speed.', | ||
'zh-CN': 'Telegram 是一款基于云的移动和桌面消息传递应用程序,专注于安全性和速度。', | ||
'tr-TR': | ||
'Telegram, güvenlik ve hıza odaklanan bulut tabanlı bir mobil ve masaüstü mesajlaşma uygulamasıdır.', | ||
ko: 'Telegram은 보안과 속도에 중점을 둔 클라우드 기반 모바일 및 데스크톱 메시징 앱입니다.', | ||
}, | ||
readme: './README.md', | ||
formItems: [ | ||
{ | ||
key: 'botToken', | ||
type: ConnectorConfigFormItemType.Text, | ||
required: true, | ||
label: 'Telegram Bot Token', | ||
placeholder: 'secret-value', | ||
}, | ||
logo: "./logo.svg", | ||
logoDark: "./logo-dark.svg", | ||
description: { | ||
en: "Telegram is a cloud-based mobile and desktop messaging app with a focus on security and speed.", | ||
"zh-CN": | ||
"Telegram 是一款基于云的移动和桌面消息传递应用程序,专注于安全性和速度。", | ||
"tr-TR": | ||
"Telegram, güvenlik ve hıza odaklanan bulut tabanlı bir mobil ve masaüstü mesajlaşma uygulamasıdır.", | ||
ko: "Telegram은 보안과 속도에 중점을 둔 클라우드 기반 모바일 및 데스크톱 메시징 앱입니다.", | ||
{ | ||
key: 'origin', | ||
type: ConnectorConfigFormItemType.Text, | ||
required: true, | ||
label: 'Bot Origin', | ||
placeholder: 'https://example.com', | ||
}, | ||
readme: "./README.md", | ||
formItems: [ | ||
{ | ||
key: 'botToken', | ||
type: ConnectorConfigFormItemType.Text, | ||
required: true, | ||
label: 'Telegram Bot Token', | ||
placeholder: 'secret-value', | ||
}, | ||
{ | ||
key: 'origin', | ||
type: ConnectorConfigFormItemType.Text, | ||
required: true, | ||
label: 'Bot Origin', | ||
placeholder: 'https://example.com', | ||
} | ||
] | ||
{ | ||
key: 'replaceCallbackURIDomain', | ||
type: ConnectorConfigFormItemType.Text, | ||
required: false, | ||
label: 'Replace Callback URI with Bot Origin?', | ||
placeholder: 'false', | ||
}, | ||
], | ||
}; | ||
|
||
export const defaultTimeout = 5000; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,126 +1,136 @@ | ||
import { createHash, createHmac } from 'node:crypto'; | ||
|
||
import { | ||
GetAuthorizationUri, | ||
GetUserInfo, | ||
SocialConnector, | ||
CreateConnector, | ||
GetConnectorConfig, | ||
parseJson, | ||
} from '@logto/connector-kit'; | ||
import { | ||
ConnectorError, | ||
ConnectorErrorCodes, | ||
validateConfig, | ||
ConnectorType, | ||
type GetAuthorizationUri, | ||
type GetUserInfo, | ||
type SocialConnector, | ||
type CreateConnector, | ||
type GetConnectorConfig, | ||
parseJson, | ||
ConnectorError, | ||
ConnectorErrorCodes, | ||
validateConfig, | ||
ConnectorType, | ||
} from '@logto/connector-kit'; | ||
|
||
import { authorizationEndpoint, scope, defaultMetadata } from './constant.js'; | ||
import { TelegramConfig, userInfoResponseGuard } from './types.js'; | ||
import { | ||
telegramConfigGuard, | ||
authResponseGuard, | ||
type TelegramConfig, | ||
userInfoResponseGuard, | ||
telegramConfigGuard, | ||
authResponseGuard, | ||
} from './types.js'; | ||
|
||
const getAuthorizationUri = (getConfig: GetConnectorConfig): GetAuthorizationUri => async ({ state, redirectUri }) => { | ||
const getAuthorizationUri = | ||
(getConfig: GetConnectorConfig): GetAuthorizationUri => | ||
async ({ state, redirectUri }) => { | ||
const config = await getConfig(defaultMetadata.id); | ||
validateConfig<TelegramConfig>(config, telegramConfigGuard); | ||
const tokenParts = config.botToken.split(':'); | ||
const botId = tokenParts[0]; | ||
if (!botId) { | ||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, { | ||
error: 'Invalid telegram bot token', | ||
}); | ||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, { | ||
error: 'Invalid telegram bot token', | ||
}); | ||
} | ||
|
||
// reconstruct the return_to url | ||
const returnTo = new URL(redirectUri); | ||
// Reconstruct the return_to url | ||
// eslint-disable-next-line | ||
let returnTo = new URL(redirectUri); | ||
|
||
if (config.replaceCallbackURIDomain) { | ||
// eslint-disable-next-line | ||
returnTo = new URL(returnTo.pathname, config.origin); | ||
} | ||
|
||
// append state to return_to url | ||
// Append state to return_to url | ||
returnTo.searchParams.set('state', state); | ||
|
||
const queryParameters = new URLSearchParams({ | ||
bot_id: botId, | ||
origin: config.origin, | ||
embed: '1', | ||
request_access: scope, | ||
return_to: returnTo.toString(), | ||
bot_id: botId, | ||
origin: config.origin, | ||
embed: '1', | ||
request_access: scope, | ||
return_to: returnTo.toString(), | ||
}); | ||
return `${authorizationEndpoint}?${queryParameters.toString()}`; | ||
}; | ||
}; | ||
|
||
const authorizationCallbackHandler = async (parameterObject: unknown) => { | ||
const result = authResponseGuard.safeParse(parameterObject); | ||
const result = authResponseGuard.safeParse(parameterObject); | ||
|
||
if (!result.success) { | ||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject)); | ||
} | ||
if (!result.success) { | ||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject)); | ||
} | ||
|
||
return result.data; | ||
return result.data; | ||
}; | ||
|
||
const performTelegramIntegrityCheck = ( | ||
telegramRepsonse: Record<string, unknown>, | ||
botToken: string | ||
telegramResponse: Record<string, unknown>, | ||
botToken: string | ||
): boolean => { | ||
const fields: string[] = []; | ||
for (const key of Object.keys(telegramRepsonse)) { | ||
if (key === 'hash') { | ||
continue; | ||
} | ||
const value = String(telegramRepsonse[key]); | ||
const field = key + '=' + value; | ||
fields.push(field); | ||
const fields: string[] = []; | ||
for (const key of Object.keys(telegramResponse)) { | ||
if (key === 'hash') { | ||
continue; | ||
} | ||
const data = fields.sort().join('\n'); | ||
|
||
const botSecretKey = createHash('sha256').update(Buffer.from(botToken)).digest(); | ||
const dataHash = createHmac('sha256', botSecretKey).update(data).digest('hex'); | ||
return dataHash === telegramRepsonse.hash; | ||
const value = String(telegramResponse[key]); | ||
const field = key + '=' + value; | ||
fields.push(field); | ||
} | ||
const data = fields.sort().join('\n'); | ||
|
||
const botSecretKey = createHash('sha256').update(Buffer.from(botToken)).digest(); | ||
const dataHash = createHmac('sha256', botSecretKey).update(data).digest('hex'); | ||
return dataHash === telegramResponse.hash; | ||
}; | ||
|
||
const getUserInfo = (getConfig: GetConnectorConfig): GetUserInfo => async (data) => { | ||
// get tgAuthResult from parameterObject | ||
const getUserInfo = | ||
(getConfig: GetConnectorConfig): GetUserInfo => | ||
async (data) => { | ||
// Get tgAuthResult from parameterObject | ||
const { tgAuthResult } = await authorizationCallbackHandler(data); | ||
|
||
// get config | ||
// Get config | ||
const config = await getConfig(defaultMetadata.id); | ||
|
||
// validate config | ||
// Validate config | ||
validateConfig<TelegramConfig>(config, telegramConfigGuard); | ||
|
||
// decode tgAuthResult from base64 to utf-8 JSON string | ||
// Decode tgAuthResult from base64 to utf-8 JSON string | ||
const decoded = Buffer.from(tgAuthResult, 'base64').toString('utf-8'); | ||
|
||
// parse JSON string to object | ||
// Parse JSON string to object | ||
const parsed = userInfoResponseGuard.safeParse(parseJson(decoded)); | ||
|
||
// validate parsed object | ||
// Validate parsed object | ||
if (!parsed.success) { | ||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, parsed.error); | ||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, parsed.error); | ||
} | ||
const ok = performTelegramIntegrityCheck(parsed.data, config.botToken); | ||
if (!ok) { | ||
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, { | ||
error: 'Telegram data integrity check failed', | ||
}); | ||
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, { | ||
error: 'Telegram data integrity check failed', | ||
}); | ||
} | ||
|
||
return { | ||
id: String(parsed.data.id), | ||
avatar: parsed.data.photo_url || '', | ||
username: parsed.data.username || '', | ||
name: `${parsed.data.first_name} ${parsed.data.last_name}`.trim() || '', | ||
} | ||
}; | ||
id: String(parsed.data.id), | ||
avatar: parsed.data.photo_url || '', | ||
username: parsed.data.username || '', | ||
name: `${parsed.data.first_name} ${parsed.data.last_name}`.trim() || '', | ||
}; | ||
}; | ||
|
||
const createTelegramConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => { | ||
return { | ||
metadata: defaultMetadata, | ||
type: ConnectorType.Social, | ||
configGuard: telegramConfigGuard, | ||
getAuthorizationUri: getAuthorizationUri(getConfig), | ||
getUserInfo: getUserInfo(getConfig), | ||
}; | ||
return { | ||
metadata: defaultMetadata, | ||
type: ConnectorType.Social, | ||
configGuard: telegramConfigGuard, | ||
getAuthorizationUri: getAuthorizationUri(getConfig), | ||
getUserInfo: getUserInfo(getConfig), | ||
}; | ||
}; | ||
|
||
export default createTelegramConnector; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,35 @@ | ||
import { z } from "zod"; | ||
import { z } from 'zod'; | ||
|
||
export const telegramConfigGuard = z.object({ | ||
botToken: z.string(), | ||
origin: z.string(), | ||
botToken: z.string(), | ||
origin: z.string(), | ||
replaceCallbackURIDomain: z.boolean().default(false), | ||
}); | ||
|
||
export type TelegramConfig = z.infer<typeof telegramConfigGuard>; | ||
|
||
// id=<number, userid> | ||
// Id=<number, userid> | ||
// first_name=<string> | ||
// username=<telegram username> | ||
// photo_url=<profile_pic_url, string> | ||
// auth_date=<unix timestamp> | ||
// hash=<hash of the data, useful for checksum validation> | ||
export const userInfoResponseGuard = z.object({ | ||
auth_date: z.number().optional().nullable(), | ||
first_name: z.string().optional().nullable(), | ||
hash: z.string().optional().nullable(), | ||
id: z.number(), | ||
last_name: z.string().optional().nullable(), | ||
photo_url: z.string().optional().nullable(), | ||
username: z.string().optional().nullable(), | ||
auth_date: z.number().optional().nullable(), | ||
first_name: z.string().optional().nullable(), | ||
hash: z.string().optional().nullable(), | ||
id: z.number(), | ||
last_name: z.string().optional().nullable(), | ||
photo_url: z.string().optional().nullable(), | ||
username: z.string().optional().nullable(), | ||
}); | ||
|
||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>; | ||
|
||
export const authorizationCallbackErrorGuard = z.object({ | ||
error: z.string(), | ||
error_description: z.string(), | ||
error_uri: z.string(), | ||
error: z.string(), | ||
error_description: z.string(), | ||
error_uri: z.string(), | ||
}); | ||
|
||
export const authResponseGuard = z.object({ tgAuthResult: z.string() }); |