311 lines
11 KiB
C#
311 lines
11 KiB
C#
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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["auth_date"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
|
|
["first_name"] = "Ada",
|
|
["id"] = "424242"
|
|
};
|
|
var query = CreateQueryCollection(botToken, values);
|
|
var invalidQuery = new QueryCollection(new Dictionary<string, StringValues>(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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<string, string>
|
|
{
|
|
["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<TelegramLoginPayload>(
|
|
"""
|
|
{
|
|
"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<string, string?>
|
|
{
|
|
["Telegram:BotToken"] = botToken
|
|
})
|
|
.Build();
|
|
|
|
private static QueryCollection CreateQueryCollection(string botToken, Dictionary<string, string> 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<string, string> 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<string, string> 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<string, string> 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();
|
|
}
|
|
}
|