using System.Net; using System.Net.Http.Headers; using Microsoft.Extensions.Logging.Abstractions; using SGU.AuthBroker.Core.Authentication; using SGU.AuthBroker.Core.Identity; using SGU.AuthBroker.Options; using SGU.AuthBroker.Services; using Xunit; namespace SGU.AuthBroker.Tests; public sealed class NtlmCredentialValidatorTests { private static readonly UserIdentity Student = new( "AL123456", "AL", "123456", InstitutionalRole.Student); [Fact] public async Task RedirectAndSuccessWithoutChallengeNeverAuthorizes() { SequenceHandler handler = new( Redirect("/psulsa/login.aspx?AspxAutoDetectCookieSupport=1"), Response(HttpStatusCode.OK)); NtlmCredentialValidator validator = CreateValidator(handler); NtlmValidationResult result = await validator.ValidateAsync( Student, "test-password", TestContext.Current.CancellationToken); Assert.Equal(NtlmValidationStatus.Unavailable, result.Status); Assert.Equal("NTLM_CHALLENGE_MISSING", result.ErrorCode); Assert.Equal(2, handler.RequestPaths.Count); } [Fact] public async Task CredentialsAreAcceptedOnlyAfterExplicitNtlmChallenge() { SequenceHandler handler = new( Challenge(), Response(HttpStatusCode.OK), Response(HttpStatusCode.OK)); NtlmCredentialValidator validator = CreateValidator(handler); NtlmValidationResult result = await validator.ValidateAsync( Student, "test-password", TestContext.Current.CancellationToken); Assert.Equal(NtlmValidationStatus.Valid, result.Status); Assert.Equal(3, handler.RequestPaths.Count); Assert.Equal("/psulsa/", handler.RequestPaths[0]); Assert.Equal("/psulsa/", handler.RequestPaths[1]); Assert.Equal( "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx", handler.RequestPaths[2]); } [Fact] public async Task RejectedCredentialsAfterChallengeAreInvalid() { SequenceHandler handler = new( Challenge(), Challenge()); NtlmCredentialValidator validator = CreateValidator(handler); NtlmValidationResult result = await validator.ValidateAsync( Student, "wrong-password", TestContext.Current.CancellationToken); Assert.Equal(NtlmValidationStatus.Invalid, result.Status); Assert.Equal("INVALID_INSTITUTIONAL_CREDENTIALS", result.ErrorCode); Assert.Equal(2, handler.RequestPaths.Count); } [Fact] public async Task SessionBootstrapRetriesOriginalRoleProfile() { SequenceHandler handler = new( Challenge(), Redirect("/psulsa/login.aspx?AspxAutoDetectCookieSupport=1"), Redirect("/psulsa/menu.aspx"), Response(HttpStatusCode.OK), Response(HttpStatusCode.OK)); NtlmCredentialValidator validator = CreateValidator(handler); NtlmValidationResult result = await validator.ValidateAsync( Student, "test-password", TestContext.Current.CancellationToken); Assert.Equal(NtlmValidationStatus.Valid, result.Status); Assert.Equal(5, handler.RequestPaths.Count); Assert.Equal("/psulsa/login.aspx", handler.RequestPaths[2]); Assert.Equal("/psulsa/menu.aspx", handler.RequestPaths[3]); Assert.Equal( "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx", handler.RequestPaths[4]); } private static NtlmCredentialValidator CreateValidator(SequenceHandler handler) { BrokerOptions options = new() { Ntlm = new NtlmOptions { Endpoint = "https://sgu.example/", AuthenticationPath = "/psulsa/", StudentProfilePath = "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx", AllowedRedirectHosts = ["sgu.example"], TimeoutSeconds = 5, ProfileTimeoutSeconds = 5 } }; return new NtlmCredentialValidator( options, NullLogger.Instance, (_, _) => handler); } private static HttpResponseMessage Challenge() { HttpResponseMessage response = Response(HttpStatusCode.Unauthorized); response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Negotiate")); response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("NTLM")); return response; } private static HttpResponseMessage Redirect(string location) { HttpResponseMessage response = Response(HttpStatusCode.Found); response.Headers.Location = new Uri(location, UriKind.Relative); return response; } private static HttpResponseMessage Response(HttpStatusCode statusCode) => new(statusCode) { Content = new StringContent("") }; private sealed class SequenceHandler(params HttpResponseMessage[] responses) : HttpMessageHandler { private readonly Queue responses = new(responses); public List RequestPaths { get; } = []; protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); RequestPaths.Add(request.RequestUri!.AbsolutePath); if (responses.Count == 0) { throw new InvalidOperationException("The validator sent more requests than expected."); } HttpResponseMessage response = responses.Dequeue(); response.RequestMessage = request; return Task.FromResult(response); } } }