JavaScript에서 Telegram WebApp 인증

Dec 09 2022
오늘은 Telegram WebApp Bot의 까다로운 인증 메커니즘에 대해 말씀드리겠습니다. Telegram WebApp 봇이란 무엇입니까? Telegram 내부의 웹사이트에서 WebView를 실행하는 기능일 뿐입니다.

오늘은 Telegram WebApp Bot의 까다로운 인증 메커니즘에 대해 말씀드리겠습니다. Telegram WebApp 봇이란 무엇입니까? Telegram 내부의 웹사이트에서 WebView를 실행하는 기능일 뿐입니다. 여기에서 자세한 내용을 읽을 수 있습니다 .

여기서 인증이 필요한 이유는 무엇입니까?

귀하의 웹사이트는 Telegram WebView에서만 열려야 합니다. 그렇다면 누군가 브라우저에서 이 작업을 수행한다면 어떻게 될까요? "해커"는 가짜 사용자 데이터, ID 등을 사용할 수 있습니다. Telegram 외부의 사람들로부터 API를 보호해야 합니다.

어떻게?

Telegram은 HMAC(해시 기반 메시지 인증 코드)를 사용합니다. 따라서 웹사이트에서 Telegram SDK 를 초기화 하면 해당 데이터를 사용하여 사용자를 식별할 수 있습니다. 인증 메커니즘을 단계별로 만들어 보겠습니다.

1단계: 요청을 통해 데이터 전달

Telegram SDK는 사용자 데이터를 앱의 전역 범위로 설정합니다. 사용자, 스마트폰, 색상 테마 등에 대한 데이터가 있습니다. 여기에서 찾을 수 있습니다.

window.Telegram.WebApp

const { initData } = window.Telegram.WebApp

auth_date=<auth_date>&query_id=<query_id>&user=<user>&hash=<hash>

axios.defaults.headers.common['Telegram-Data'] = window?.Telegram?.WebApp?.initData;

2단계: 인증 미들웨어 생성

내 프로젝트에서 Nest.js를 사용하지만 미들웨어를 만드는 방법은 Express.js와 Nest.js에서 거의 동일합니다.

먼저 몇 줄의 코드로 미들웨어를 만들어야 합니다.

export function telegramAuthMiddleware(req, res, next) {
    // take initData from headers
    const iniData = req.headers[
      'telegram-data'
      ];
    // use our helpers (see bellow) to validate string
    // and get user from it
    const user = checkAuthorization(iniData);

    // add uses to  the request "context" for the future
    if (user) {
      req.user = user;
      next();
    // or if the validation is failed response 401
    } else {
      res.writeHead(401, { 'content-type': 'application/json' });
      res.write('unauthorized');
      res.end();
    }
}

3단계: initData 구문 분석

프로세스를 설명하고 코드를 보여드리겠습니다.

  1. initData 문자열 을 구문 분석해야 합니다.
  2. 해당 문자열에서 해시 필드를 가져와 미래를 위해 보관 하십시오 .
  3. 나머지 필드를 알파벳순으로 정렬
  4. 줄 바꿈기(\n)를 사용하여 이러한 필드를 결합하십시오. 왜요? 으니까! 텔레그램이 원한다!

코드를 살펴보겠습니다.

function parseAuthString(iniData) {
  // parse string to get params
  const searchParams = new URLSearchParams(iniData);
  
  // take the hash and remove it from params list
  const hash = searchParams.get('hash');
  searchParams.delete('hash');

  // sort params
  const restKeys = Array.from(searchParams.entries());
  restKeys.sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey));

  // and join it with \n
  const dataCheckString = restKeys.map(([n, v]) => `${n}=${v}`).join('\n');

  
  return {
    dataCheckString,
    hash,
    // get metaData from params
    metaData: {
      user: JSON.parse(searchParams.get('user')),
      auth_date: searchParams.get('auth_date'),
      query_id: searchParams.get('query_id'),
    },
  };
}

이것이 우리 여행의 마지막 장입니다. 이전 단계의 함수와 약간의 암호화를 사용하여 initData 를 구문 분석해야 합니다 .

