Skip to content

Commit

Permalink
Merge pull request #40 from Atralupus/feat/jwt-auth
Browse files Browse the repository at this point in the history
Implement ES256KAuthenticationHandler
  • Loading branch information
Atralupus authored Jan 2, 2025
2 parents 8cae5ba + dde68ab commit 3a653a4
Show file tree
Hide file tree
Showing 22 changed files with 739 additions and 344 deletions.
228 changes: 228 additions & 0 deletions ArenaService.Tests/Auth/ES256KAuthenticationHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
namespace ArenaService.Tests.Auth;

using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using global::ArenaService.Auth;
using Libplanet.Common;
using Libplanet.Crypto;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Newtonsoft.Json;
using Xunit;

public class ES256KAuthenticationHandlerTests
{
private readonly Mock<IOptionsMonitor<AuthenticationSchemeOptions>> _optionsMonitor;
private readonly Mock<ILoggerFactory> _loggerFactory;
private readonly Mock<UrlEncoder> _encoder;
private readonly Mock<ISystemClock> _clock;

public ES256KAuthenticationHandlerTests()
{
_optionsMonitor = new Mock<IOptionsMonitor<AuthenticationSchemeOptions>>();
_optionsMonitor
.Setup(m => m.Get(It.IsAny<string>()))
.Returns(new AuthenticationSchemeOptions());
_optionsMonitor.Setup(m => m.CurrentValue).Returns(new AuthenticationSchemeOptions());
_loggerFactory = new Mock<ILoggerFactory>();
var mockLogger = new Mock<ILogger<ES256KAuthenticationHandler>>();
_loggerFactory.Setup(x => x.CreateLogger(It.IsAny<string>())).Returns(mockLogger.Object);
_encoder = new Mock<UrlEncoder>();
_clock = new Mock<ISystemClock>();
}

[Fact]
public async Task HandleAuthenticateAsync_ValidUserRole_ReturnsSuccess()
{
var privateKey = new PrivateKey();
var jwt = JwtCreator.CreateJwt(privateKey, role: "User");
var publicKey = privateKey.PublicKey;
var address = publicKey.Address.ToString();

var context = new DefaultHttpContext();
context.Request.Headers["Authorization"] = $"Bearer {jwt}";

var handler = new ES256KAuthenticationHandler(
_optionsMonitor.Object,
_loggerFactory.Object,
_encoder.Object,
_clock.Object
);

await handler.InitializeAsync(
new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)),
context
);

var result = await handler.AuthenticateAsync();

Assert.True(result.Succeeded);
Assert.NotNull(result.Principal);
Assert.Equal(publicKey.ToString(), result.Principal.FindFirst("public_key")?.Value);
Assert.Equal(address, result.Principal.FindFirst("address")?.Value);
Assert.Equal("test", result.Principal.FindFirst("avatar_address")?.Value);
}

[Fact]
public async Task HandleAuthenticateAsync_ValidAdminRole_ReturnsSuccess()
{
var privateKey = new PrivateKey();
var jwt = JwtCreator.CreateJwt(privateKey, role: "Admin");
var publicKey = privateKey.PublicKey;

Environment.SetEnvironmentVariable("ALLOWED_ADMIN_PUBLIC_KEY", publicKey.ToHex(true));

var context = new DefaultHttpContext();
context.Request.Headers["Authorization"] = $"Bearer {jwt}";

var handler = new ES256KAuthenticationHandler(
_optionsMonitor.Object,
_loggerFactory.Object,
_encoder.Object,
_clock.Object
);

await handler.InitializeAsync(
new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)),
context
);

var result = await handler.AuthenticateAsync();

Assert.True(result.Succeeded);
Assert.NotNull(result.Principal);
Assert.Equal(publicKey.ToString(), result.Principal.FindFirst("public_key")?.Value);
}

