Skip to content

Instantly share code, notes, and snippets.

@v0l
Created March 31, 2023 09:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save v0l/74346ae530896115bfe2504c8cd018d3 to your computer and use it in GitHub Desktop.
Save v0l/74346ae530896115bfe2504c8cd018d3 to your computer and use it in GitHub Desktop.
Nostr auth handler for ASP.NET
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using NNostr.Client;
public static class NostrAuth
{
public const string Scheme = "Nostr";
}
public class NostrAuthOptions : AuthenticationSchemeOptions
{
}
public class NostrAuthHandler : AuthenticationHandler<NostrAuthOptions>
{
public NostrAuthHandler(IOptionsMonitor<NostrAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) :
base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var auth = Request.Headers.Authorization.FirstOrDefault()?.Trim();
if (string.IsNullOrEmpty(auth))
{
return AuthenticateResult.Fail("Missing Authorization header");
}
if (!auth.StartsWith(NostrAuth.Scheme))
{
return AuthenticateResult.Fail("Invalid auth scheme");
}
var token = auth[6..];
var bToken = Convert.FromBase64String(token);
if (string.IsNullOrEmpty(token) || bToken.Length == 0 || bToken[0] != '{')
{
return AuthenticateResult.Fail("Invalid token");
}
var ev = JsonSerializer.Deserialize<NostrEvent>(bToken);
if (ev == default)
{
return AuthenticateResult.Fail("Invalid nostr event");
}
if (!ev.Verify())
{
return AuthenticateResult.Fail("Invalid nostr event, invalid sig");
}
if (ev.Kind != 27_235)
{
return AuthenticateResult.Fail("Invalid nostr event, wrong kind");
}
var diffTime = Math.Abs((ev.CreatedAt!.Value - DateTime.UtcNow).TotalSeconds);
if (diffTime > 10d)
{
return AuthenticateResult.Fail("Invalid nostr event, timestamp out of range");
}
var urlTag = ev.Tags.FirstOrDefault(a => a.TagIdentifier == "url");
var methodTag = ev.Tags.FirstOrDefault(a => a.TagIdentifier == "method");
if (string.IsNullOrEmpty(urlTag?.Data[0]) ||
!new Uri(urlTag.Data[0]).AbsolutePath.Equals(Request.Path, StringComparison.InvariantCultureIgnoreCase))
{
return AuthenticateResult.Fail("Invalid nostr event, url tag invalid");
}
if (string.IsNullOrEmpty(methodTag?.Data[0]) ||
!methodTag.Data[0].Equals(Request.Method, StringComparison.InvariantCultureIgnoreCase))
{
return AuthenticateResult.Fail("Invalid nostr event, method tag invalid");
}
var principal = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, ev.PublicKey)
});
return AuthenticateResult.Success(new(new ClaimsPrincipal(new[] {principal}), Scheme.Name));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment