Skip to content

Instantly share code, notes, and snippets.

@eman41
Last active October 22, 2021 15:10
Show Gist options
  • Save eman41/683e7a9cf727921c1dc9d5fd5af54c4f to your computer and use it in GitHub Desktop.
Save eman41/683e7a9cf727921c1dc9d5fd5af54c4f to your computer and use it in GitHub Desktop.
Example verifying a Slack slash command POST request (C#, ASP.NET, Core 3.1)
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// In ASP .NET Core 3.1, there is some required middleware in Startup.cs to enable reading the raw body text more than once
// Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
// !!!!! REQUIRED FOR READING THE FULL BODY TEXT IN AN API CONTROLLER
// This must come before mapping routes/controllers
app.Use(next => context =>
{
context.Request.EnableBuffering();
return next(context);
});
// !!!!! END MIDDLEWARE CODE. Below this line is standard initialization
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
public enum VerificationStatus
{
/// <summary>
/// Request is valid and may be processed
/// </summary>
Ok = 0,
/// <summary>
/// Request is too old and rejected
/// </summary>
Expired,
/// <summary>
/// Request could not be validated
/// </summary>
Invalid,
/// <summary>
/// Verification service is not ready or has not been properly setup
/// </summary>
NotReady
}
public interface IVerificationService
{
/// <summary>
/// Verify the provided web request
/// </summary>
/// <param name="toVerify">Incoming http request</param>
/// <returns>Verification result</returns>
VerificationStatus Verify(HttpContext toVerify);
}
// Uses an IVerificationService interface for better integration with ASP's Service provider
public class SlackVerificationService : IVerificationService
{
private string _secret;
private const string SlackEnvVar = "SLACK_SIGNING_SECRET";
public void Init(bool allowPassThrough = true)
{
// There are lots of methods for storing/retrieving the signing secret
// This one is good for development, but you probably want to use a platform
// supported vault or something like that. In most cases EnvVars are fine.
_secret = Environment.GetEnvironmentVariable(SlackEnvVar);
}
public VerificationStatus Verify(HttpContext toVerify)
{
if (string.IsNullOrEmpty(_secret))
{
return VerificationStatus.NotReady;
}
var buffer = new byte[Convert.ToInt32(toVerify.Request.ContentLength)];
toVerify.Request.Body.ReadAsync(buffer, 0, buffer.Length);
string body = Encoding.UTF8.GetString(buffer);
string timestamp = toVerify.Request.Headers["X-Slack-Request-Timestamp"];
// We don't test message age in this example
string signature = toVerify.Request.Headers["X-Slack-Signature"];
if (timestamp != null && signature != null)
{
Encoding encoding = Encoding.UTF8;
string verificationString = $"v0:{timestamp}:{body}";
var hmac = new HMACSHA256(encoding.GetBytes(_secret));
byte[] hashBytes = hmac.ComputeHash(encoding.GetBytes(verificationString));
// This line cleans up the hex version of the hash to match Slack's signature format
string hexDigest = "v0=" + BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
int compare = string.Compare(hexDigest, signature, StringComparison.CurrentCulture);
if (compare == 0)
{
return VerificationStatus.Ok;
}
}
return VerificationStatus.Invalid;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment