Альтернативная авторизация в Pyrogram через чат-бота Телеграм
Создаю юзербота на Pyrogram с управлением через чат-бота телеграм, который в свою очередь работает на PyTelegrambotApi. Создаю авторизацию через чат-бота, чтобы создавался файл сессии в Pyrogram. По сути пользователь вводит свой номер телефона чат-боту, потом код подтверждения, должен быть создан файл сессии. Добавил асинхронность, так как без неё не работает ничего. Теперь вылазит ошибка Telegram says: [400 PHONE_COED_EXPIRED] — The confirmation code has expired (caused by «auth.SignIn»).
import telebot import db import asyncio from telebot import types from pyrogram import Client bot = telebot.TeleBot('token') user_dict = <> class User: def __init__(self, id): self.id = id self.api_id = None self.api_hash = None self.number = None auth_dict = <> class Auth: def __init__(self, id): self.id = id self.number = None self.app = None self.sc = None self.loop = None self.code = None def sendcode(self, number): #async self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.app = Client(f"users//", api_id=apiid, api_hash='apihash') self.app.connect() self.number = number self.sc = Client.send_code(self.app, phone_number=self.number) self.app.disconnect() def signin(self, code): try: self.app.connect() except: pass print(self.sc.phone_code_hash) Client.sign_in(self.app, phone_number=self.number, phone_code_hash=self.sc.phone_code_hash, phone_code=str(self.code)) self.app.disconnect() self.loop.close()
В самом боте Telegram я сначала спрашиваю у пользователя номер, потом вызываю функцию sendcode, затем через bot.register_next_step_handler получаю код и вызываю функцию signin
Отслеживать
задан 5 июн 2022 в 20:23
11 2 2 бронзовых знака
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
я тоже столкнулся с необходимостью авторизацией через отдельного бота, посмотрел ваше сообщение, набросал код и тоже полезли ошибки.
Возможно вопрос уже решён, но оставлю разяснение последующим пользователям,которые захотят повторить это.
Проблемма в том, что хЭш кода авторизации можна использовать в текущем сеансе а у вас в функциях «sendcode» и «signin» приложение подключается и отключается между шагами авторизации и ето не подходит. По этому нужно избежать отключения пока не передастся код авторизации и не произойдёт вход в учётную запись. Наводку на суть проблеммы получил вот здесь
Сразу скажу, я нуб, только начал изучать питон и нашел только способ избежать отключение, сделав одну функцию (вместо 2х) и подерил её на 2 этапа с использованием переменной «menu» и её логической проверкой, чтобы ненужный код не выполнился в ненужный момент. И заранее извиняюсь за возможный баг в передачи номера телефона аккаунта на который происходит авторизация в переменную. Там можно на прямую прописать номер и оно точно заработает. (ну есть нуб)
Также, всех кто хочет попробовать повторить данный способ, хочу предупредить — если в бота передавать чистый код авторизации с того аккаунта в который происходит авторизация (надеюсь нормально объяснил хд) — он (код авторизации) аннулируется, потому я использую лайфхак с вводом кода через пробел и он работает
Вот мой говнокод (да, я для бота использую тот же пирограм, но суть дела не меняет)(upd спустя сутки, перепроверил код, поправил, что бы не било ошибки, тысяча извиненй, скопировал не с того тестового файла):
import asyncio from pyrogram import Client, filters api_id = 00000000 api_hash = "xxxxxxxxxxxxxx" bot_token = "000000:xxxxxxxxxxx" menu = 0 phone_number = "" sc = None bot = Client("cp_bot", api_id=api_id, api_hash=api_hash, bot_token=bot_token) app = Client("test_auth", api_id=api_id, api_hash=api_hash, phone_number=phone_number) async def auth(number): global api_id global api_hash global phone_number global sc global menu global app loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) print(number) if menu == 1: await app.connect() print("trying send code") sc = await app.send_code(phone_number=number) print("OK") elif menu == 2: code = number ch = sc.phone_code_hash print(ch) await app.sign_in(phone_number=phone_number, phone_code_hash=ch, phone_code=str(code)) await app.disconnect() loop.close() @bot.on_message(filters.command(["start"]) & filters.text) async def command_start(client, message): global menu await message.reply_text("Старт аутентификации! Отправь номер телефона в международном формате (+1234567890)") menu = 1 @bot.on_message(filters.text) async def command_start(client, message): global menu global phone_number global code if menu == 1: phone = message.text phone = phone.replace("+", "") if phone.isdigit(): print(phone) phone_number = phone await auth(phone_number) menu = 2 print("is ok") await message.reply_text("DONE!\nТеперь отправь код авторизации, с цифрами через пробел (прим. \"1 2 266\" и подобное)") else: await message.reply_text("Phone incorrect! try again") elif menu == 2: v_dig = message.text v_dig = v_dig.replace(" ", "") # если в бота или личку ТГ с которого производится авторизация, пересылается # чистый код авторизации - он аннулируется, потому я использую лайфхак с вводом кода через пробел и он работает print(v_dig) if v_dig.isdigit(): code = v_dig print(code) await auth(code) await message.reply_text("DONE!\nАвторизация прошла успешно!") menu = 0 else: await message.reply_text("CODE incorrect! try again") bot.run()
Работает ли telegram oauth в России?

