using System.Security.Cryptography; using System.Text; using System.Text.Json; using GmRelay.Web.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; namespace GmRelay.Bot.Tests.Web; public sealed class TelegramAuthServiceTests { [Fact] public void Verify_ShouldAcceptValidTelegramPayload() { const string botToken = "test-bot-token"; var authDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var query = CreateQueryCollection( botToken, new Dictionary { ["auth_date"] = authDate, ["first_name"] = "Ada", ["id"] = "424242", ["last_name"] = "Lovelace", ["username"] = "ada" }); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.Verify(query, out var telegramId, out var name); Assert.True(verified); Assert.Equal(424242L, telegramId); Assert.Equal("Ada Lovelace", name); } [Fact] public void Verify_ShouldRejectTamperedHash() { const string botToken = "test-bot-token"; var values = new Dictionary { ["auth_date"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ["first_name"] = "Ada", ["id"] = "424242" }; var query = CreateQueryCollection(botToken, values); var invalidQuery = new QueryCollection(new Dictionary(query.ToDictionary( pair => pair.Key, pair => pair.Value)) { ["hash"] = "00" }); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.Verify(invalidQuery, out _, out _); Assert.False(verified); } [Fact] public void Verify_ShouldRejectExpiredPayload() { const string botToken = "test-bot-token"; var expiredAuthDate = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds().ToString(); var query = CreateQueryCollection( botToken, new Dictionary { ["auth_date"] = expiredAuthDate, ["first_name"] = "Ada", ["id"] = "424242" }); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.Verify(query, out _, out _); Assert.False(verified); } [Fact] public void VerifyWebAppInitData_ShouldAcceptValidTelegramWebAppPayload() { const string botToken = "test-bot-token"; var initData = CreateWebAppInitData( botToken, new Dictionary { ["auth_date"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ["query_id"] = "AAHdF6IQAAAAAN0XohDhrOrc", ["user"] = """{"id":424242,"first_name":"Ada","last_name":"Lovelace","username":"ada"}""" }); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyWebAppInitData(initData, out var telegramId, out var name); Assert.True(verified); Assert.Equal(424242L, telegramId); Assert.Equal("Ada Lovelace", name); } [Fact] public void VerifyWebAppInitData_ShouldRejectTamperedHash() { const string botToken = "test-bot-token"; var initData = CreateWebAppInitData( botToken, new Dictionary { ["auth_date"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ["user"] = """{"id":424242,"first_name":"Ada"}""" }); var tamperedInitData = initData.Replace("hash=", "hash=00", StringComparison.Ordinal); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyWebAppInitData(tamperedInitData, out _, out _); Assert.False(verified); } [Fact] public void VerifyWebAppInitData_ShouldRejectExpiredPayload() { const string botToken = "test-bot-token"; var initData = CreateWebAppInitData( botToken, new Dictionary { ["auth_date"] = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds().ToString(), ["user"] = """{"id":424242,"first_name":"Ada"}""" }); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyWebAppInitData(initData, out _, out _); Assert.False(verified); } [Fact] public void VerifyLoginPayload_ShouldAcceptValidTelegramWidgetCallbackPayload() { const string botToken = "test-bot-token"; var authDate = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var values = new Dictionary { ["auth_date"] = authDate.ToString(), ["first_name"] = "Ada", ["id"] = "424242", ["last_name"] = "Lovelace", ["photo_url"] = "https://t.me/i/userpic/320/ada.jpg", ["username"] = "ada" }; var payload = new TelegramLoginPayload( 424242, "Ada", "Lovelace", "ada", "https://t.me/i/userpic/320/ada.jpg", authDate, ComputeTelegramHash(botToken, values)); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyLoginPayload(payload, out var telegramId, out var name); Assert.True(verified); Assert.Equal(424242L, telegramId); Assert.Equal("Ada Lovelace", name); } [Fact] public void VerifyLoginPayload_ShouldRejectTamperedCallbackHash() { var payload = new TelegramLoginPayload( 424242, "Ada", null, null, null, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), "00"); var service = new TelegramAuthService(CreateConfiguration("test-bot-token")); var verified = service.VerifyLoginPayload(payload, out _, out _); Assert.False(verified); } [Fact] public void VerifyLoginPayload_ShouldRejectExpiredCallbackPayload() { const string botToken = "test-bot-token"; var authDate = DateTimeOffset.UtcNow.AddDays(-2).ToUnixTimeSeconds(); var values = new Dictionary { ["auth_date"] = authDate.ToString(), ["first_name"] = "Ada", ["id"] = "424242" }; var payload = new TelegramLoginPayload( 424242, "Ada", null, null, null, authDate, ComputeTelegramHash(botToken, values)); var service = new TelegramAuthService(CreateConfiguration(botToken)); var verified = service.VerifyLoginPayload(payload, out _, out _); Assert.False(verified); } [Fact] public void VerifyLoginPayload_ShouldRejectMissingRequiredCallbackFields() { var payload = new TelegramLoginPayload( 0, "", null, null, null, DateTimeOffset.UtcNow.ToUnixTimeSeconds(), ""); var service = new TelegramAuthService(CreateConfiguration("test-bot-token")); var verified = service.VerifyLoginPayload(payload, out _, out _); Assert.False(verified); } [Fact] public void TelegramLoginPayload_ShouldDeserializeTelegramWidgetSnakeCaseJson() { var payload = JsonSerializer.Deserialize( """ { "id": 424242, "first_name": "Ada", "last_name": "Lovelace", "username": "ada", "photo_url": "https://t.me/i/userpic/320/ada.jpg", "auth_date": 1714300000, "hash": "abcdef" } """); Assert.NotNull(payload); Assert.Equal(424242L, payload.Id); Assert.Equal("Ada", payload.FirstName); Assert.Equal("Lovelace", payload.LastName); Assert.Equal("ada", payload.Username); Assert.Equal("https://t.me/i/userpic/320/ada.jpg", payload.PhotoUrl); Assert.Equal(1714300000L, payload.AuthDate); Assert.Equal("abcdef", payload.Hash); } private static IConfiguration CreateConfiguration(string botToken) => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Telegram:BotToken"] = botToken }) .Build(); private static QueryCollection CreateQueryCollection(string botToken, Dictionary values) { var hash = ComputeTelegramHash(botToken, values); var queryValues = values.ToDictionary( pair => pair.Key, pair => new StringValues(pair.Value)); queryValues["hash"] = new StringValues(hash); return new QueryCollection(queryValues); } private static string ComputeTelegramHash(string botToken, IReadOnlyDictionary values) { var dataCheckString = string.Join( "\n", values .OrderBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => $"{pair.Key}={pair.Value}")); var secretKey = SHA256.HashData(Encoding.UTF8.GetBytes(botToken)); var hashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString)); return Convert.ToHexString(hashBytes).ToLowerInvariant(); } private static string CreateWebAppInitData(string botToken, IReadOnlyDictionary values) { var hash = ComputeTelegramWebAppHash(botToken, values); var encodedPairs = values .OrderBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}") .Append($"hash={hash}"); return string.Join("&", encodedPairs); } private static string ComputeTelegramWebAppHash(string botToken, IReadOnlyDictionary values) { var dataCheckString = string.Join( "\n", values .OrderBy(pair => pair.Key, StringComparer.Ordinal) .Select(pair => $"{pair.Key}={pair.Value}")); var secretKey = HMACSHA256.HashData(Encoding.UTF8.GetBytes("WebAppData"), Encoding.UTF8.GetBytes(botToken)); var hashBytes = HMACSHA256.HashData(secretKey, Encoding.UTF8.GetBytes(dataCheckString)); return Convert.ToHexString(hashBytes).ToLowerInvariant(); } }