test(security): cookie+JWT roundtrip, role mapper, LDAP escape/RDN helpers
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Security.Jwt;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Security.Tests;
|
||||
|
||||
public sealed class JwtTokenServiceTests
|
||||
{
|
||||
private const string TestKey = "this-is-a-32-byte-test-signing-key!!";
|
||||
|
||||
private static JwtTokenService NewService(string key = TestKey, int expiryMinutes = 15) =>
|
||||
new(Options.Create(new JwtOptions
|
||||
{
|
||||
SigningKey = key,
|
||||
Issuer = "otopcua-test",
|
||||
Audience = "otopcua-test",
|
||||
ExpiryMinutes = expiryMinutes,
|
||||
}), NullLogger<JwtTokenService>.Instance);
|
||||
|
||||
[Fact]
|
||||
public void Short_signing_key_throws()
|
||||
{
|
||||
Should.Throw<InvalidOperationException>(() => NewService("too-short"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issue_then_validate_roundtrips_claims()
|
||||
{
|
||||
var jwt = NewService();
|
||||
var token = jwt.Issue("Joe User", "joe", new[] { "ReadOnly", "AlarmAck" });
|
||||
|
||||
jwt.TryValidate(token, out var principal).ShouldBeTrue();
|
||||
principal.ShouldNotBeNull();
|
||||
principal!.FindFirst(JwtTokenService.UsernameClaimType)!.Value.ShouldBe("joe");
|
||||
principal.FindFirst(JwtTokenService.DisplayNameClaimType)!.Value.ShouldBe("Joe User");
|
||||
principal.FindAll(JwtTokenService.RoleClaimType)
|
||||
.Select(c => c.Value).ShouldBe(new[] { "ReadOnly", "AlarmAck" }, ignoreOrder: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tampered_token_is_rejected()
|
||||
{
|
||||
var jwt = NewService();
|
||||
var token = jwt.Issue("Joe", "joe", Array.Empty<string>());
|
||||
|
||||
// Corrupt the payload — flip a char in the middle segment.
|
||||
var parts = token.Split('.');
|
||||
parts[1] = parts[1][..^2] + "AA";
|
||||
var tampered = string.Join('.', parts);
|
||||
|
||||
jwt.TryValidate(tampered, out var principal).ShouldBeFalse();
|
||||
principal.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Expired_token_is_rejected()
|
||||
{
|
||||
// Issue with -1 min expiry so it's already past at validate time.
|
||||
var jwt = NewService(expiryMinutes: -1);
|
||||
var token = jwt.Issue("Joe", "joe", Array.Empty<string>());
|
||||
|
||||
jwt.TryValidate(token, out var principal).ShouldBeFalse();
|
||||
principal.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cross_key_token_is_rejected()
|
||||
{
|
||||
var issuer = NewService();
|
||||
var token = issuer.Issue("Joe", "joe", Array.Empty<string>());
|
||||
|
||||
var different = NewService("a-different-32-byte-signing-key!!!");
|
||||
different.TryValidate(token, out var principal).ShouldBeFalse();
|
||||
principal.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Security.Tests;
|
||||
|
||||
public sealed class LdapHelperTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("joe", "joe")]
|
||||
[InlineData("jo*e", "jo\\2ae")]
|
||||
[InlineData("jo(e", "jo\\28e")]
|
||||
[InlineData("jo)e", "jo\\29e")]
|
||||
[InlineData("jo\\e", "jo\\5ce")]
|
||||
public void EscapeLdapFilter_escapes_special_chars(string input, string expected)
|
||||
{
|
||||
LdapAuthService.EscapeLdapFilter(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("cn=joe,ou=Admins,dc=lmxopcua,dc=local", "Admins")]
|
||||
[InlineData("cn=alice,dc=lmxopcua,dc=local", null)]
|
||||
[InlineData("ou=Admins,dc=lmxopcua,dc=local", "Admins")]
|
||||
public void ExtractOuSegment_returns_first_ou(string dn, string? expected)
|
||||
{
|
||||
LdapAuthService.ExtractOuSegment(dn).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("cn=Admins,dc=lmxopcua,dc=local", "Admins")]
|
||||
[InlineData("cn=Admins", "Admins")]
|
||||
[InlineData("Admins", "Admins")]
|
||||
public void ExtractFirstRdnValue_handles_full_and_short_dns(string dn, string expected)
|
||||
{
|
||||
LdapAuthService.ExtractFirstRdnValue(dn).ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Security.Tests;
|
||||
|
||||
public sealed class RoleMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Empty_mapping_returns_empty()
|
||||
{
|
||||
RoleMapper.Map(new[] { "Admins" }, new Dictionary<string, string>())
|
||||
.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Maps_group_to_role()
|
||||
{
|
||||
RoleMapper.Map(
|
||||
new[] { "AdminGroup" },
|
||||
new Dictionary<string, string> { ["AdminGroup"] = "FleetAdmin" })
|
||||
.ShouldBe(new[] { "FleetAdmin" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Case_insensitive_group_match()
|
||||
{
|
||||
RoleMapper.Map(
|
||||
new[] { "admingroup" },
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["AdminGroup"] = "FleetAdmin",
|
||||
})
|
||||
.ShouldBe(new[] { "FleetAdmin" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Multiple_groups_dedup_roles()
|
||||
{
|
||||
var roles = RoleMapper.Map(
|
||||
new[] { "AdminGroup", "AlsoAdmin" },
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["AdminGroup"] = "FleetAdmin",
|
||||
["AlsoAdmin"] = "FleetAdmin",
|
||||
});
|
||||
|
||||
roles.ShouldBe(new[] { "FleetAdmin" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Security.Tests</RootNamespace>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3"/>
|
||||
<PackageReference Include="Shouldly"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Server\ZB.MOM.WW.OtOpcUa.Security\ZB.MOM.WW.OtOpcUa.Security.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Core\ZB.MOM.WW.OtOpcUa.Configuration\ZB.MOM.WW.OtOpcUa.Configuration.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user