다음 경로를 따라야 합니다.

  1. sh256 알고리즘과 를 사용하여 메시지를 인코딩하는 함수 작성
  2. 이전 단계의 함수를 사용하여 문자열 구문 분석
  3. "WebAppData" 키로 텔레그램 봇 토큰을 인코딩하여 비밀 키 생성
  4. 이전 스템의 dataCheckString을 비밀 키로 인코딩하여 유효성 검사 해시를 생성합니다.
  5. 검증 해시를 initData 의 해시와 비교

const crypto = require('crypto')

const WEB_APP_DATA_CONST = "WebAppData"
const TELEGRAM_BOT_TOKEN = "so secret token!!"

// encoding message with key
// we need two types of representation here: Buffer and Hex 
function encodeHmac(message, key, repr=undefined) {
  return crypto.createHmac('sha256', key).update(message).digest(repr);
}

function checkAuthorization(iniData){
  // parsing the iniData sting
  const authTelegramData = parseAuthString(iniData);

  // creating the secret key and keep it as a Buffer (important!)
  const secretKey = encodeHmac(
    TELEGRAM_BOT_TOKEN,
    WEB_APP_DATA_CONST,
  );

  // creating the validation key (and transform it to HEX)
  const validationKey = encodeHmac(
    authTelegramData.dataCheckString,
    secretKey,
    'hex',
  );

  // the final step - comparing and returning
  if (validationKey === authTelegramData.hash) {
    return authTelegramData.metaData.user;
  }

  return null;
}

const crypto = require('crypto')
const WEB_APP_DATA_CONST = "WebAppData"
const TELEGRAM_BOT_TOKEN = "so secret token!!"

export function telegramAuthMiddleware(req, res, next) {
  // take initData from headers
  const iniData = req.headers[
    'telegram-data'
    ];
  // use our helpers (see bellow) to validate string
  // and get user from it
  const user = checkAuthorization(iniData);

  // add uses to  the request "context" for the future
  if (user) {
    req.user = user;
    next();
    // or if the validation is failed response 401
  } else {
    res.writeHead(401, { 'content-type': 'application/json' });
    res.write('unauthorized');
    res.end();
  }
}

function parseAuthString(iniData) {
  // parse string to get params
  const searchParams = new URLSearchParams(iniData);

  // take the hash and remove it from params list
  const hash = searchParams.get('hash');
  searchParams.delete('hash');

  // sort params
  const restKeys = Array.from(searchParams.entries());
  restKeys.sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey));

  // and join it with \n
  const dataCheckString = restKeys.map(([n, v]) => `${n}=${v}`).join('\n');


  return {
    dataCheckString,
    hash,
    // get metaData from params
    metaData: {
      user: JSON.parse(searchParams.get('user')),
      auth_date: searchParams.get('auth_date'),
      query_id: searchParams.get('query_id'),
    },
  };
}


// encoding message with key
// we need two types of representation here: Buffer and Hex
function encodeHmac(message, key, repr=undefined) {
  return crypto.createHmac('sha256', key).update(message).digest(repr);
}

function checkAuthorization(iniData){
  // parsing the iniData sting
  const authTelegramData = parseAuthString(iniData);

  // creating the secret key and keep it as a Buffer (important!)
  const secretKey = encodeHmac(
    TELEGRAM_BOT_TOKEN,
    WEB_APP_DATA_CONST,
  );

  // creating the validation key (and transform it to HEX)
  const validationKey = encodeHmac(
    authTelegramData.dataCheckString,
    secretKey,
    'hex',
  );

  // the final step - comparing and returning
  if (validationKey === authTelegramData.hash) {
    return authTelegramData.metaData.user;
  }

  return null;
}

  1. 암호화는 프로세서에 대해 매우 복잡한 것이기 때문에 캐싱을 추가할 수 있습니다. 따라서 예를 들어 Redis 또는 메모리 내 캐시를 사용하여 initData 문자열을 키와 같은 값으로 유지하고 userData JSON을 값으로 유지할 수 있습니다.
  2. initData의 유효성을 검사한 후 자신의 JWT 토큰을 한 번 생성하고 쿠키로 설정할 수 있습니다. 인증을 생성하는 더 강력한 방법이라고 생각합니다.