如何验证对 .NET 中的 Discord 交互 webhook 发出的请求?

How can you authenticate requests made to a Discord interactions webhook in .NET?

根据 Discord documentation,webhook 必须验证每个请求的 headers 才能被接受。该文档提供了以下代码示例:

const nacl = require('tweetnacl');

// Your public key can be found on your application in the Developer Portal
const PUBLIC_KEY = 'APPLICATION_PUBLIC_KEY';

const signature = req.get('X-Signature-Ed25519');
const timestamp = req.get('X-Signature-Timestamp');
const body = req.rawBody; // rawBody is expected to be a string, not raw bytes

const isVerified = nacl.sign.detached.verify(
  Buffer.from(timestamp + body),
  Buffer.from(signature, 'hex'),
  Buffer.from(PUBLIC_KEY, 'hex')
);

if (!isVerified) {
  return res.status(401).end('invalid request signature');
}

如何在 .NET 5.0 中执行此操作?我没能找到任何 Ed25519 验证的例子。

此实现需要 NSec.Cryptography NuGet package.

首先,您必须创建一个 ActionFilter 以放置在您的 WebAPI 控制器或端点上。最简单的方法是扩展 ActionFilterAttribute:

public class DiscordAuthorizationActionFilterAttribute : ActionFilterAttribute
{
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // This is needed to move the request stream to the beginning as it has already been evaluated for model binding
        context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);

        var signature = context.HttpContext.Request.Headers["X-Signature-Ed25519"].FirstOrDefault();
        var timestamp = context.HttpContext.Request.Headers["X-Signature-Timestamp"].FirstOrDefault();
        var body = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
        var key = "{YOUR API KEY HERE}";
        var algorithm = SignatureAlgorithm.Ed25519;
        var publicKey = PublicKey.Import(algorithm, GetBytesFromHexString(key), KeyBlobFormat.RawPublicKey);
        var data = Encoding.UTF8.GetBytes(timestamp + body);
        var verified = algorithm.Verify(publicKey, data, GetBytesFromHexString(signature));

        if (!verified)
            context.Result = new UnauthorizedObjectResult("Invalid request");
        else
            await next();
    }

    private byte[] GetBytesFromHexString(string hex)
    {
        var length = hex.Length;
        var bytes = new byte[length / 2];

        for (int i = 0; i < length; i += 2)
            bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);

        return bytes;
    }
}

注意这条评论:

// This is needed to move the request stream to the beginning as it has already been evaluated for model binding
context.HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);

要允许重用请求主体流,您必须在第一次访问请求管道之前在请求管道中显式启用它,可能是在模型绑定期间。为此,您可以在 Startup.cs 中添加一个简单的步骤:

public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
    app.UseStaticFiles();
    app.UseRouting();

    // This is needed to retrieve request body as JSON string in ActionFilter
    app.Use(async (context, next) =>
    {
        var controller = context.Request.RouteValues["controller"] as string;

        if (controller == "Discord")
            context.Request.EnableBuffering();

        await next();
    });

    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

注意检查控制器名称;因为我将属性放在 DiscordController 上,所以存储在 RouteValues 集合中的控制器值是“Discord”。

最后,只需将属性添加到接受 POST 请求的端点:

public class DiscordController : ControllerBase
{
    [DiscordAuthorizationActionFilter]
    [HttpPost]
    public async Task<IActionResult> PostAsync(DiscordInteraction interaction)
    {
        if (interaction == null)
            return BadRequest();

        if (interaction.Type == DiscordInteractionType.Ping)
            return Ok(new { Type = 1 });

        // Request processing here

        return Ok();
    }
}

请注意,DiscordInteraction 模型是自定义代码,在我所知道的任何库中都不可用。按照文档创建它很简单。为了测试这一点,我发现使用 ngrok 将请求从 Discord 路由到我的开发环境很有帮助。