Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ES256KAuthenticationHandler #40

Merged
merged 1 commit into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading