Support Telegram Provider for building Mini Apps #846

Closed
opened 2026-03-13 08:06:50 -05:00 by GiteaMirror · 10 comments
Owner

Originally created by @mi3lix9 on GitHub (Mar 14, 2025).

Is this suited for github?

  • Yes, this is suited for github

Better Auth currently does not support Telegram Mini Apps Authorization, which is required for authenticating users in Telegram Mini Apps. Unlike standard OAuth providers, Telegram uses a unique authorization method that relies on signed user data rather than OAuth tokens.

Telegram Mini Apps require authentication via the telegram.initData parameter, which is sent as part of the app initialization. The app must verify this signed data using Telegram’s public key, as described in the Telegram Mini Apps Authorization Documentation.

Without native support in Better Auth, developers must manually implement this verification process, making integration more complex and inconsistent.

Describe the solution you'd like

Better Auth should implement Telegram Mini Apps Authorization by:

  1. Receiving User Data: When the Mini App is launched, Telegram provides an initData string containing user details and a signature.

  2. Validating the Signature: The backend verifies initData using Telegram’s public key to ensure authenticity.

  3. Issuing an Authentication Token: If the verification succeeds, Better Auth should issue a token, enabling seamless authentication.

This should be integrated into Better Auth like other authentication providers, allowing developers to enable Telegram login with minimal setup.

Describe alternatives you've considered

  • Manually Implementing the Verification: Developers currently have to extract initData, validate it, and manage authentication separately. This adds complexity and security risks if not implemented correctly.
  • Using OAuth: Telegram does not support OAuth, making standard OAuth-based authentication infeasible.

By adding native Telegram support, Better Auth can simplify authentication for Mini Apps.

Additional context

Telegram Mini Apps Authorization