spaceatmoon, от провайдера зависит, в клиенты уже завезли обход DPI и там, где он плохой, блокировка вообще не ощущается.
Решения вопроса 1

Life is too short for bad software
Telegram login widget в России не работает, но авторизация через Telegram элементарно реализуется с помощью бота. При отправке команды бот вызывает веб-хук, передавая имя пользователя, в ответ отправляется одноразовая ссылка для входа на сайт. Бота нужно запускать на зарубежном хостинге, т.к. с него должен быть доступен api.telegram.org, пользователь может находиться где угодно.
- Через @BotFather нужно создать нового бота, записать полученный token —
- Добавить команду /login с помощью /setcommands
- В консоли сгенерировать случайный uuid ( uuidgen -r ) —
- Настроить веб-хук (вместо httpie можно использовать curl, wget, или любую другую подобную утилиту):
http "https://api.telegram.org/bot/setWebhook?url=https:///bot//"
. TELEGRAM_BOT_ACCESS_TOKEN = TELEGRAM_BOT_WEBHOOK_TOKEN = TELEGRAM_BOT_OTP_TIMEOUT = 120 USE_X_FORWARDED_HOST = True CACHES = < 'default': < 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'django_cache', >> .
. from .views import BotView, LoginView urlpatterns = [ . path('bot//', BotView.as_view()), path('login//', LoginView.as_view(), name='login'), ]
import json import requests import uuid from django.conf import settings from django.contrib.auth import login from django.contrib.auth.models import User from django.core.cache import cache from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect from django.urls import reverse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView . @method_decorator(csrf_exempt, name='dispatch') class BotView(View): def post(self, request, token): if str(token) != settings.TELEGRAM_BOT_WEBHOOK_TOKEN: return HttpResponseNotFound() params = json.loads(request.body) message = params['message'] if message.get('text') == '/login': user = message['from'] chat_id = user['id'] data = < 'username': user['username'], 'first_name': user.get('first_name', ''), 'last_name': user.get('last_name', ''), >otp = uuid.uuid4() cache.set(otp, data, settings.TELEGRAM_BOT_OTP_TIMEOUT) path = reverse('login', args=[otp]) login_url = request.build_absolute_uri(path) url = 'https://api.telegram.org/bot<>/sendMessage?chat_id=<>&text=<>'.format( settings.TELEGRAM_BOT_ACCESS_TOKEN, chat_id, login_url, ) requests.get(url, timeout=10) return HttpResponse('') class LoginView(TemplateView): template_name = 'login.html' def dispatch(self, request, otp): self.data = cache.get(otp, <>) return super().dispatch(request, otp) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['data'] = self.data return context def post(self, request, otp): cache.delete(otp) username = self.data.get('username') if not username: return HttpResponseRedirect(request.path) user, created = User.objects.update_or_create( username=username, defaults=< 'first_name': self.data.get('first_name', ''), 'last_name': self.data.get('last_name', ''), >, ) login(request, user) path = reverse('home') return HttpResponseRedirect(path)
This url invalid or expired.
Login as >
Подтверждение входа с помощью Telegram на Spring Boot
Недавно столкнулся с проблемой: все приложения используют Telegram-бота в качестве подтверждения входа в аккаунт, а мое — нет. Я был настроен серьезно и провёл уйму времени в интернете в поиске туториала, но меня ждало разочарование. Задача сложная и имеет много подводных камней, а туториалов — ноль.
Следующую неделю я потратил на написание своей имплементации данной фичи и готов поделиться успехом.
Весь код, который мы сегодня напишем, доступен в репозитории на GitHub. Рекомендую параллельно с чтением статьи проверять этот код в проекте, чтобы не упустить детали.
Создание проекта
Итак, для начала создадим проект. Для этого я использовал Spring Initializr. Для проекта нам понадобится Spring MVC, Spring Security и Spring WebSocket. В качестве базы данных будем использовать H2. Мои настройки выглядели вот так:
Затем в наш pom.xml добавим дополнительные зависимости: библиотека для работы с Telegram и webjars: bootstrap (для красивого дизайна), stomp-websocket и sockjs-client для работы с Spring WebSocket.
В итоге наш pom.xml будет выглядеть вот так.
Зависимости pom.xml
org.springframework.boot spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-thymeleaf org.thymeleaf.extras thymeleaf-extras-springsecurity5 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-websocket org.springframework.security spring-security-messaging com.h2database h2 runtime org.telegram telegrambots 4.8.1 org.telegram telegrambotsextensions 4.8.1 org.webjars bootstrap 4.4.1-1 org.webjars stomp-websocket 2.3.3 org.webjars sockjs-client 1.0.2 org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.security spring-security-test test Настройка базовой авторизации
Перейдём к настройке базовой авторизации. Сейчас наш класс настройки Web Security выглядит так, но позже он сильно измениться:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter < private UserService userService; @Autowired public WebSecurityConfig(UserService userService) < this.userService = userService; >@Override protected void configure(HttpSecurity http) throws Exception < http .csrf().and() .authorizeRequests() .antMatchers("/login").anonymous() .antMatchers("/resource/**", "/webjars/**", "/websocket/**").permitAll() .antMatchers("/**").authenticated() .and() .formLogin() .loginPage("/login") .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login?logout"); >@Override public void configure(WebSecurity web) throws Exception < web.ignoring().antMatchers("/static/**", "/webjars/**"); >@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception < auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); >@Bean public PasswordEncoder passwordEncoder() < return PasswordEncoderFactories.createDelegatingPasswordEncoder(); >>Вы можете обратить внимание, что мы сказали Spring’у, что он должен разрешать запрос /websocket/** всем. Пока что в этом нет никакого смысла, но в будущем это будет очень важная строка.
Перепись авторизации на JSON формат
Наше приложение будет работать по такому алгоритму:
Как вы видите, тут проверка пользователя идёт по порядку: спросили одно и ждём ответа, при получении ответа, если надо, спрашиваем второе. Перенаправлять пользователя со страницы на страницу было бы не очень удобно, а вот технологию AJAX применить можно было бы. Для этого перепишем нашу авторизацию на JSON. Делать мы это будем с помощью AuthenticationSuccessHandler и AuthenticationFailureHandler , но для начала создадим модель, которую будем возвращать в качестве информации об авторизации:
public class AuthenticationInfo < private boolean success; private String redirectUrl; private String errorMessage; private SetrequiredMfas; public enum RequiredMfa < TELEGRAM_MFA >// getters, setters and no args constructor >Как вы видите, тут мы используем Set requiredMfas вместо простого boolean askTelegramMfa . Как вы думаете почему?
Действительно, в данном примерочном проекте большого смысла нет. Однако тут я ориентируюсь на большие проекты где, помимо Telegram подтверждения, пользователи могут использовать разные способы подтверждения (больше одного).
Теперь напишем RequireTelegramMfaException . Это Exception, который мы будем выбрасывать, если пользователь должен авторизоваться с помощью Telegram. Почему? Пока пользователь не подтвердил авторизацию в Telegram, мы не должны его авторизовать, а значит мы должны выбросить Exception, что бы Spring этого не сделал. Почему мы пишем его сейчас? Далее мы напишем CustomAuthenticationFailureHandler , который будет проверять эту ошибку.
Наш RequireTelegramMfaException обязательно должен наследоваться от AuthenticationException , а выглядеть будет так:
public class RequireTelegramMfaException extends AuthenticationException < public RequireTelegramMfaException(String msg) < super(msg); >>Теперь перейдём непосредственно к CustomAuthenticationFailureHandler . Наш код будет выглядеть вот так:
public class CustomFailureHandler implements AuthenticationFailureHandler < private ObjectMapper objectMapper = new ObjectMapper(); @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException < AuthenticationInfo info = new AuthenticationInfo(); info.setSuccess(false); info.setErrorMessage(e.getMessage()); if (e instanceof RequireTelegramMfaException) < info.setRequiredMfas(Collections.singleton(TELEGRAM_MFA)); >response.setCharacterEncoding(CharEncoding.UTF_8); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); objectMapper.writeValue(response.getWriter(), info); > >Тут всё просто: в начале мы создаем и настраиваем AuthenticationInfo , говорим, что авторизация была не успешна, ошибку берём из Exception и если наш Exception — это RequireTelegramMfaException говорим, что пользователь ещё должен подтвердить авторизацию в Telegram. Затем мы уже просто настраиваем ответ: ставим character encoding = UTF-8, отправляем статус 401 (UNAUTHORIZE) и ставим content type = application/json . Затем просто возвращаем наш AuthenticationInfo .
Наш CustomSuccessHandler будет возвращать AuthenticationInfo с параметром success=true и указывать адрес, на который надо перенаправлять после авторизации (последняя открытая страница). Статус ответа будет 200 (OK). Делается это вот так:
public class CustomSuccessHandler implements AuthenticationSuccessHandler < private RequestCache requestCache = new HttpSessionRequestCache(); private ObjectMapper objectMapper = new ObjectMapper(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException < AuthenticationInfo info = new AuthenticationInfo(); info.setSuccess(true); info.setRedirectUrl(getRedirectUrl(request, response)); response.setCharacterEncoding(CharEncoding.UTF_8); response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); objectMapper.writeValue(response.getWriter(), info); >public String getRedirectUrl(HttpServletRequest request, HttpServletResponse response) < SavedRequest cache = requestCache.getRequest(request, response); return cache == null ? "/" : cache.getRedirectUrl(); >public void setRequestCache(RequestCache requestCache) < this.requestCache = requestCache; >>Как вы видите, redirectUrl мы берём из объекта RequestCache , который мы можем взять у объекта HttpSecurity . Наш новый WebSecurityConfig будет выглядеть вот так (в будущем мы будем его редактировать ещё один раз):
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter < // fields declarations and constructor @Override protected void configure(HttpSecurity http) throws Exception < http .csrf().and() .authorizeRequests() .antMatchers("/login").anonymous() .antMatchers("/resource/**", "/webjars/**", "/websocket/**").permitAll() .antMatchers("/**").authenticated() .and() .formLogin() // ставим только что написанные нами handler'ы .failureHandler(authenticationFailureHandler()) .successHandler(authenticationSuccessHandler()) .loginPage("/login") .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login?logout"); // запрашиваем у HttpSecurity объект RequestCache // если он не null - передаём его в наш CustomSuccessHandler RequestCache requestCache = http.getSharedObject(RequestCache.class); if (requestCache != null) < authenticationSuccessHandler().setRequestCache(requestCache); >> @Override public void configure(WebSecurity web) throws Exception < web.ignoring().antMatchers("/static/**", "/webjars/**"); >@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception < auth.userDetailsService(userService).passwordEncoder(passwordEncoder()); >@Bean public PasswordEncoder passwordEncoder() < return PasswordEncoderFactories.createDelegatingPasswordEncoder(); >@Bean public CustomSuccessHandler authenticationSuccessHandler() < return new CustomSuccessHandler(); >@Bean public CustomFailureHandler authenticationFailureHandler() < return new CustomFailureHandler(); >>После этого наш frontend нужно переписать на систему AJAX. В этом примере я использовал Bootstrap Carousel, а авторизацию переписал вот так (код html и js):
document.addEventListener('DOMContentLoaded', () => < loginForm.addEventListener('submit', e => < e.preventDefault(); $.ajax(< method: 'POST', url: '/login', data: $(loginForm).serialize(), error: response => < let data = response.responseJSON; // если требуется подтверждение авторизации в Telegram - // открыть нужный слайд, где будет сообщение об этом if (data.requiredMfas && data.requiredMfas.includes('TELEGRAM_MFA')) < $(carousel).carousel(TELEGRAM_SLIDE); >else < // иначе - выводим сообщение ошибки showAlert(data.errorMessage, 'danger'); loginForm.querySelector('input[name="password"]').value = ''; >> >).done(response => < loginModal.classList.add('fullscreen-loading-modal'); // запускаем анимацию загрузки location.href = response.redirectUrl; // перенаправляем пользователя >); >); >Хотелось бы отметить, почему мы не проверяем параметр success : в своих handler’ах мы указываем нужные статусы ответа (200, если авторизован и 401, если не авторизован). Благодаря этому, jQuery сам направит response в нужный callback: error , если ошибка (в нашем случае, ответ 401) или done , если всё нормально (в нашем случае, ответ 200).
Создание и настройка Telegram бота
Теперь займёмся самим Telegram ботом. В нашем pom.xml должны быть две зависимости: наркотическая и никотиновая org.telegram:telegrambots и org.telegram:telegrambotsextensions . Код нашего бота будет выглядеть вот так:
@Component public class TelegramBot extends TelegramLongPollingCommandBot < private String botUsername; private String botToken; public TelegramBot(Environment env, ConnectAccountCommand connectAccountCommand) throws TelegramApiException < super(ApiContext.getInstance(DefaultBotOptions.class), false); this.botToken = env.getRequiredProperty("telegram.bot.token"); this.botUsername = getMe().getUserName(); register(connectAccountCommand); >@PostConstruct public void addBot() throws TelegramApiRequestException < TelegramBotsApi botsApi = new TelegramBotsApi(); botsApi.registerBot(this); >@Override public void processNonCommandUpdate(Update update) < >@Override public String getBotUsername() < return botUsername; >@Override public String getBotToken() < return botToken; >>Тут мы в конструкторе вызываем базовые настройки, где говорим, что мы не хотим работать с командами для бота, которые были вызваны с помощью имени бота (пример: /start@myBot ). Так же мы регистрируем ConnectAccountCommand . Это команда, для подключения вашего аккаунта на сайте к аккаунту Telegram. Также, вы можете зарегистрировать команду /start , для вывода стартового сообщения. Метод addBot() помечен аннотацией @PostConstruct . Это значит, что этот метод будет вызван, когда Bean компонент уже сконфигурирован. Тут мы просто создаем TelegramBotsApi и добавляем туда нашего бота.
ConnectAccountCommand
@Component public class ConnectAccountCommand extends BotCommand < private static final Logger log = LoggerFactory.getLogger(ConnectAccountCommand.class); private UserService userService; public ConnectAccountCommand(UserService userService) < super("connect", "Команда для подключения аккаунта"); this.userService = userService; >@Override public void execute(AbsSender sender, User user, Chat chat, String[] strings) < String username = strings[0]; userService.connectBot(username, chat.getId()); SendMessage message = new SendMessage() .setChatId(chat.getId()) .setText("Вы успешно подключили бота!"); try < sender.execute(message); >catch (TelegramApiException e) < log.error("Error sending success telegram bot connect message", e); >> >Подтверждение авторизации с помощью Telegram бота
Перейдём к написанию самой авторизации с помощью Telegram. Для начала нам понадобится свой WebAuthenticationDetails . Нам понадобится HttpServletRequest, мы будем с ним работать. Наш CustomWebAuthenticationDetails будет выглядеть вот так:
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails < private final HttpServletRequest request; public CustomWebAuthenticationDetails(HttpServletRequest request) < super(request); this.request = request; >public HttpServletRequest getRequest() < return request; >>Теперь нам понадобится свой AuthenticationProvider , который будет проверять нужно ли подтверждение с помощью Telegram и если да — отправлять сообщение в Telegram и сообщать об этом пользователю. Мы будем наследоваться от класса DaoAuthenticationProvider . У него есть метод additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken) . Он может делать проверку пользователя уже после того, как мы проверили логин и пароль пользователя.
public class CustomAuthenticationProvider extends DaoAuthenticationProvider < @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException < HttpServletRequest request = ((CustomWebAuthenticationDetails) authentication.getDetails()).getRequest(); AuthorizedUser authUser = (AuthorizedUser) userDetails; User user = authUser.getUser(); if (user.getTelegramChatId() != null) < // TODO: send telegram confirm message throw new RequireTelegramMfaException("Пожалуйста, подтвердите вход в Telegram!"); >super.additionalAuthenticationChecks(userDetails, authentication); > @Override public boolean supports(Class authentication) < return authentication.equals(UsernamePasswordAuthenticationToken.class); >>Создадим команду Telegram бота для подтверждения авторизации MfaCommand :
@Component public class MfaCommand < private static final Logger log = LoggerFactory.getLogger(MfaCommand.class); private static final String CONFIRM_BUTTON = "confirm"; private MapconnectingUser = new HashMap<>(); private TelegramBot telegramBot; private WebSocketService webSocketService; private CustomSuccessHandler customSuccessHandler; @Autowired public MfaCommand(TelegramBot telegramBot, WebSocketService webSocketService, @Lazy CustomSuccessHandler customSuccessHandler) < this.telegramBot = telegramBot; this.webSocketService = webSocketService; this.customSuccessHandler = customSuccessHandler; >// теперь, наш CustomAuthenticationProvider будет вызывать этот метод public void requireMfa(Authentication authentication, SecurityContext context, HttpServletRequest request) < User user = ((AuthorizedUser) authentication.getPrincipal()).getUser(); // Мы создаём объект AuthInfo и кладём его в нашу мапу // В качестве ключа используем chat id // CSRF токен нам понадобится позже String csrfToken = request.getParameter("_csrf"); HttpSession session = request.getSession(true); // Если продебажить код - мы увидим, // что Spring использует HttpSessionRequestCache // в качестве RequestCache, а посмотрев его исходный код мы увидим, // что HttpServletResponse он никак не использует. // Поэтому мы можем передавать туда null String redirectUrl = customSuccessHandler.getRedirectUrl(request, null); AuthInfo authInfo = new AuthInfo(authentication, context, session, csrfToken, redirectUrl); connectingUser.put(user.getTelegramChatId(), authInfo); sendUserMessage(user); >// Когда пользователь нажмёт кнопку отклонить или подтвердить - // наш Telegram бот вызовет этот метод public void onCallbackQuery(CallbackQuery callbackQuery) < Message message = callbackQuery.getMessage(); // Ищем наш AuthInfo по chat id, извлекаем и удаляем его AuthInfo authInfo = connectingUser.remove(message.getChatId()); EditMessageText editMessageText = new EditMessageText() .setChatId(message.getChatId()) .setMessageId(message.getMessageId()); AuthenticationInfo info = new AuthenticationInfo(); // Если пользователь нажал кнопку "Подтвердить" if (callbackQuery.getData().equals(CONFIRM_BUTTON)) < // Берем авторизацию, // которая к нам пришла из CustomAuthenticationProvider Authentication authentication = authInfo.getAuthentication(); // Устанавливаем её в SecurityContext, // который нам пришёл из CustomAuthenticationProvider authInfo.getSecurityContext().setAuthentication(authentication); // И записываем авторизацию в сессию браузера authInfo.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, authInfo.getSecurityContext()); // Редактируем сообщение и заполняем AuthenticationInfo editMessageText.setText("Вы успешно подтвердили вход!"); info.setSuccess(true); info.setRedirectUrl(authInfo.getRedirectUrl()); >else < // Если пользователь нажал кнопку "Отклонить" // Тогда просто редактируем сообщение и заполняем AuthenticationInfo, // где говорим, что авторизация не прошла успешно editMessageText.setText("Вы успешно отклонили вход!"); info.setSuccess(false); info.setErrorMessage("Вы отклонили вход в Telegram"); >// TODO: send browser notification try < telegramBot.execute(editMessageText); >catch (TelegramApiException e) < log.error("Error updating telegram MFA message", e); >> private void sendUserMessage(User user) < InlineKeyboardButton confirmButton = new InlineKeyboardButton("Подтвердить"); confirmButton.setCallbackData(CONFIRM_BUTTON); InlineKeyboardButton declineButton = new InlineKeyboardButton("Отклонить"); declineButton.setCallbackData("decline"); InlineKeyboardMarkup markup = new InlineKeyboardMarkup( Collections.singletonList( Arrays.asList(confirmButton, declineButton) ) ); SendMessage sendMessage = new SendMessage() .setChatId(user.getTelegramChatId()) .setText("Подтвердите вход в аккаунт " + user.getUsername() + "") .setParseMode("HTML") .setReplyMarkup(markup); try < telegramBot.execute(sendMessage); >catch (TelegramApiException e) < log.error("Error sending telegram MFA message", e); >> private static class AuthInfo < private final Authentication authentication; private final SecurityContext securityContext; private final HttpSession session; private final String csrf; private final String redirectUrl; // all args constructor and getters >>Перепишем if в методе additionalAuthenticationChecks() класса CustomAuthenticationProvider .
if (user.getTelegramChatId() != null)И обновим метод processNonCommandUpdate(Update) класса TelegramBot :
@Override public void processNonCommandUpdate(Update update) < if (update.hasCallbackQuery()) < mfaCommand.onCallbackQuery(update.getCallbackQuery()); >>Теперь обновим WebSecurityConfig где добавим парсер AuthenticationDetais в CustomWebAuthenticationDetails и наш новый CustomAuthenticationProvider .
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter < private UserService userService; private MfaCommand mfaCommand; @Autowired public WebSecurityConfig(UserService userService, MfaCommand mfaCommand) < this.userService = userService; this.mfaCommand = mfaCommand; >@Override protected void configure(HttpSecurity http) throws Exception < http .csrf().and() .authorizeRequests() .antMatchers("/login").anonymous() .antMatchers("/webjars/**", "/resource/**", "/websocket/**").permitAll() .antMatchers("/**").authenticated() .and() .formLogin() // парсер AuthenticationDetails в CustomWebAuthenticationDetails // аналог: details ->new CustomWebAuthenticationDetails(details) .authenticationDetailsSource(CustomWebAuthenticationDetails::new) .failureHandler(authenticationFailureHandler()) .successHandler(authenticationSuccessHandler()) .loginPage("/login") .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login?logout"); RequestCache requestCache = http.getSharedObject(RequestCache.class); if (requestCache != null) < authenticationSuccessHandler().setRequestCache(requestCache); >> // WebSecurity configure @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception < auth.authenticationProvider(customAuthenticationProvider()); >@Bean public CustomAuthenticationProvider customAuthenticationProvider() < var provider = new CustomAuthenticationProvider(mfaCommand); provider.setUserDetailsService(userService); provider.setPasswordEncoder(passwordEncoder()); return provider; >// old beans >На этом этапе можете запустить приложение. Если у вас подключён Telegram аккаунт — бот отправит вам сообщение для подтверждения. Если вы нажмёте кнопку «Подтвердить» — вас авторизуют, но вы этого не увидите. Для этого обновите страницу в браузере или, что лучше, перейдите на главную страницу (к странице авторизации доступа у вас уже не будет, так как вы авторизованы).
Отправка сообщение в браузер, об успешной авторизации
Последнее, что осталось сделать — потанцевать с бубном написать уведомление для браузера. Мы уже авторизуем браузер при подтверждении, но браузер об этом не знает. Мы должны вручную обновлять страничку, чтобы увидеть это. От этого будем пытаться избавиться.
Начнём сразу с проблемы, которая у нас встречается: мы можем подключать Spring WebSocket, но как мы будем ему говорить, кому отправлять сообщение? Пользователь подключается ещё до авторизации, так что его логин ещё неизвестен. Если при подключении пользователь не авторизован, в метод simpMessagingTemplate.convertAndSendToUser нужно передавать Session ID в качестве String user параметра: ID подключения. Однако, Session ID , который к нам приходит в HttpServletRequest , отличается от Session ID , который к нам приходит при подключении к WebSocket.
Сделаем свой репозиторий, который будет хранить Session ID подключения WebSocket. Но как мы будем аутентифицировать пользователя? В качестве решения решил использовать CSRF токен. Для подключения WebSocket, нам нужно указать CSRF токен, для запросов — тоже. Перейдём к реализации:
@Component public class WebSocketSessionStorage implements ApplicationListener < private Mapstorage = new HashMap<>(); @Override public void onApplicationEvent(SessionConnectEvent event) < StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); // берём Session ID подключения. String sessionId = sha.getSessionId(); // . и CSRF токен ListnativeHeader = sha.getNativeHeader("X-CSRF-TOKEN"); if (nativeHeader != null && nativeHeader.size() != 0) < // и кладём его в наш репозиторий storage.put(nativeHeader.get(0), sessionId); >> public String getSessionId(String csrf) < return storage.remove(csrf); >>Теперь создадим WebSocketService , который будет отправлять сообщение для бразура. Он будет принимать информацию об авторизации и CSRF токен.
@Service public class WebSocketService < private SimpMessagingTemplate simpMessagingTemplate; private WebSocketSessionStorage sessionStorage; // all arg constructor public void sendLoginStatus(AuthenticationInfo info, String csrf) < // ищем Session ID, используя CSRF токен String sessionId = sessionStorage.getSessionId(csrf); var headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); headerAccessor.setSessionId(sessionId); headerAccessor.setLeaveMutable(true); // отправляем сообщения по адресу "/queue/login" simpMessagingTemplate.convertAndSendToUser(sessionId, "/queue/login", info, headerAccessor.getMessageHeaders()); >>Добавим вызов этого метода в MfaCommand :
public void onCallbackQuery(CallbackQuery callbackQuery) < Message message = callbackQuery.getMessage(); AuthInfo authInfo = connectingUser.remove(message.getChatId()); // . AuthenticationInfo info = new AuthenticationInfo(); if (callbackQuery.getData().equals(CONFIRM_BUTTON)) < // . >// отправляем уведомление для браузера webSocketService.sendLoginStatus(info, authInfo.getCsrf()); try < telegramBot.execute(editMessageText); >catch (TelegramApiException e) < log.error("Error updating telegram MFA message", e); >>Создадим WebSocketConfig и WebSocketSecurityConfiguration :
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer < @Override public void configureMessageBroker(MessageBrokerRegistry registry) < registry.enableSimpleBroker("/queue/login"); registry.setApplicationDestinationPrefixes("/ws"); >@Override public void registerStompEndpoints(StompEndpointRegistry registry) < registry .addEndpoint("/websocket") .setAllowedOrigins("*") .withSockJS(); >> @Configuration public class WebSocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer < @Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) < messages // говорим, что подключаться, // отключаться и отписываться - могу все .simpTypeMatchers( SimpMessageType.CONNECT, SimpMessageType.DISCONNECT, SimpMessageType.UNSUBSCRIBE ).permitAll() // слушать информацию об авторизации // могут только не авторизованные пользователи .simpSubscribeDestMatchers("/user/queue/login").anonymous() // все остальные сообщения - // только для авторизованных пользователей .anyMessage().authenticated(); >>Напоню, что в WebSecurityConfig должно стоять .antMatchers(«/websocket/**»).permitAll() . Это очень важно. Иначе, неавторизованный пользователь не сможет подключиться.
Осталось только переписать frontend, что бы он ловил эти сообщения и делал какие-то действия, в зависимости от содержимого:
let socket = new SockJS('/websocket'); let stompClient = Stomp.over(socket); // создаем объект headers, куда кладём все Header'ы, // необходимые для подключения let headers = <>; // кладём CSRF токен // по нему мы и будем авторизовывать нашего пользователя // а без него - WebSecurity запретит подключение let csrfHeader = document.querySelector('meta[name="_csrf_header"]').content; headers[csrfHeader] = document.querySelector('meta[name="_csrf"]').content; // подключаемся к WebSocket серверу stompClient.connect(headers, frame => < console.log(frame); // подписываемся на уведомления по авторизации stompClient.subscribe('/user/queue/login', data => < const info = JSON.parse(data.body); // если авторизация успешна - // выводим анимацию загрузки // и открываем нужную страницу if (info.success) < loginModal.classList.add('fullscreen-loading-modal'); location.href = info.redirectUrl; >else < // иначе - выводим ошибку // и открываем слайд ввода логина и пароля loginForm.querySelector('input[name="password"]').value = ''; $(carousel).carousel(LOGIN_SLIDE); showAlert(info.errorMessage, 'danger'); >>); >);Наше приложение готово! Напомню, что весь исходный код лежит в GitHub репозитории. Вы можете ознакомиться с ним ещё раз или попробовать запустить приложение.
Также, в этом приложении мы это не рассматривали, но, для продакшона, вам, скорее всего, понадобится реализовать функцию «Запомнить меня». Дело в том, что если пользователь использует вход с помощью телеграма — приложение не сможет добавить ему remember me cookie . Для исправления этого, вы можете записывать cookie с помощью JavaScript или, к примеру, перенаправлять пользователя на страничку с параметром ?rememberMe=true , а затем, используя Filter , проверять на наличие этого параметра и, если это необходимо, записывать cookie .
- Информационная безопасность
- Java
Авторизация пользователей через Telegram
Недавно Telegram добавил поддержку виджета для авторизации пользователей на сайте. Мы решили поэкспериментировать с ним и составить простую инструкцию, как настроить такую авторизацию самостоятельно.
В качестве примера будем использовать код на PHP, однако, данные шаги актуальны и для других языков программирования.
Настройка бота
Для использования виджета вам понадобится Telegram-бот.
Перейдите в чат с системным пользователем @botfather. Если у вас ещё нет ни одного бота, создайте его командой /newbot. Посмотреть список своих пользователей вы можете с помощью команды /mybots.
Скопируйте токен бота, через которого вы хотите производить авторизацию пользователей.
Here is the token for bot codex_cloud @codex_cloud_bot: 558<. >728:AWBEwgUg<. >HBKuiINt
Название и аватарка выбранного вами бота будут показаны пользователю во всплывающем окне. А вы получите возможность отправлять пользователю личные сообщения через этого бота.
Для каждого бота нужно привязать конкретный адрес сайта, на котором пользователи могут авторизоваться. В диалоге с @botfather введите команду /setdomain и напишите адрес сайта в виде http://ifmo.su.
Настройка виджета
На сайте можно получить код виджета и выбрать его внешний вид. К сожалению, возможностей для его произвольного конфигурирования на данный момент нет т.к. виджет встраивается на сайт посредством iframe.
Встраивание на сайт
Создайте файл index.php со следующим содержанием
После того, как пользователь нажмёт на кнопку, Telegram готов отправить вам данные любым из двух способов:
- Отправить пользователя на ваш сайт путём редиректа, передав информацию о нём в GET параметрах.
- Вызвать JavaScript функцию, передав в неё информацию о пользователе в качестве аргументов.
На данный момент поддерживаются следующие данные о пользователе:
- id – уникальный идентификатор пользователя в Telegram
- first_name, last_name – фамилия и имя из профиля пользователя
- username – уникальное имя из профиля
- photo_url – ссылка на аватарку пользователя в виде https://t.me/i/. /user.jpg
- auth_date – дата авторазации
- hash – HMAC-подпись ответа на основе секретного токена бота
Получение данных через JavaScript callback
Выберите в конструкторе виджета опцию Authorization Type: Callback. Сгенерированный в результате код виджета содержит JavaScript функцию, которая будет вызвана после успешной авторизации.
function onTelegramAuth(user) < alert('Logged in as ' + user.first_name + ' ' + user.last_name + ' (' + user.id + (user.username ? ', @' + user.username : '') + ')'); >
Эту функцию нужно передать в аттрибуте data-onauth тега
Вы можете произвольным образом реализовать функцию onTelegramAuth. Например, послать AJAX запрос на сервер с полученными аргументами.
Получение данных через Redirect
Выберите в конструкторе виджета опцию Authorization Type: Redirect to URL и введите URL, на который вы хотите получить запрос с данными пользователя. Например, введите адрес http://example.com/auth/telegram.
На странице обработки можно положить скрипт index.php следующего содержания:
Проверка данных пользователя
Чтобы удостовериться в правильности полученных данных, нужно проверить hash. Разработчики Telegram приводят пример кода проверки, добавим эту функцию в код из файла index.php
function checkTelegramAuthorization($auth_data) < $check_hash = $auth_data['hash']; unset($auth_data['hash']); $data_check_arr = []; foreach ($auth_data as $key =>$value) < $data_check_arr[] = $key . '=' . $value; >sort($data_check_arr); $data_check_string = implode(«\n», $data_check_arr); $secret_key = hash(‘sha256’, BOT_TOKEN, true); $hash = hash_hmac(‘sha256’, $data_check_string, $secret_key); if (strcmp($hash, $check_hash) !== 0) < throw new Exception('Data is NOT from Telegram'); >if ((time() — $auth_data[‘auth_date’]) > 86400) < throw new Exception('Data is outdated'); >return $auth_data; >
Разберём механизм работы функции проверки. В качестве аргумента она получает массив с данными пользователя.
array(7) < ["id"]=>string(7) «1831337» [«first_name»]=> string(18) «Александр» [«last_name»]=> string(16) «Менщиков» [«username»]=> string(5) «n0str» [«photo_url»]=> string(36) «https://t.me/i/userpic/100/n0str.jpg» [«auth_date»]=> string(10) «1518168109» [«hash»]=> string(64) «abba<..>1345″ >
На первом шаге из массива извлекается значение по ключу hash и сохраняется в переменной.
На втором шаге массив преобразуется к виду key=value и сортируется в лексикографическом порядке. Полученные данные склеиваются в одну строку через разделитель “\n” (код символа – 0xA0).
Далее происходит проверка равенства HMAC-SHA-256 подписи этой строки и значения сохранённого hash. Дополнительно проверяется не устарела ли auth_date.
В случае успеха, функция возвращает исходный массив без параметра hash.
Авторизация пользователя на сайте
Добавим в файл код вызова функции проверки
if (isset($_GET[‘hash’])) < try < $auth_data = checkTelegramAuthorization($_GET); echo "Hello, " . $auth_data['username']; >catch (Exception $e) < die ($e->getMessage()); > >
Пользователь увидит сообщение с приветствием в случае успешной авторизации. Теперь вы можете сохранить информацию о нём в базу данных и привязать его ID к текущей сессии.
Пример кода из рабочего проекта
try < $profile = $tg->checkTelegramAuthorization($_GET); $id = $profile[‘id’]; $user = Model_User::findByAttribute(‘telegram_id’, $id); if ($user->is_empty()) < $user = new Model_User(); $user->telegram_id = $id; . $user->save() > else < . >>
Кастомизация кнопки
Сейчас из-за ограничений iframe нельзя изменить внешний вид кнопки. Однако, если возникла сильная необходимость, можно обойти это ограничения с помощью clickjacking.
Внимание! Это решение не рекомендуется к использованию. Clickjacking вводит пользователей в заблуждение, а поисковые системы могут понизить ваш сайт в выдаче в качестве наказания.
Идея состоит в том, чтобы разместить iframe с кнопкой поверх ссылки, оформленной по вашему вкусу. Если такой iframe сделать прозрачным, то пользователь кликнет внутрь него, пытаясь нажать на ссылку.
Итоги
Telegram выпустила полезный инструмент, который позволяет авторизовать пользователей на своём сайте и привязать их профиль к Telegram-аккаунту. К сожалению, пока не поддерживается свободная кастомизация их виджета, а также нет удобного API для аутентификации. Вероятно, в ближайшее время они добавят такие возможности.
Ссылки для подробного изучения
- Официальная документация – https://core.telegram.org/widgets/login
- Анонс виджета – https://telegram.org/blog/login
- Документация по боту Telegram – https://core.telegram.org/bots#creating-a-new-bot
- Примеры кода проверки авторизации – https://gist.github.com/anonymous/6516521b1fb3b464534fbc30ea3573c2
If you like this article, share a link with your friends
Read more
We talk about interesting technologies and share our experience of using them.