[Fact]
public async Task HandleAuthenticateAsync_InvalidAdminKey_ReturnsFail()
{
var privateKey = new PrivateKey();
var jwt = JwtCreator.CreateJwt(privateKey, role: "Admin");

Environment.SetEnvironmentVariable(
"ALLOWED_ADMIN_PUBLIC_KEY",
new PrivateKey().PublicKey.ToHex(true)
);

var context = new DefaultHttpContext();
context.Request.Headers["Authorization"] = $"Bearer {jwt}";

var handler = new ES256KAuthenticationHandler(
_optionsMonitor.Object,
_loggerFactory.Object,
_encoder.Object,
_clock.Object
);

await handler.InitializeAsync(
new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)),
context
);

var result = await handler.AuthenticateAsync();

Assert.False(result.Succeeded);
Assert.Null(result.Principal);
}

[Fact]
public async Task HandleAuthenticateAsync_MissingToken_ReturnsFail()
{
var context = new DefaultHttpContext();
var handler = new ES256KAuthenticationHandler(
_optionsMonitor.Object,
_loggerFactory.Object,
_encoder.Object,
_clock.Object
);

await handler.InitializeAsync(
new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)),
context
);

var result = await handler.AuthenticateAsync();

Assert.False(result.Succeeded);
Assert.Null(result.Principal);
Assert.Contains("Missing or invalid Authorization header.", result.Failure.Message);
}

[Fact]
public async Task HandleAuthenticateAsync_InvalidTokenFormat_ReturnsFail()
{
var context = new DefaultHttpContext();
context.Request.Headers["Authorization"] = "Bearer invalid.token.format";

var handler = new ES256KAuthenticationHandler(
_optionsMonitor.Object,
_loggerFactory.Object,
_encoder.Object,
_clock.Object
);

await handler.InitializeAsync(
new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)),
context
);

var result = await handler.AuthenticateAsync();

Assert.False(result.Succeeded);
Assert.Null(result.Principal);
Assert.Contains("Invalid token.", result.Failure.Message);
}

[Fact]
public async Task HandleAuthenticateAsync_ExpiredToken_ReturnsFail()
{
var privateKey = new PrivateKey();
var payload = new
{
iss = "user",
avt_adr = "test",
pbk = privateKey.PublicKey.ToHex(true),
role = "User",
iat = DateTimeOffset.UtcNow.AddHours(-2).ToUnixTimeSeconds(),
exp = DateTimeOffset.UtcNow.AddHours(-1).ToUnixTimeSeconds()
};

string payloadJson = JsonConvert.SerializeObject(payload);
byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadJson);
byte[] hash = HashDigest<SHA256>.DeriveFrom(payloadBytes).ToByteArray();
byte[] signature = privateKey.Sign(hash);

string signatureBase64 = Convert.ToBase64String(signature);
string headerJson = JsonConvert.SerializeObject(new { alg = "ES256K", typ = "JWT" });
string headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(headerJson));
string payloadBase64 = Convert.ToBase64String(payloadBytes);

var jwt = $"{headerBase64}.{payloadBase64}.{signatureBase64}";

var context = new DefaultHttpContext();
context.Request.Headers["Authorization"] = $"Bearer {jwt}";

var handler = new ES256KAuthenticationHandler(
_optionsMonitor.Object,
_loggerFactory.Object,
_encoder.Object,
_clock.Object
);

await handler.InitializeAsync(
new AuthenticationScheme("ES256K", null, typeof(ES256KAuthenticationHandler)),
context
);

var result = await handler.AuthenticateAsync();

Assert.False(result.Succeeded);
Assert.Null(result.Principal);
Assert.Contains("Invalid token.", result.Failure.Message);
}
}
37 changes: 37 additions & 0 deletions ArenaService.Tests/Auth/JwtCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace ArenaService.Tests.Auth;

using System;
using System.Security.Cryptography;
using System.Text;
using Libplanet.Common;
using Libplanet.Crypto;
using Newtonsoft.Json;

