first commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using SequenceAuth.Lib;
|
||||
|
||||
namespace SequenceAuth.Example.Tests;
|
||||
|
||||
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<ISequenceStore>();
|
||||
services.AddSingleton<ISequenceStore, InMemorySequenceStore>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SequenceAuth.Lib;
|
||||
|
||||
namespace SequenceAuth.Example.Tests;
|
||||
|
||||
public class InMemorySequenceStore : ISequenceStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SequenceData> _store = new();
|
||||
|
||||
public Task<Option<SequenceData>> GetSequenceAsync(string sequenceId)
|
||||
{
|
||||
if (_store.TryGetValue(sequenceId, out var data))
|
||||
{
|
||||
return Task.FromResult(Option<SequenceData>.Some(data));
|
||||
}
|
||||
return Task.FromResult(Option<SequenceData>.None());
|
||||
}
|
||||
|
||||
public Task<StoreOutcome> SaveSequenceAsync(string sequenceId, SequenceData data)
|
||||
{
|
||||
_store[sequenceId] = data;
|
||||
return Task.FromResult(StoreOutcome.Success);
|
||||
}
|
||||
|
||||
public Task<StoreOutcome> InvalidateUserSessionsAsync(string userId)
|
||||
{
|
||||
var keysToRemove = _store.Where(kvp => kvp.Value.UserId == userId).Select(kvp => kvp.Key).ToList();
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_store.TryRemove(key, out _);
|
||||
}
|
||||
return Task.FromResult(StoreOutcome.Success);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.10.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\example\SequenceAuth.Example.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using SequenceAuth.Lib;
|
||||
using Xunit;
|
||||
|
||||
namespace SequenceAuth.Example.Tests;
|
||||
|
||||
public class SequenceAuthE2ETests : IClassFixture<CustomWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public SequenceAuthE2ETests(CustomWebApplicationFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
private async Task<string> InitSessionAsync()
|
||||
{
|
||||
var loginResponse = await _client.PostAsJsonAsync("/auth/login", new { username = "testuser" });
|
||||
loginResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var token = loginResponse.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).FirstOrDefault();
|
||||
token.Should().NotBeNullOrWhiteSpace();
|
||||
return token!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestWithoutHeader_ReturnsUnauthorized()
|
||||
{
|
||||
var response = await _client.GetAsync("/secure/get-data");
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidChain_FollowsSequenceAndSucceeds()
|
||||
{
|
||||
// 1. Init Session
|
||||
var token1 = await InitSessionAsync();
|
||||
|
||||
// 2. First Request
|
||||
var request1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
request1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1);
|
||||
var response1 = await _client.SendAsync(request1);
|
||||
|
||||
response1.EnsureSuccessStatusCode();
|
||||
var token2 = response1.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First();
|
||||
token2.Should().NotBeNullOrWhiteSpace();
|
||||
|
||||
// 3. Second Request with New Token
|
||||
var request2 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
request2.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token2);
|
||||
var response2 = await _client.SendAsync(request2);
|
||||
|
||||
response2.EnsureSuccessStatusCode();
|
||||
var token3 = response2.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First();
|
||||
token3.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttackerReplaysToken_CompromisesSequence()
|
||||
{
|
||||
var token1 = await InitSessionAsync();
|
||||
System.Console.WriteLine($"Token 1: {token1}");
|
||||
|
||||
var userRequest1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
userRequest1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1);
|
||||
var userResponse1 = await _client.SendAsync(userRequest1);
|
||||
userResponse1.EnsureSuccessStatusCode();
|
||||
var token2 = userResponse1.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First();
|
||||
System.Console.WriteLine($"Token 2: {token2}");
|
||||
|
||||
var attackerRequest = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
attackerRequest.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1);
|
||||
var attackerResponse = await _client.SendAsync(attackerRequest);
|
||||
System.Console.WriteLine($"Attacker Response: {attackerResponse.StatusCode}");
|
||||
var attackerBody = await attackerResponse.Content.ReadAsStringAsync();
|
||||
System.Console.WriteLine($"Attacker Body: {attackerBody}");
|
||||
|
||||
attackerResponse.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
var userRequest2 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
userRequest2.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token2);
|
||||
var userResponse2 = await _client.SendAsync(userRequest2);
|
||||
|
||||
userResponse2.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttackerStrikesFirst_CompromisesSequence()
|
||||
{
|
||||
var token1 = await InitSessionAsync();
|
||||
|
||||
var attackerRequest1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
attackerRequest1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1);
|
||||
var attackerResponse1 = await _client.SendAsync(attackerRequest1);
|
||||
|
||||
attackerResponse1.EnsureSuccessStatusCode();
|
||||
var token2 = attackerResponse1.Headers.GetValues(new SequenceAuthOptions().NextHeaderName).First();
|
||||
|
||||
var userRequest1 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
userRequest1.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token1);
|
||||
var userResponse1 = await _client.SendAsync(userRequest1);
|
||||
|
||||
System.Console.WriteLine($"User Response 1: {userResponse1.StatusCode}");
|
||||
userResponse1.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
|
||||
var attackerRequest2 = new HttpRequestMessage(HttpMethod.Get, "/secure/get-data");
|
||||
attackerRequest2.Headers.Add(new SequenceAuthOptions().AuthHeaderName, token2);
|
||||
var attackerResponse2 = await _client.SendAsync(attackerRequest2);
|
||||
|
||||
attackerResponse2.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\lib\SequenceAuth.Lib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,88 @@
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using SequenceAuth.Lib;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SequenceAuth.Lib.Tests;
|
||||
|
||||
public class SequenceManagerTests
|
||||
{
|
||||
private readonly Mock<ISequenceStore> _mockStore;
|
||||
private readonly SequenceManager _manager;
|
||||
|
||||
public SequenceManagerTests()
|
||||
{
|
||||
_mockStore = new Mock<ISequenceStore>();
|
||||
_manager = new SequenceManager(_mockStore.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndRotateAsync_ValidToken_ReturnsSuccessAndGeneratesNewToken()
|
||||
{
|
||||
// Arrange
|
||||
var token = "valid-token";
|
||||
var nextToken = "pre-generated-next-token";
|
||||
var sequenceData = new SequenceData("user123", 100, SequenceState.Active, nextToken);
|
||||
|
||||
_mockStore.Setup(s => s.GetSequenceAsync(token))
|
||||
.ReturnsAsync(Option<SequenceData>.Some(sequenceData));
|
||||
|
||||
_mockStore.Setup(s => s.SaveSequenceAsync(It.IsAny<string>(), It.IsAny<SequenceData>()))
|
||||
.ReturnsAsync(StoreOutcome.Success);
|
||||
|
||||
// Act
|
||||
var result = await _manager.ValidateAndRotateAsync(token);
|
||||
|
||||
// Assert
|
||||
result.Outcome.Should().Be(ValidationOutcome.Success);
|
||||
result.NextSequence.State.Should().Be(OptionState.Some);
|
||||
result.NextSequence.Value.Should().Be(nextToken);
|
||||
|
||||
// Ensure the old sequence was marked as Rotated
|
||||
_mockStore.Verify(s => s.SaveSequenceAsync(token, It.Is<SequenceData>(d =>
|
||||
d.State == SequenceState.Rotated)), Times.Once);
|
||||
|
||||
// Ensure the new sequence was saved
|
||||
_mockStore.Verify(s => s.SaveSequenceAsync(nextToken, It.Is<SequenceData>(d =>
|
||||
d.State == SequenceState.Active)), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndRotateAsync_RotatedToken_ReturnsCompromisedAndDestroysSession()
|
||||
{
|
||||
// Arrange
|
||||
var token = "compromised-token";
|
||||
var sequenceData = new SequenceData("user123", 100, SequenceState.Rotated, "some-next-token");
|
||||
|
||||
_mockStore.Setup(s => s.GetSequenceAsync(token))
|
||||
.ReturnsAsync(Option<SequenceData>.Some(sequenceData));
|
||||
|
||||
// Act
|
||||
var result = await _manager.ValidateAndRotateAsync(token);
|
||||
|
||||
// Assert
|
||||
result.Outcome.Should().Be(ValidationOutcome.CompromisedSequenceDetected);
|
||||
result.NextSequence.State.Should().Be(OptionState.None);
|
||||
|
||||
// Ensure InvalidateUserSessionsAsync was called for the user
|
||||
_mockStore.Verify(s => s.InvalidateUserSessionsAsync("user123"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAndRotateAsync_MissingToken_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var token = "invalid-token";
|
||||
|
||||
_mockStore.Setup(s => s.GetSequenceAsync(token))
|
||||
.ReturnsAsync(Option<SequenceData>.None());
|
||||
|
||||
// Act
|
||||
var result = await _manager.ValidateAndRotateAsync(token);
|
||||
|
||||
// Assert
|
||||
result.Outcome.Should().Be(ValidationOutcome.SequenceNotFound);
|
||||
result.NextSequence.State.Should().Be(OptionState.None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user