Skip to content

Commit

Permalink
fix: Add replace Callback URI Domain parameter (#78)
Browse files Browse the repository at this point in the history
* 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
3 people authored Jun 30, 2023
1 parent c540371 commit 8e49758
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 126 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Changelog

## [1.1.2](https://github.com/cheqd/connector-telegram/compare/1.1.1...1.1.2) (2023-05-30)

## [1.1.1-develop.3](https://github.com/cheqd/connector-telegram/compare/1.1.1-develop.2...1.1.1-develop.3) (2023-06-29)

## [1.1.1-develop.2](https://github.com/cheqd/connector-telegram/compare/1.1.1-develop.1...1.1.1-develop.2) (2023-05-30)

## [1.1.1](https://github.com/cheqd/connector-telegram/compare/1.1.0...1.1.1) (2023-05-24)

## [1.1.1-develop.1](https://github.com/cheqd/connector-telegram/compare/1.1.0...1.1.1-develop.1) (2023-05-24)

## [1.1.0](https://github.com/cheqd/connector-telegram/compare/1.0.2...1.1.0) (2023-05-23)
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 44 additions & 38 deletions src/constant.ts
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;
154 changes: 82 additions & 72 deletions src/index.ts
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;
29 changes: 15 additions & 14 deletions src/types.ts
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() });

0 comments on commit 8e49758

Please sign in to comment.