diff --git a/src/handlers/HandlerFactory.ts b/src/handlers/HandlerFactory.ts index 09627f24..867f0870 100644 --- a/src/handlers/HandlerFactory.ts +++ b/src/handlers/HandlerFactory.ts @@ -7,6 +7,7 @@ import JwtHandler from "./JwtHandler"; import MockLoginHandler from "./MockLoginHandler"; import PasskeysHandler from "./PasskeysHandler"; import PasswordlessHandler from "./PasswordlessHandler"; +import TelegramHandler from "./TelegramHandler"; import TwitchHandler from "./TwitchHandler"; import Web3AuthPasswordlessHandler from "./Web3AuthPasswordlessHandler"; @@ -19,6 +20,8 @@ const createHandler = (params: CreateHandlerParams): ILoginHandler => { switch (typeOfLogin) { case LOGIN.GOOGLE: return new GoogleHandler(params); + case LOGIN.TELEGRAM: + return new TelegramHandler(params); case LOGIN.FACEBOOK: return new FacebookHandler(params); case LOGIN.TWITCH: diff --git a/src/handlers/TelegramHandler.ts b/src/handlers/TelegramHandler.ts new file mode 100644 index 00000000..35b5fd45 --- /dev/null +++ b/src/handlers/TelegramHandler.ts @@ -0,0 +1,146 @@ +import deepmerge from "deepmerge"; + +import { UX_MODE } from "../utils/enums"; +import { broadcastChannelOptions, getTimeout } from "../utils/helpers"; +import log from "../utils/loglevel"; +import PopupHandler from "../utils/PopupHandler"; +import AbstractLoginHandler from "./AbstractLoginHandler"; +import { CreateHandlerParams, LoginWindowResponse, TorusVerifierResponse } from "./interfaces"; + +type PopupResponse = { + result: { + auth_date: number; + first_name: string; + hash: string; + id: number; + last_name: string; + photo_url: string; + username: string; + }; + origin: string; + event: string; +}; + +export default class TelegramHandler extends AbstractLoginHandler { + private readonly RESPONSE_TYPE: string = "token"; + + private readonly SCOPE: string = "profile"; + + private readonly PROMPT: string = "select_account"; + + constructor(params: CreateHandlerParams) { + super(params); + this.setFinalUrl(); + } + + setFinalUrl(): void { + const finalUrl = new URL("https://oauth.telegram.org/auth"); + const clonedParams = JSON.parse(JSON.stringify(this.params.jwtParams || {})); + clonedParams.origin = `${this.params.redirect_uri}?state=${this.state}&nonce=${this.nonce}`; + + const finalJwtParams = deepmerge( + { + state: this.state, + response_type: this.RESPONSE_TYPE, + bot_id: this.params.clientId, + prompt: this.PROMPT, + redirect_uri: `${this.params.redirect_uri}?state=${this.state}&nonce=${this.nonce}`, + scope: this.SCOPE, + nonce: this.nonce, + }, + clonedParams + ); + Object.keys(finalJwtParams).forEach((key: string) => { + const localKey = key as keyof typeof finalJwtParams; + if (finalJwtParams[localKey]) finalUrl.searchParams.append(localKey, finalJwtParams[localKey]); + }); + this.finalURL = finalUrl; + } + + objectToAuthDataMap(tgAuthenticationResulr: string) { + return JSON.parse(atob(tgAuthenticationResulr)) as { first_name: string; last_name: string; photo_url: string; username: string }; + } + + async getUserInfo(params: LoginWindowResponse): Promise { + const { tgAuthResult } = params; + const userInfo = this.objectToAuthDataMap(tgAuthResult); + const { photo_url: profileImage = "", username = "", first_name = "", last_name = "" } = userInfo; + return { + email: "", // Telegram does not provide email + name: `${first_name} ${last_name}`, + profileImage, + verifier: this.params.verifier, + verifierId: username.toLowerCase(), + typeOfLogin: this.params.typeOfLogin, + }; + } + + async handleLoginWindow(params: { locationReplaceOnRedirect?: boolean; popupFeatures?: string }): Promise { + const verifierWindow = new PopupHandler({ url: this.finalURL, features: params.popupFeatures, timeout: getTimeout(this.params.typeOfLogin) }); + if (this.params.uxMode === UX_MODE.REDIRECT) { + verifierWindow.redirect(params.locationReplaceOnRedirect); + } else { + const { BroadcastChannel } = await import("@toruslabs/broadcast-channel"); + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let bc: any; + const handleData = async (ev: string) => { + try { + const { event, origin, result, ...rest } = (JSON.parse(ev) as PopupResponse) || {}; + // 1. Parse URL + const parsedUrl = new URL(origin); + // 2. Get state param + const stateParam = parsedUrl.searchParams.get("state"); + + if (event && event === "auth_result") { + if (!this.params.redirectToOpener && bc) await bc.postMessage({ success: true }); + // properly resolve the data + resolve({ + accessToken: "", + tgAuthResult: btoa(JSON.stringify(result)) || "", + ...rest, + // State has to be last here otherwise it will be overwritten + state: atob(stateParam) as unknown as { [key: string]: string }, + }); + } + } catch (error) { + log.error(error); + reject(error); + } + }; + + if (!this.params.redirectToOpener) { + bc = new BroadcastChannel<{ + error: string; + data: PopupResponse; + }>(`redirect_channel_${this.nonce}`, broadcastChannelOptions); + bc.addEventListener("message", async (ev: string) => { + await handleData(ev); + bc.close(); + verifierWindow.close(); + }); + } + const postMessageEventHandler = async (postMessageEvent: MessageEvent) => { + if (!postMessageEvent.data) return; + const ev = postMessageEvent.data; + window.removeEventListener("message", postMessageEventHandler); + handleData(ev); + verifierWindow.close(); + }; + window.addEventListener("message", postMessageEventHandler); + try { + verifierWindow.open(); + } catch (error) { + log.error(error); + reject(error); + return; + } + verifierWindow.once("close", () => { + if (bc) bc.close(); + reject(new Error("user closed popup")); + }); + }); + } + return null; + } +} diff --git a/src/handlers/interfaces.ts b/src/handlers/interfaces.ts index 0754cb43..8f87e776 100644 --- a/src/handlers/interfaces.ts +++ b/src/handlers/interfaces.ts @@ -58,6 +58,7 @@ export interface TorusSubVerifierInfo { export interface LoginWindowResponse { accessToken: string; idToken?: string; + tgAuthResult?: string; ref?: string; extraParams?: string; extraParamsPassed?: string; diff --git a/src/login.ts b/src/login.ts index 95215df5..a4834fc3 100644 --- a/src/login.ts +++ b/src/login.ts @@ -187,7 +187,7 @@ class CustomAuth { verifier, userInfo.verifierId, { verifier_id: userInfo.verifierId }, - loginParams.idToken || loginParams.accessToken, + loginParams.idToken || loginParams.accessToken || loginParams.tgAuthResult, userInfo.extraVerifierParams ); return { diff --git a/src/utils/enums.ts b/src/utils/enums.ts index 6d4e95f7..c12764b6 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -1,5 +1,6 @@ export const LOGIN = { GOOGLE: "google", + TELEGRAM: "telegram", FACEBOOK: "facebook", REDDIT: "reddit", DISCORD: "discord",