public class JwtCreator
{
public static string CreateJwt(PrivateKey privateKey, string role = "user")
{
var payload = new
{
iss = "user",
avt_adr = "test",
sub = privateKey.PublicKey.ToHex(true),
role,
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
exp = DateTimeOffset.UtcNow.AddMinutes(60).ToUnixTimeSeconds()
};

string payloadJson = JsonConvert.SerializeObject(payload);
byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadJson);

byte[] signature = privateKey.Sign(payloadBytes);

string signatureBase64 = Convert.ToBase64String(signature);

string headerJson = JsonConvert.SerializeObject(new { alg = "ES256K", typ = "JWT" });
string headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(headerJson));
string payloadBase64 = Convert.ToBase64String(payloadBytes);

return $"{headerBase64}.{payloadBase64}.{signatureBase64}";
}
}
37 changes: 12 additions & 25 deletions ArenaService.Tests/Controllers/AvailableOpponentControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using ArenaService.Dtos;
using ArenaService.Models;
using ArenaService.Repositories;
using ArenaService.Services;
using ArenaService.Tests.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -14,33 +14,28 @@ namespace ArenaService.Tests.Controllers;
public class AvailableOpponentControllerTests
{
private readonly AvailableOpponentController _controller;
private Mock<IAvailableOpponentRepository> _availableOpponentRepositoryMock;
private AvailableOpponentService _availableOpponentService;
private Mock<IParticipantRepository> _participantRepositoryMock;
private ParticipantService _participantService;
private Mock<IAvailableOpponentRepository> _availableOpponentRepoMock;
private Mock<IParticipantRepository> _participantRepoMock;

public AvailableOpponentControllerTests()
{
var availableOpponentRepositoryMock = new Mock<IAvailableOpponentRepository>();
_availableOpponentRepositoryMock = availableOpponentRepositoryMock;
_availableOpponentService = new AvailableOpponentService(
_availableOpponentRepositoryMock.Object
);
var participantRepositoryMock = new Mock<IParticipantRepository>();
_participantRepositoryMock = participantRepositoryMock;
_participantService = new ParticipantService(_participantRepositoryMock.Object);
var availableOpponentRepoMock = new Mock<IAvailableOpponentRepository>();
_availableOpponentRepoMock = availableOpponentRepoMock;
var participantRepoMock = new Mock<IParticipantRepository>();
_participantRepoMock = participantRepoMock;
_controller = new AvailableOpponentController(
_availableOpponentService,
_participantService
_availableOpponentRepoMock.Object,
_participantRepoMock.Object
);
}

[Fact]
public async Task GetAvailableOpponents_WithValidHeader_ReturnsOk()
{
var avatarAddress = "DDF1472fD5a79B8F46C28e7643eDEF045e36BD3d";
ControllerTestUtils.ConfigureMockHttpContextWithAuth(_controller, avatarAddress);

_participantRepositoryMock
_participantRepoMock
.Setup(repo => repo.GetParticipantByAvatarAddressAsync(1, avatarAddress))
.ReturnsAsync(
new Participant
Expand All @@ -52,7 +47,7 @@ public async Task GetAvailableOpponents_WithValidHeader_ReturnsOk()
}
);

_availableOpponentRepositoryMock
_availableOpponentRepoMock
.Setup(repo => repo.GetAvailableOpponents(1))
.ReturnsAsync(
[
Expand Down Expand Up @@ -87,14 +82,6 @@ public async Task GetAvailableOpponents_WithValidHeader_ReturnsOk()
]
);

_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext()
};
_controller.ControllerContext.HttpContext.User = new ClaimsPrincipal(
new ClaimsIdentity([new Claim("avatar", avatarAddress)])
);

var result = await _controller.GetAvailableOpponents(1);

var okResult = Assert.IsType<Ok<AvailableOpponentsResponse>>(result.Result);
Expand Down
Loading

0 comments on commit 3a653a4

Please sign in to comment.