Add SGU credential provider and authentication broker

This commit is contained in:
2026-08-31 17:48:18 -06:00
parent 5e216f42a4
commit 1f43f200b4
50 changed files with 3226 additions and 236 deletions
@@ -0,0 +1,99 @@
using SGU.AuthBroker.Core.Authentication;
using SGU.AuthBroker.Core.Directory;
using SGU.AuthBroker.Core.Identity;
using Xunit;
namespace SGU.AuthBroker.Core.Tests;
public sealed class AuthenticationWorkflowTests
{
[Fact]
public async Task PassesTheExactOriginalPasswordToNtlmAndActiveDirectory()
{
const string original = "Árbol-Exacto-🔐-NoDerivar-27!";
CapturingNtlmValidator ntlm = new(NtlmValidationResult.Valid());
CapturingDirectorySynchronizer directory = new();
AuthenticationWorkflow workflow = new(ntlm, directory);
AuthenticationFlowResult result = await workflow.AuthenticateAsync(
"do123456",
original,
TestContext.Current.CancellationToken);
Assert.Equal(AuthenticationFlowOutcome.Authorized, result.Outcome);
Assert.Same(original, ntlm.Password);
Assert.Same(original, directory.Password);
Assert.Equal("DO123456", ntlm.UserName);
Assert.Equal(InstitutionalRole.Professor, directory.Identity?.Role);
}
[Fact]
public async Task InvalidNtlmCredentialsNeverReachActiveDirectory()
{
CapturingDirectorySynchronizer directory = new();
AuthenticationWorkflow workflow = new(
new CapturingNtlmValidator(NtlmValidationResult.Invalid()),
directory);
AuthenticationFlowResult result = await workflow.AuthenticateAsync(
"AL123456",
"Wrong",
TestContext.Current.CancellationToken);
Assert.Equal(AuthenticationFlowOutcome.InvalidCredentials, result.Outcome);
Assert.Null(directory.Password);
}
[Fact]
public async Task NtlmOutageIsReportedAsUnavailableForProviderFallback()
{
CapturingDirectorySynchronizer directory = new();
AuthenticationWorkflow workflow = new(
new CapturingNtlmValidator(NtlmValidationResult.Unavailable()),
directory);
AuthenticationFlowResult result = await workflow.AuthenticateAsync(
"AD123456",
"LastKnownPassword",
TestContext.Current.CancellationToken);
Assert.Equal(AuthenticationFlowOutcome.Unavailable, result.Outcome);
Assert.Null(directory.Password);
}
private sealed class CapturingNtlmValidator(NtlmValidationResult result) : INtlmCredentialValidator
{
public string? UserName { get; private set; }
public string? Password { get; private set; }
public Task<NtlmValidationResult> ValidateAsync(string userName, string password, CancellationToken cancellationToken)
{
UserName = userName;
Password = password;
return Task.FromResult(result);
}
}
private sealed class CapturingDirectorySynchronizer : IActiveDirectorySynchronizer
{
public UserIdentity? Identity { get; private set; }
public string? Password { get; private set; }
public Task<DirectorySyncResult> SynchronizeAsync(
UserIdentity identity,
string password,
CancellationToken cancellationToken)
{
Identity = identity;
Password = password;
return Task.FromResult(new DirectorySyncResult(
"LCI",
identity.UserName,
$"{identity.UserName}@lci.lasalle.mx",
true,
false));
}
}
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SGU.AuthBroker.Core\SGU.AuthBroker.Core.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="xunit.v3" Version="4.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,31 @@
using SGU.AuthBroker.Core.Identity;
using Xunit;
namespace SGU.AuthBroker.Core.Tests;
public sealed class UserIdentityClassifierTests
{
[Theory]
[InlineData("DO123456", "DO123456", InstitutionalRole.Professor)]
[InlineData("al000001", "AL000001", InstitutionalRole.Student)]
[InlineData("LCI\\AD654321", "AD654321", InstitutionalRole.Administrative)]
[InlineData("do123456@lci.lasalle.mx", "DO123456", InstitutionalRole.Professor)]
public void MapsPrefixesToExpectedRoles(string input, string expectedUserName, InstitutionalRole expectedRole)
{
Assert.True(UserIdentityClassifier.TryParse(input, out UserIdentity? identity));
Assert.NotNull(identity);
Assert.Equal(expectedUserName, identity.UserName);
Assert.Equal(expectedRole, identity.Role);
}
[Theory]
[InlineData("")]
[InlineData("XX123456")]
[InlineData("DO12345")]
[InlineData("AL1234567")]
[InlineData("AD12A456")]
public void RejectsUnknownOrMalformedUserNames(string input)
{
Assert.False(UserIdentityClassifier.TryParse(input, out _));
}
}