Originally created by @mi3lix9 on GitHub (Mar 14, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. Better Auth currently does not support Telegram Mini Apps Authorization, which is required for authenticating users in Telegram Mini Apps. Unlike standard OAuth providers, Telegram uses a unique authorization method that relies on signed user data rather than OAuth tokens. Telegram Mini Apps require authentication via the telegram.initData parameter, which is sent as part of the app initialization. The app must verify this signed data using Telegram’s public key, as described in the [Telegram Mini Apps Authorization Documentation](https://docs.telegram-mini-apps.com/platform/authorizing-user). Without native support in Better Auth, developers must manually implement this verification process, making integration more complex and inconsistent. ### Describe the solution you'd like Better Auth should implement Telegram Mini Apps Authorization by: 1. Receiving User Data: When the Mini App is launched, Telegram provides an initData string containing user details and a signature. 2. Validating the Signature: The backend verifies initData using Telegram’s public key to ensure authenticity. 3. Issuing an Authentication Token: If the verification succeeds, Better Auth should issue a token, enabling seamless authentication. This should be integrated into Better Auth like other authentication providers, allowing developers to enable Telegram login with minimal setup. ### Describe alternatives you've considered - Manually Implementing the Verification: Developers currently have to extract initData, validate it, and manage authentication separately. This adds complexity and security risks if not implemented correctly. - Using OAuth: Telegram does not support OAuth, making standard OAuth-based authentication infeasible. By adding native Telegram support, Better Auth can simplify authentication for Mini Apps. ### Additional context [Telegram Mini Apps Authorization](https://docs.telegram-mini-apps.com/platform/authorizing-user)
Author
Owner

@mi3lix9 commented on GitHub (Mar 14, 2025):

I will try doing this next month, but I want to discuss how to do it first.
Should we support Telegram auth from the web using the Login Widget?

What should we consider as well?

@mi3lix9 commented on GitHub (Mar 14, 2025): I will try doing this next month, but I want to discuss how to do it first. Should we support Telegram auth from the web using the [Login Widget](https://core.telegram.org/widgets/login)? What should we consider as well?
Author
Owner

@Cyenoch commented on GitHub (Mar 24, 2025):

This might be of some help.

Authorizing User

I will try doing this next month, but I want to discuss how to do it first. Should we support Telegram auth from the web using the Login Widget?

What should we consider as well?

@Cyenoch commented on GitHub (Mar 24, 2025): This might be of some help. [Authorizing User](https://docs.telegram-mini-apps.com/platform/authorizing-user) > I will try doing this next month, but I want to discuss how to do it first. Should we support Telegram auth from the web using the [Login Widget](https://core.telegram.org/widgets/login)? > > What should we consider as well?
Author
Owner

@arshx86 commented on GitHub (May 19, 2025):

I will try doing this next month, but I want to discuss how to do it first. Should we support Telegram auth from the web using the Login Widget?

What should we consider as well?

Yes, although it's not generic OAuth we can try to support login widgets, thus not-only mini app developers but everyone can use it.

@arshx86 commented on GitHub (May 19, 2025): > I will try doing this next month, but I want to discuss how to do it first. Should we support Telegram auth from the web using the [Login Widget](https://core.telegram.org/widgets/login)? > > What should we consider as well? Yes, although it's not generic OAuth we can try to support login widgets, thus not-only mini app developers but everyone can use it.
Author
Owner

@mi3lix9 commented on GitHub (May 20, 2025):

Hello by guys I create a basic code but I have an issue with the cookie, it is always null for some reason.

@mi3lix9 commented on GitHub (May 20, 2025): Hello by guys I create a basic code but I have an issue with the cookie, it is always null for some reason.
Author
Owner

@parmetra commented on GitHub (May 20, 2025):

Hello by guys I create a basic code but I have an issue with the cookie, it is always null for some reason.

Hello! Could you please show your code so we can take a look and see what's going on?

@parmetra commented on GitHub (May 20, 2025): > Hello by guys I create a basic code but I have an issue with the cookie, it is always null for some reason. Hello! Could you please show your code so we can take a look and see what's going on?
Author
Owner

@mi3lix9 commented on GitHub (May 20, 2025):

Hello by guys I create a basic code but I have an issue with the cookie, it is always null for some reason.

Hello! Could you please show your code so we can take a look and see what's going on?

Sure, I'll put the code today and update you

@mi3lix9 commented on GitHub (May 20, 2025): > > Hello by guys I create a basic code but I have an issue with the cookie, it is always null for some reason. > > Hello! Could you please show your code so we can take a look and see what's going on? Sure, I'll put the code today and update you
Author
Owner

@mi3lix9 commented on GitHub (May 20, 2025):

Sorry for being late, this is my initial code below, I assume the following use cases:

  1. Using Telegram Mini App, which is clear
  2. The user my be using the bot directly, so we will generate the hash on the bot's server then send it to better-auth server, which provide one authentication method. I thought about this to be easier to use with tRPC etc... as well.
  3. I didn't implement this but we should support Login Widget as well.

I saw this issue #2196 who face my problem and he implement the code in another way, I think his method works with Login widget.

I used @telegram-apps/init-data-node library to validate and parse the data coming from the request.

For now, only the cookie is not working with me, the rest should be ok.

import { isValid, parse } from "@telegram-apps/init-data-node";
import { APIError, createAuthMiddleware } from "better-auth/api";
import { parseSetCookieHeader } from "better-auth/cookies";
import type { BetterAuthPlugin, User } from "better-auth/types";

interface TelegramMiniAppOptions {
  /**
   * The bot token that Telegram issued for your bot.
   * This is required for validating `initDataRaw`.
   */
  botToken?: string;
}

/**
 * Telegram Mini‑App plugin.
 *
 * * Validates the `tma` header coming from the client.
 * * Creates / finds a user by `telegramId`.
 * * Converts the raw `initData` into a signed session cookie
 *   so Better‑Auth can authorize downstream requests.
 *
 * The plugin **does not** create an `account` record or a new
 * Better‑Auth session row; it simply re‑uses the signed cookie
 * mechanism in the same spirit as the built‑in `bearer` plugin.
 */
export const telegramAuth = (
  opts: TelegramMiniAppOptions = { botToken: process.env.BOT_TOKEN! }
): BetterAuthPlugin => {
  if (!opts?.botToken) {
    throw new Error("telegramMiniApp plugin requires botToken");
  }

  const prefix = "tma ";

  return {
    id: "telegram-miniapp",

    // Adds `telegramId` to the user schema if it does not exist.
    schema: {
      user: {
        fields: {
          telegramId: { type: "string", unique: true },
        },
      },
    },

    hooks: {
      before: [
        {
          matcher(ctx) {
            const h =
              ctx.request?.headers.get("authorization") ??
              ctx.headers?.get("authorization");
            return Boolean(h?.toLowerCase().startsWith(prefix));
          },

          handler: createAuthMiddleware(async (ctx) => {
            /* ── 1. READ & VALIDATE HEADER ─────────────────────────── */
            const rawHeader =
              ctx.request?.headers.get("authorization") ??
              ctx.headers?.get("authorization")!;

            const initDataRaw = rawHeader.slice(prefix.length).trim();

            if (!isValid(initDataRaw, opts.botToken!)) {
              throw new APIError("UNAUTHORIZED", { message: "INVALID_TOKEN" });
            }

            /* ── 2. PARSE USER DATA ────────────────────────────────── */
            const data = parse(initDataRaw);
            const tg = data.user ?? data.receiver;

            if (!tg?.id) {
              throw new APIError("UNPROCESSABLE_ENTITY", {
                message: "INVALID_TOKEN",
              });
            }

            /* ── 3. UPSERT USER (no Account/Session creation) ──────── */
            let user = await ctx.context.adapter.findOne<User>({
              model: "user",
              where: [{ field: "telegramId", value: tg.id.toString() }],
            });

            if (!user) {
              user = await ctx.context.internalAdapter.createUser({
                name: [tg.firstName, tg.lastName].filter(Boolean).join(" "),
                email: `${tg.id}@telegram.com`,
                telegramId: tg.id.toString(),
              });
            }

            const cookie = ctx.context.createAuthCookie("session_token", {
              maxAge: 60,
              httpOnly: true,
              sameSite: "none",
            });

            const signedCookie = await ctx.setSignedCookie(
              cookie.name,
              initDataRaw,
              ctx.context.secret,
              cookie.attributes
            );
            ctx.setCookie(cookie.name, signedCookie, cookie.attributes);
            ctx.headers?.set("hello", "world");
            ctx.headers?.set("set-cookie", signedCookie);
          }),
        },
      ],
    },
  };
};
@mi3lix9 commented on GitHub (May 20, 2025): Sorry for being late, this is my initial code below, I assume the following use cases: 1. Using Telegram Mini App, which is clear 2. The user my be using the bot directly, so we will generate the hash on the bot's server then send it to better-auth server, which provide one authentication method. I thought about this to be easier to use with tRPC etc... as well. 3. I didn't implement this but we should support Login Widget as well. I saw this issue #2196 who face my problem and he implement the code in another way, I think his method works with Login widget. I used `@telegram-apps/init-data-node` library to validate and parse the data coming from the request. For now, only the cookie is not working with me, the rest should be ok. ``` import { isValid, parse } from "@telegram-apps/init-data-node"; import { APIError, createAuthMiddleware } from "better-auth/api"; import { parseSetCookieHeader } from "better-auth/cookies"; import type { BetterAuthPlugin, User } from "better-auth/types"; interface TelegramMiniAppOptions { /** * The bot token that Telegram issued for your bot. * This is required for validating `initDataRaw`. */ botToken?: string; } /** * Telegram Mini‑App plugin. * * * Validates the `tma` header coming from the client. * * Creates / finds a user by `telegramId`. * * Converts the raw `initData` into a signed session cookie * so Better‑Auth can authorize downstream requests. * * The plugin **does not** create an `account` record or a new * Better‑Auth session row; it simply re‑uses the signed cookie * mechanism in the same spirit as the built‑in `bearer` plugin. */ export const telegramAuth = ( opts: TelegramMiniAppOptions = { botToken: process.env.BOT_TOKEN! } ): BetterAuthPlugin => { if (!opts?.botToken) { throw new Error("telegramMiniApp plugin requires botToken"); } const prefix = "tma "; return { id: "telegram-miniapp", // Adds `telegramId` to the user schema if it does not exist. schema: { user: { fields: { telegramId: { type: "string", unique: true }, }, }, }, hooks: { before: [ { matcher(ctx) { const h = ctx.request?.headers.get("authorization") ?? ctx.headers?.get("authorization"); return Boolean(h?.toLowerCase().startsWith(prefix)); }, handler: createAuthMiddleware(async (ctx) => { /* ── 1. READ & VALIDATE HEADER ─────────────────────────── */ const rawHeader = ctx.request?.headers.get("authorization") ?? ctx.headers?.get("authorization")!; const initDataRaw = rawHeader.slice(prefix.length).trim(); if (!isValid(initDataRaw, opts.botToken!)) { throw new APIError("UNAUTHORIZED", { message: "INVALID_TOKEN" }); } /* ── 2. PARSE USER DATA ────────────────────────────────── */ const data = parse(initDataRaw); const tg = data.user ?? data.receiver; if (!tg?.id) { throw new APIError("UNPROCESSABLE_ENTITY", { message: "INVALID_TOKEN", }); } /* ── 3. UPSERT USER (no Account/Session creation) ──────── */ let user = await ctx.context.adapter.findOne<User>({ model: "user", where: [{ field: "telegramId", value: tg.id.toString() }], }); if (!user) { user = await ctx.context.internalAdapter.createUser({ name: [tg.firstName, tg.lastName].filter(Boolean).join(" "), email: `${tg.id}@telegram.com`, telegramId: tg.id.toString(), }); } const cookie = ctx.context.createAuthCookie("session_token", { maxAge: 60, httpOnly: true, sameSite: "none", }); const signedCookie = await ctx.setSignedCookie( cookie.name, initDataRaw, ctx.context.secret, cookie.attributes ); ctx.setCookie(cookie.name, signedCookie, cookie.attributes); ctx.headers?.set("hello", "world"); ctx.headers?.set("set-cookie", signedCookie); }), }, ], }, }; }; ```
Author
Owner

@iatomic1 commented on GitHub (Jul 17, 2025):

Sorry for being late, this is my initial code below, I assume the following use cases:

  1. Using Telegram Mini App, which is clear
  2. The user my be using the bot directly, so we will generate the hash on the bot's server then send it to better-auth server, which provide one authentication method. I thought about this to be easier to use with tRPC etc... as well.
  3. I didn't implement this but we should support Login Widget as well.

I saw this issue #2196 who face my problem and he implement the code in another way, I think his method works with Login widget.

I used @telegram-apps/init-data-node library to validate and parse the data coming from the request.

For now, only the cookie is not working with me, the rest should be ok.

import { isValid, parse } from "@telegram-apps/init-data-node";
import { APIError, createAuthMiddleware } from "better-auth/api";
import { parseSetCookieHeader } from "better-auth/cookies";
import type { BetterAuthPlugin, User } from "better-auth/types";

interface TelegramMiniAppOptions {
  /**
   * The bot token that Telegram issued for your bot.
   * This is required for validating `initDataRaw`.
   */
  botToken?: string;
}

/**
 * Telegram Mini‑App plugin.
 *
 * * Validates the `tma` header coming from the client.
 * * Creates / finds a user by `telegramId`.
 * * Converts the raw `initData` into a signed session cookie
 *   so Better‑Auth can authorize downstream requests.
 *
 * The plugin **does not** create an `account` record or a new
 * Better‑Auth session row; it simply re‑uses the signed cookie
 * mechanism in the same spirit as the built‑in `bearer` plugin.
 */
export const telegramAuth = (
  opts: TelegramMiniAppOptions = { botToken: process.env.BOT_TOKEN! }
): BetterAuthPlugin => {
  if (!opts?.botToken) {
    throw new Error("telegramMiniApp plugin requires botToken");
  }

  const prefix = "tma ";

  return {
    id: "telegram-miniapp",

    // Adds `telegramId` to the user schema if it does not exist.
    schema: {
      user: {
        fields: {
          telegramId: { type: "string", unique: true },
        },
      },
    },

    hooks: {
      before: [
        {
          matcher(ctx) {
            const h =
              ctx.request?.headers.get("authorization") ??
              ctx.headers?.get("authorization");
            return Boolean(h?.toLowerCase().startsWith(prefix));
          },

          handler: createAuthMiddleware(async (ctx) => {
            /* ── 1. READ & VALIDATE HEADER ─────────────────────────── */
            const rawHeader =
              ctx.request?.headers.get("authorization") ??
              ctx.headers?.get("authorization")!;

            const initDataRaw = rawHeader.slice(prefix.length).trim();

            if (!isValid(initDataRaw, opts.botToken!)) {
              throw new APIError("UNAUTHORIZED", { message: "INVALID_TOKEN" });
            }

            /* ── 2. PARSE USER DATA ────────────────────────────────── */
            const data = parse(initDataRaw);
            const tg = data.user ?? data.receiver;

            if (!tg?.id) {
              throw new APIError("UNPROCESSABLE_ENTITY", {
                message: "INVALID_TOKEN",
              });
            }

            /* ── 3. UPSERT USER (no Account/Session creation) ──────── */
            let user = await ctx.context.adapter.findOne<User>({
              model: "user",
              where: [{ field: "telegramId", value: tg.id.toString() }],
            });

            if (!user) {
              user = await ctx.context.internalAdapter.createUser({
                name: [tg.firstName, tg.lastName].filter(Boolean).join(" "),
                email: `${tg.id}@telegram.com`,
                telegramId: tg.id.toString(),
              });
            }

            const cookie = ctx.context.createAuthCookie("session_token", {
              maxAge: 60,
              httpOnly: true,
              sameSite: "none",
            });

            const signedCookie = await ctx.setSignedCookie(
              cookie.name,
              initDataRaw,
              ctx.context.secret,
              cookie.attributes
            );
            ctx.setCookie(cookie.name, signedCookie, cookie.attributes);
            ctx.headers?.set("hello", "world");
            ctx.headers?.set("set-cookie", signedCookie);
          }),
        },
      ],
    },
  };
};

Hey @mi3lix9, would this work for my use case?

  • User uses the /login /signup command for signing up and logging in
  • Then we use the norms getting session and all on each event the user does to know if they are logged in

Basically, I'm just building a bot, no mini-app

@iatomic1 commented on GitHub (Jul 17, 2025): > Sorry for being late, this is my initial code below, I assume the following use cases: > > 1. Using Telegram Mini App, which is clear > 2. The user my be using the bot directly, so we will generate the hash on the bot's server then send it to better-auth server, which provide one authentication method. I thought about this to be easier to use with tRPC etc... as well. > 3. I didn't implement this but we should support Login Widget as well. > > I saw this issue [#2196](https://github.com/better-auth/better-auth/issues/2196) who face my problem and he implement the code in another way, I think his method works with Login widget. > > I used `@telegram-apps/init-data-node` library to validate and parse the data coming from the request. > > For now, only the cookie is not working with me, the rest should be ok. > > ``` > import { isValid, parse } from "@telegram-apps/init-data-node"; > import { APIError, createAuthMiddleware } from "better-auth/api"; > import { parseSetCookieHeader } from "better-auth/cookies"; > import type { BetterAuthPlugin, User } from "better-auth/types"; > > interface TelegramMiniAppOptions { > /** > * The bot token that Telegram issued for your bot. > * This is required for validating `initDataRaw`. > */ > botToken?: string; > } > > /** > * Telegram Mini‑App plugin. > * > * * Validates the `tma` header coming from the client. > * * Creates / finds a user by `telegramId`. > * * Converts the raw `initData` into a signed session cookie > * so Better‑Auth can authorize downstream requests. > * > * The plugin **does not** create an `account` record or a new > * Better‑Auth session row; it simply re‑uses the signed cookie > * mechanism in the same spirit as the built‑in `bearer` plugin. > */ > export const telegramAuth = ( > opts: TelegramMiniAppOptions = { botToken: process.env.BOT_TOKEN! } > ): BetterAuthPlugin => { > if (!opts?.botToken) { > throw new Error("telegramMiniApp plugin requires botToken"); > } > > const prefix = "tma "; > > return { > id: "telegram-miniapp", > > // Adds `telegramId` to the user schema if it does not exist. > schema: { > user: { > fields: { > telegramId: { type: "string", unique: true }, > }, > }, > }, > > hooks: { > before: [ > { > matcher(ctx) { > const h = > ctx.request?.headers.get("authorization") ?? > ctx.headers?.get("authorization"); > return Boolean(h?.toLowerCase().startsWith(prefix)); > }, > > handler: createAuthMiddleware(async (ctx) => { > /* ── 1. READ & VALIDATE HEADER ─────────────────────────── */ > const rawHeader = > ctx.request?.headers.get("authorization") ?? > ctx.headers?.get("authorization")!; > > const initDataRaw = rawHeader.slice(prefix.length).trim(); > > if (!isValid(initDataRaw, opts.botToken!)) { > throw new APIError("UNAUTHORIZED", { message: "INVALID_TOKEN" }); > } > > /* ── 2. PARSE USER DATA ────────────────────────────────── */ > const data = parse(initDataRaw); > const tg = data.user ?? data.receiver; > > if (!tg?.id) { > throw new APIError("UNPROCESSABLE_ENTITY", { > message: "INVALID_TOKEN", > }); > } > > /* ── 3. UPSERT USER (no Account/Session creation) ──────── */ > let user = await ctx.context.adapter.findOne<User>({ > model: "user", > where: [{ field: "telegramId", value: tg.id.toString() }], > }); > > if (!user) { > user = await ctx.context.internalAdapter.createUser({ > name: [tg.firstName, tg.lastName].filter(Boolean).join(" "), > email: `${tg.id}@telegram.com`, > telegramId: tg.id.toString(), > }); > } > > const cookie = ctx.context.createAuthCookie("session_token", { > maxAge: 60, > httpOnly: true, > sameSite: "none", > }); > > const signedCookie = await ctx.setSignedCookie( > cookie.name, > initDataRaw, > ctx.context.secret, > cookie.attributes > ); > ctx.setCookie(cookie.name, signedCookie, cookie.attributes); > ctx.headers?.set("hello", "world"); > ctx.headers?.set("set-cookie", signedCookie); > }), > }, > ], > }, > }; > }; > ``` Hey @mi3lix9, would this work for my use case? - User uses the /login /signup command for signing up and logging in - Then we use the norms getting session and all on each event the user does to know if they are logged in Basically, I'm just building a bot, no mini-app
Author
Owner

@mi3lix9 commented on GitHub (Jul 17, 2025):

Sorry for being late, this is my initial code below, I assume the following use cases:

  1. Using Telegram Mini App, which is clear
  2. The user my be using the bot directly, so we will generate the hash on the bot's server then send it to better-auth server, which provide one authentication method. I thought about this to be easier to use with tRPC etc... as well.
  3. I didn't implement this but we should support Login Widget as well.

I saw this issue #2196 who face my problem and he implement the code in another way, I think his method works with Login widget.
I used @telegram-apps/init-data-node library to validate and parse the data coming from the request.
For now, only the cookie is not working with me, the rest should be ok.

import { isValid, parse } from "@telegram-apps/init-data-node";
import { APIError, createAuthMiddleware } from "better-auth/api";
import { parseSetCookieHeader } from "better-auth/cookies";
import type { BetterAuthPlugin, User } from "better-auth/types";

interface TelegramMiniAppOptions {
  /**
   * The bot token that Telegram issued for your bot.
   * This is required for validating `initDataRaw`.
   */
  botToken?: string;
}

/**
 * Telegram Mini‑App plugin.
 *
 * * Validates the `tma` header coming from the client.
 * * Creates / finds a user by `telegramId`.
 * * Converts the raw `initData` into a signed session cookie
 *   so Better‑Auth can authorize downstream requests.
 *
 * The plugin **does not** create an `account` record or a new
 * Better‑Auth session row; it simply re‑uses the signed cookie
 * mechanism in the same spirit as the built‑in `bearer` plugin.
 */
export const telegramAuth = (
  opts: TelegramMiniAppOptions = { botToken: process.env.BOT_TOKEN! }
): BetterAuthPlugin => {
  if (!opts?.botToken) {
    throw new Error("telegramMiniApp plugin requires botToken");
  }

  const prefix = "tma ";

  return {
    id: "telegram-miniapp",

    // Adds `telegramId` to the user schema if it does not exist.
    schema: {
      user: {
        fields: {
          telegramId: { type: "string", unique: true },
        },
      },
    },

    hooks: {
      before: [
        {
          matcher(ctx) {
            const h =
              ctx.request?.headers.get("authorization") ??
              ctx.headers?.get("authorization");
            return Boolean(h?.toLowerCase().startsWith(prefix));
          },

          handler: createAuthMiddleware(async (ctx) => {
            /* ── 1. READ & VALIDATE HEADER ─────────────────────────── */
            const rawHeader =
              ctx.request?.headers.get("authorization") ??
              ctx.headers?.get("authorization")!;

            const initDataRaw = rawHeader.slice(prefix.length).trim();

            if (!isValid(initDataRaw, opts.botToken!)) {
              throw new APIError("UNAUTHORIZED", { message: "INVALID_TOKEN" });
            }

            /* ── 2. PARSE USER DATA ────────────────────────────────── */
            const data = parse(initDataRaw);
            const tg = data.user ?? data.receiver;

            if (!tg?.id) {
              throw new APIError("UNPROCESSABLE_ENTITY", {
                message: "INVALID_TOKEN",
              });
            }

            /* ── 3. UPSERT USER (no Account/Session creation) ──────── */
            let user = await ctx.context.adapter.findOne<User>({
              model: "user",
              where: [{ field: "telegramId", value: tg.id.toString() }],
            });

            if (!user) {
              user = await ctx.context.internalAdapter.createUser({
                name: [tg.firstName, tg.lastName].filter(Boolean).join(" "),
                email: `${tg.id}@telegram.com`,
                telegramId: tg.id.toString(),
              });
            }

            const cookie = ctx.context.createAuthCookie("session_token", {
              maxAge: 60,
              httpOnly: true,
              sameSite: "none",
            });

            const signedCookie = await ctx.setSignedCookie(
              cookie.name,
              initDataRaw,
              ctx.context.secret,
              cookie.attributes
            );
            ctx.setCookie(cookie.name, signedCookie, cookie.attributes);
            ctx.headers?.set("hello", "world");
            ctx.headers?.set("set-cookie", signedCookie);
          }),
        },
      ],
    },
  };
};

Hey @mi3lix9, would this work for my use case?

* User uses the /login /signup command for signing up and logging in

* Then we use the norms getting session and all on each event the user does to know if they are logged in

Basically, I'm just building a bot, no mini-app

Actually I forget what I did, if it is working for mini app it should work for bots as well, but you need to pass the initData

@mi3lix9 commented on GitHub (Jul 17, 2025): > > Sorry for being late, this is my initial code below, I assume the following use cases: > > > > 1. Using Telegram Mini App, which is clear > > 2. The user my be using the bot directly, so we will generate the hash on the bot's server then send it to better-auth server, which provide one authentication method. I thought about this to be easier to use with tRPC etc... as well. > > 3. I didn't implement this but we should support Login Widget as well. > > > > I saw this issue [#2196](https://github.com/better-auth/better-auth/issues/2196) who face my problem and he implement the code in another way, I think his method works with Login widget. > > I used `@telegram-apps/init-data-node` library to validate and parse the data coming from the request. > > For now, only the cookie is not working with me, the rest should be ok. > > ``` > > import { isValid, parse } from "@telegram-apps/init-data-node"; > > import { APIError, createAuthMiddleware } from "better-auth/api"; > > import { parseSetCookieHeader } from "better-auth/cookies"; > > import type { BetterAuthPlugin, User } from "better-auth/types"; > > > > interface TelegramMiniAppOptions { > > /** > > * The bot token that Telegram issued for your bot. > > * This is required for validating `initDataRaw`. > > */ > > botToken?: string; > > } > > > > /** > > * Telegram Mini‑App plugin. > > * > > * * Validates the `tma` header coming from the client. > > * * Creates / finds a user by `telegramId`. > > * * Converts the raw `initData` into a signed session cookie > > * so Better‑Auth can authorize downstream requests. > > * > > * The plugin **does not** create an `account` record or a new > > * Better‑Auth session row; it simply re‑uses the signed cookie > > * mechanism in the same spirit as the built‑in `bearer` plugin. > > */ > > export const telegramAuth = ( > > opts: TelegramMiniAppOptions = { botToken: process.env.BOT_TOKEN! } > > ): BetterAuthPlugin => { > > if (!opts?.botToken) { > > throw new Error("telegramMiniApp plugin requires botToken"); > > } > > > > const prefix = "tma "; > > > > return { > > id: "telegram-miniapp", > > > > // Adds `telegramId` to the user schema if it does not exist. > > schema: { > > user: { > > fields: { > > telegramId: { type: "string", unique: true }, > > }, > > }, > > }, > > > > hooks: { > > before: [ > > { > > matcher(ctx) { > > const h = > > ctx.request?.headers.get("authorization") ?? > > ctx.headers?.get("authorization"); > > return Boolean(h?.toLowerCase().startsWith(prefix)); > > }, > > > > handler: createAuthMiddleware(async (ctx) => { > > /* ── 1. READ & VALIDATE HEADER ─────────────────────────── */ > > const rawHeader = > > ctx.request?.headers.get("authorization") ?? > > ctx.headers?.get("authorization")!; > > > > const initDataRaw = rawHeader.slice(prefix.length).trim(); > > > > if (!isValid(initDataRaw, opts.botToken!)) { > > throw new APIError("UNAUTHORIZED", { message: "INVALID_TOKEN" }); > > } > > > > /* ── 2. PARSE USER DATA ────────────────────────────────── */ > > const data = parse(initDataRaw); > > const tg = data.user ?? data.receiver; > > > > if (!tg?.id) { > > throw new APIError("UNPROCESSABLE_ENTITY", { > > message: "INVALID_TOKEN", > > }); > > } > > > > /* ── 3. UPSERT USER (no Account/Session creation) ──────── */ > > let user = await ctx.context.adapter.findOne<User>({ > > model: "user", > > where: [{ field: "telegramId", value: tg.id.toString() }], > > }); > > > > if (!user) { > > user = await ctx.context.internalAdapter.createUser({ > > name: [tg.firstName, tg.lastName].filter(Boolean).join(" "), > > email: `${tg.id}@telegram.com`, > > telegramId: tg.id.toString(), > > }); > > } > > > > const cookie = ctx.context.createAuthCookie("session_token", { > > maxAge: 60, > > httpOnly: true, > > sameSite: "none", > > }); > > > > const signedCookie = await ctx.setSignedCookie( > > cookie.name, > > initDataRaw, > > ctx.context.secret, > > cookie.attributes > > ); > > ctx.setCookie(cookie.name, signedCookie, cookie.attributes); > > ctx.headers?.set("hello", "world"); > > ctx.headers?.set("set-cookie", signedCookie); > > }), > > }, > > ], > > }, > > }; > > }; > > ``` > > Hey [@mi3lix9](https://github.com/mi3lix9), would this work for my use case? > > * User uses the /login /signup command for signing up and logging in > > * Then we use the norms getting session and all on each event the user does to know if they are logged in > > > Basically, I'm just building a bot, no mini-app Actually I forget what I did, if it is working for mini app it should work for bots as well, but you need to pass the initData
Author
Owner

@ping-maxwell commented on GitHub (Sep 30, 2025):

Hello all, we likely won't support this natively. I recommend creating a custom plugin to achieve this flow.

@ping-maxwell commented on GitHub (Sep 30, 2025): Hello all, we likely won't support this natively. I recommend creating a custom plugin to achieve this flow.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#846