From f64f9f16537893a3f643848c9769d291b202fc69 Mon Sep 17 00:00:00 2001 From: "s.naidenov" Date: Thu, 18 Sep 2025 17:17:32 +0700 Subject: [PATCH 1/3] DEV-270 --- .../LdapFirstFactorProcessorTests.cs | 200 +++------- .../PapAuthProcessorTests.cs | 191 +++++++++ .../Unit/MsChapV2/Rfc2759Tests.cs | 77 ++++ .../Unit/MsChapV2/Rfc2868Tests.cs | 32 ++ .../Unit/MsChapV2/Rfc3079Tests.cs | 38 ++ .../Core/FirstFactor/LdapAuth/AuthResult.cs | 8 + .../LdapAuth/ILdapAuthProcessor.cs | 10 + .../FirstFactor/LdapAuth/LdapAuthProvider.cs | 23 ++ .../Core/FirstFactor/LdapAuth/MsChapV2/MD4.cs | 364 ++++++++++++++++++ .../MsChapV2/MsChapV2AuthProcessor.cs | 115 ++++++ .../FirstFactor/LdapAuth/MsChapV2/Rfc2759.cs | 140 +++++++ .../FirstFactor/LdapAuth/MsChapV2/Rfc2868.cs | 133 +++++++ .../FirstFactor/LdapAuth/MsChapV2/Rfc3079.cs | 95 +++++ .../FirstFactor/LdapAuth/PapAuthProcessor.cs | 115 ++++++ .../FirstFactor/LdapFirstFactorProcessor.cs | 122 +----- .../Core/Pipeline/IResponseInformation.cs | 6 +- .../Core/Pipeline/ResponseInformation.cs | 4 + .../Extensions/ServiceCollectionExtensions.cs | 5 + .../Services/Ldap/GetUserPasswordRequest.cs | 23 ++ .../Services/Ldap/ILdapProfileService.cs | 1 + .../Services/Ldap/LdapProfileService.cs | 7 + 21 files changed, 1456 insertions(+), 253 deletions(-) create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2759Tests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2868Tests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc3079Tests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/AuthResult.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/ILdapAuthProcessor.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/LdapAuthProvider.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MD4.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MsChapV2AuthProcessor.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2759.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2868.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc3079.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs create mode 100644 src/Multifactor.Radius.Adapter.v2/Services/Ldap/GetUserPasswordRequest.cs diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs index 32acead..2b23f5c 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs @@ -1,19 +1,12 @@ -using System.DirectoryServices.Protocols; -using System.Net; -using System.Runtime.InteropServices; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Radius.Adapter.v2.Core; using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; +using Multifactor.Radius.Adapter.v2.Core.Radius; using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; -using ILdapConnectionFactory = Multifactor.Core.Ldap.Connection.LdapConnectionFactory.ILdapConnectionFactory; namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; @@ -21,174 +14,87 @@ namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; public class LdapFirstFactorProcessorTests { [Fact] - public async Task LdapFirstFactorProcessor_CorrectCredentials_ShouldAccept() + public async Task LdapFirstFactorProcessor_NoRequestPacket_ShouldThrow() { - var sensitiveData = GetConfig(); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), - NullLogger.Instance); + var authProviderMock = new Mock(); + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); var contextMock = new Mock(); - var packetMock = new Mock(); - packetMock.Setup(x => x.UserName).Returns(sensitiveData["UserName"]); - packetMock.Setup(x => x.TryGetUserPassword()).Returns(sensitiveData["Password"]); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - - var serverSettings = new Mock(); - serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - var authState = new AuthenticationState(); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - - var transformRules = new UserNameTransformRules(); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(sensitiveData["Password"], PreAuthModeDescriptor.Default)); - await processor.ProcessFirstFactor(contextMock.Object); - Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); + contextMock.Setup(x => x.RequestPacket).Returns(() => null); + + await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); } - + [Fact] - public async Task LdapFirstFactorProcessor_IncorrectPassword_ShouldReject() + public async Task LdapFirstFactorProcessor_NoLdapServerConfiguration_ShouldThrow() { - var sensitiveData = GetConfig(); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), - NullLogger.Instance); + var authProviderMock = new Mock(); + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); + var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns(sensitiveData["UserName"]); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - await processor.ProcessFirstFactor(contextMock.Object); - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(() => null); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); + + await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); } - + [Fact] - public async Task LdapFirstFactorProcessor_IncorrectLogin_ShouldReject() + public async Task LdapFirstFactorProcessor_NoAuthProcessors_ShouldThrow() { - var sensitiveData = GetConfig(); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), - NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("userName"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns(sensitiveData["Password"]); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + var authProviderMock = new Mock(); + authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(() => null); - await processor.ProcessFirstFactor(contextMock.Object); - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyLogin_ShouldReject(string login) - { - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), - NullLogger.Instance); var contextMock = new Mock(); - var packetMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns(login); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("correctLogin"); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - - await processor.ProcessFirstFactor(contextMock.Object); - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); + + await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyPassword_ShouldReject(string pwd) + + [Fact] + public async Task LdapFirstFactorProcessor_AuthFailed_ShouldReject() { - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), - NullLogger.Instance); + var authProviderMock = new Mock(); + var authProcessorMock = new Mock(); + authProcessorMock.Setup(x=> x.Auth(It.IsAny())).ReturnsAsync(new AuthResult() { IsSuccess = false }); + authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(authProcessorMock.Object); + + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); + var contextMock = new Mock(); - var packetMock = new Mock(); + var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("correctLogin"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns(pwd); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); await processor.ProcessFirstFactor(contextMock.Object); Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); } - + [Fact] - public async Task LdapFirstFactorProcessor_MustChangePasswordResponse_ShouldReject() + public async Task LdapFirstFactorProcessor_AuthSucceed_ShouldReject() { - var factoryMock = new Mock(); - factoryMock.Setup(x => x.CreateConnection(It.IsAny())).Throws(GetLdapException); - factoryMock.Setup(x => x.TargetPlatform).Returns(OSPlatform.Windows); - var factory = new CustomLdapConnectionFactory([factoryMock.Object]); - var processor = new LdapFirstFactorProcessor(factory, NullLogger.Instance); + var authProviderMock = new Mock(); + var authProcessorMock = new Mock(); + authProcessorMock.Setup(x=> x.Auth(It.IsAny())).ReturnsAsync(new AuthResult() { IsSuccess = true }); + authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(authProcessorMock.Object); + + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); + var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); + var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("user"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns("your.domain"); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.SetupProperty(x => x.MustChangePasswordDomain); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - var context = contextMock.Object; - - await processor.ProcessFirstFactor(context); - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - Assert.Equal("your.domain", context.MustChangePasswordDomain); - } - - private LdapException GetLdapException() - { - var ex = new LdapException(1, "message", "data 773"); - return ex; - } - - private Dictionary GetConfig() - { - return ConfigUtils.GetConfigSensitiveData("LdapFirstFactorProcessorTests.txt", "|"); + + await processor.ProcessFirstFactor(contextMock.Object); + Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs new file mode 100644 index 0000000..fc73a3e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs @@ -0,0 +1,191 @@ +using System.DirectoryServices.Protocols; +using System.Net; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Multifactor.Core.Ldap.Connection; +using Multifactor.Radius.Adapter.v2.Core; +using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; +using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; +using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Tests.Fixture; +using ILdapConnectionFactory = Multifactor.Core.Ldap.Connection.LdapConnectionFactory.ILdapConnectionFactory; + +namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; + +[Collection("ActiveDirectory")] +public class PapAuthProcessorTests +{ + [Fact] + public async Task LdapFirstFactorProcessor_CorrectCredentials_ShouldAccept() + { + var sensitiveData = GetConfig(); + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), NullLogger.Instance); + + var contextMock = new Mock(); + var packetMock = new Mock(); + packetMock.Setup(x => x.UserName).Returns(sensitiveData["UserName"]); + packetMock.Setup(x => x.TryGetUserPassword()).Returns(sensitiveData["Password"]); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + + var serverSettings = new Mock(); + serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); + serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + + var authState = new AuthenticationState(); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + + var transformRules = new UserNameTransformRules(); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(sensitiveData["Password"], PreAuthModeDescriptor.Default)); + var result = await processor.Auth(contextMock.Object); + Assert.True(result.IsSuccess); + } + + [Fact] + public async Task LdapFirstFactorProcessor_IncorrectPassword_ShouldReject() + { + var sensitiveData = GetConfig(); + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var serverSettings = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns(sensitiveData["UserName"]); + packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); + serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + + var result = await processor.Auth(contextMock.Object); + Assert.False(result.IsSuccess); + } + + [Fact] + public async Task LdapFirstFactorProcessor_IncorrectLogin_ShouldReject() + { + var sensitiveData = GetConfig(); + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var serverSettings = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns("userName"); + packetMock.Setup(x => x.TryGetUserPassword()).Returns(sensitiveData["Password"]); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); + serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + + var result = await processor.Auth(contextMock.Object); + Assert.False(result.IsSuccess); + } + + + [Theory] + [ClassData(typeof(EmptyStringsListInput))] + public async Task LdapFirstFactorProcessor_EmptyLogin_ShouldReject(string login) + { + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), + NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns(login); + packetMock.Setup(x => x.TryGetUserPassword()).Returns("correctLogin"); + packetMock.Setup(x => x.Identifier).Returns(0); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + + var result = await processor.Auth(contextMock.Object); + Assert.False(result.IsSuccess); + } + + [Theory] + [ClassData(typeof(EmptyStringsListInput))] + public async Task LdapFirstFactorProcessor_EmptyPassword_ShouldReject(string pwd) + { + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), + NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns("correctLogin"); + packetMock.Setup(x => x.Identifier).Returns(0); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(pwd, PreAuthModeDescriptor.Default)); + + var result = await processor.Auth(contextMock.Object); + Assert.False(result.IsSuccess); + } + + [Fact] + public async Task LdapFirstFactorProcessor_MustChangePasswordResponse_ShouldReject() + { + var factoryMock = new Mock(); + factoryMock.Setup(x => x.CreateConnection(It.IsAny())).Throws(GetLdapException); + factoryMock.Setup(x => x.TargetPlatform).Returns(OSPlatform.Windows); + var factory = new CustomLdapConnectionFactory([factoryMock.Object]); + var processor = new PapAuthProcessor(factory, NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var serverSettings = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns("user"); + packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + serverSettings.Setup(x => x.ConnectionString).Returns("your.domain"); + serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + contextMock.SetupProperty(x => x.MustChangePasswordDomain); + contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + var context = contextMock.Object; + + var result = await processor.Auth(context); + Assert.False(result.IsSuccess); + Assert.Equal("your.domain", context.MustChangePasswordDomain); + } + + private LdapException GetLdapException() + { + var ex = new LdapException(1, "message", "data 773"); + return ex; + } + + private Dictionary GetConfig() + { + return ConfigUtils.GetConfigSensitiveData("LdapFirstFactorProcessorTests.txt", "|"); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2759Tests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2759Tests.cs new file mode 100644 index 0000000..36db07e --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2759Tests.cs @@ -0,0 +1,77 @@ +using System.Text; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.MsChapV2; + +namespace Multifactor.Radius.Adapter.v2.Tests.Unit.MsChapV2; + +public class Rfc2759Tests +{ + [Fact] + public void GenerateNTResponse_ShouldGenerateNTResponse() + { + var authenticatorChallenge = new byte[] { 0x77, 0xac, 0x2d, 0x4c, 0x31, 0x2a, 0x6a, 0xfe, 0xb9, 0xd1, 0x76, 0xb4, 0xdd, 0x1d, 0x1a, 0x1d}; + var peerChallenge = new byte[] { 0x34, 0x13, 0x16, 0x83, 0x81, 0xf7, 0x4b, 0x7b, 0x28, 0xe6, 0x08, 0x8b, 0xd7, 0xa5, 0x0d, 0xe9 }; + var username = "test"; + var password = "superSecretPassword"; + + var expected = new byte[] { 0x62, 0x95, 0xb2, 0x14, 0x39, 0x95, 0xf9, 0xf6, 0x58, 0x69, 0x19, 0x77, 0xef, 0x12, 0x79, 0x89, 0x10, 0xff, 0x29, 0x73, 0xb5, 0xb5, 0x13, 0xba }; + var result = Rfc2759.GenerateNTResponse(authenticatorChallenge, peerChallenge, username, password); + Assert.True(expected.SequenceEqual(result)); + } + + [Fact] + public void GenerateNTResponse_ShouldGenerateNTResponse2() + { + var authenticatorChallenge = new byte[] { 0xd5, 0x71, 0x7d, 0x58, 0xe9, 0xfb, 0x9c, 0xf4, 0x2d, 0xbb, 0x0c, 0x1a, 0x8a, 0xdf, 0x98, 0x79 }; + var peerChallenge = new byte[] { 0x27, 0x9c, 0xb4, 0x11, 0x49, 0x4d, 0x5a, 0x84, 0xcd, 0xf2, 0xd2, 0xee, 0x36, 0xfb, 0x5c, 0xdd }; + var username = "test"; + var password = "superSecretPassword"; + + var expected = new byte[] { 0xf5, 0xe4, 0x71, 0xec, 0xb5, 0x59, 0xa9, 0xf7, 0xc6, 0x9a, 0x70, 0x8b, 0x12, 0xe7, 0xa8, 0x6d, 0xd2, 0xfe, 0xf9, 0xab, 0x3f, 0x2a, 0xed, 0x0a }; + var result = Rfc2759.GenerateNTResponse(authenticatorChallenge, peerChallenge, username, password); + Assert.True(expected.SequenceEqual(result)); + } + + [Fact] + public void PadKey_ShouldPadKey() + { + var key = new byte[] { 0x61, 0xee, 0x8b, 0x50, 0x74, 0x8f, 0x5e }; + var expected = new byte[] { 0x61, 0xf7, 0xa2, 0x6b, 0x07, 0xa4, 0x3d, 0xbc}; + var result = Rfc2759.ParityPadDESKey(key); + Assert.True(expected.SequenceEqual(result)); + } + + [Fact] + public void ChallengeHash_ShouldCalculateChallengeHash() + { + var authenticatorChallenge = new byte[] { 0x77, 0xac, 0x2d, 0x4c, 0x31, 0x2a, 0x6a, 0xfe, 0xb9, 0xd1, 0x76, 0xb4, 0xdd, 0x1d, 0x1a, 0x1d }; + var peerChallenge = new byte[] { 0x34, 0x13, 0x16, 0x83, 0x81, 0xf7, 0x4b, 0x7b, 0x28, 0xe6, 0x08, 0x8b, 0xd7, 0xa5, 0x0d, 0xe9 }; + var username = "test"; + + var sha1Hash = Rfc2759.ChallengeHash(authenticatorChallenge, peerChallenge, username); + var expected = new byte[] { 75, 54, 203, 169, 26, 155, 168, 104 }; + Assert.True(expected.SequenceEqual(sha1Hash)); + } + + [Fact] + public void ToUtf16() + { + var password = "superSecretPassword"; + var utf16 = Encoding.Unicode.GetBytes(password); + + var expected = new byte[] {115,0,117,0,112,0,101,0,114,0,83,0,101,0,99,0,114,0,101,0,116,0,80,0,97,0,115,0,115,0,119,0,111,0,114,0,100,0}; + Assert.True(utf16.SequenceEqual(expected)); + } + + [Fact] + public void GenerateAuthenticatorResponse_ShouldGenerateAuthenticatorResponse() + { + var authenticatorChallenge = new byte[] { 0xd5, 0x71, 0x7d, 0x58, 0xe9, 0xfb, 0x9c, 0xf4, 0x2d, 0xbb, 0x0c, 0x1a, 0x8a, 0xdf, 0x98, 0x79 }; + var peerChallenge = new byte[] { 0x27, 0x9c, 0xb4, 0x11, 0x49, 0x4d, 0x5a, 0x84, 0xcd, 0xf2, 0xd2, 0xee, 0x36, 0xfb, 0x5c, 0xdd }; + var ntResponse = new byte[] { 0xe6, 0xad, 0x73, 0xb3, 0x73, 0x88, 0x39, 0xcc, 0xcf, 0xc0, 0xfb, 0xf3, 0x45, 0x9a, 0x5b, 0x26, 0xac, 0x4b, 0x15, 0x9e, 0xfa, 0xb6, 0xb0, 0x3f }; + var userName = "test"; + var password = "superSecretPassword"; + var expected = "S=E776FC79AC79DED99AEFF66893C920EB34F63396"; + var res = Rfc2759.GenerateAuthenticatorResponse(authenticatorChallenge, peerChallenge, ntResponse, userName, password); + Assert.Equal(expected, res); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2868Tests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2868Tests.cs new file mode 100644 index 0000000..6c6f462 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc2868Tests.cs @@ -0,0 +1,32 @@ +using System.Security.Cryptography; +using System.Text; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.MsChapV2; + +namespace Multifactor.Radius.Adapter.v2.Tests.Unit.MsChapV2; + +public class Rfc2868Tests +{ + [Theory] + [InlineData("")] + [InlineData("a")] + [InlineData("Hello")] + [InlineData("0123456789abcde")] + [InlineData("0123456789abcdef")] + [InlineData("0123456789abcdef0123456789abcdef0123456789abcdef")] + [InlineData("Qwerty123!")] + public void TestTunnelPassword(string password) + { + //var salt = new byte[] { 0x83, 0x45 }; + var salt = Rfc2868.GenerateSalt(); + + var secret = Encoding.ASCII.GetBytes("secret"); + var requestAuthenticator = new byte[16]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(requestAuthenticator); + + var pwd = Encoding.ASCII.GetBytes(password); + var newTunnelPassword = Rfc2868.NewTunnelPassword(pwd, salt, secret, requestAuthenticator); + var decryptedPassword = Rfc2868.TunnelPassword(newTunnelPassword, secret, requestAuthenticator, out var decryptedSalt); + Assert.True(pwd.SequenceEqual(decryptedPassword)); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc3079Tests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc3079Tests.cs new file mode 100644 index 0000000..0e7132c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/MsChapV2/Rfc3079Tests.cs @@ -0,0 +1,38 @@ +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.MsChapV2; + +namespace Multifactor.Radius.Adapter.v2.Tests.Unit.MsChapV2; + +public class Rfc3079Tests +{ + [Fact] + public void GetMasterKey() + { + var passwordHashHash = new byte[] { 0x41, 0xC0, 0x0C, 0x58, 0x4B, 0xD2, 0xD9, 0x1C, 0x40, 0x17, 0xA2, 0xA1, 0x2F, 0xA5, 0x9F, 0x3F }; + var ntResponse = new byte[] { 0x82, 0x30, 0x9E, 0xCD, 0x8D, 0x70, 0x8B, 0x5E, 0xA0, 0x8F, 0xAA, 0x39, 0x81, 0xCD, 0x83, 0x54, 0x42, 0x33, 0x11, 0x4A, 0x3D, 0x85, 0xD6, 0xDF }; + var expected = new byte[] { 0xFD, 0xEC, 0xE3, 0x71, 0x7A, 0x8C, 0x83, 0x8C, 0xB3, 0x88, 0xE5, 0x27, 0xAE, 0x3C, 0xDD, 0x31 }; + var result = Rfc3079.GetMasterKey(passwordHashHash, ntResponse); + Assert.True(expected.SequenceEqual(result)); + } + + [Fact] + public void GetAsymmetricStartKey() + { + var masterKey = new byte[] { 0xFD, 0xEC, 0xE3, 0x71, 0x7A, 0x8C, 0x83, 0x8C, 0xB3, 0x88, 0xE5, 0x27, 0xAE, 0x3C, 0xDD, 0x31 }; + var sessionKeyLength = 8; + var isSend = true; + var expected = new byte[] { 0x8B, 0x7C, 0xDC, 0x14, 0x9B, 0x99, 0x3A, 0x1B }; + var result = Rfc3079.GetAsymmetricStartKey(masterKey, sessionKeyLength, isSend); + Assert.True(expected.SequenceEqual(result)); + } + + [Fact] + public void GetAsymmetricStartKey2() + { + var masterKey = new byte[] { 0xFD, 0xEC, 0xE3, 0x71, 0x7A, 0x8C, 0x83, 0x8C, 0xB3, 0x88, 0xE5, 0x27, 0xAE, 0x3C, 0xDD, 0x31 }; + var sessionKeyLength = 16; + var isSend = true; + var expected = new byte[] { 0x8B, 0x7C, 0xDC, 0x14, 0x9B, 0x99, 0x3A, 0x1B, 0xA1, 0x18, 0xCB, 0x15, 0x3F, 0x56, 0xDC, 0xCB }; + var result = Rfc3079.GetAsymmetricStartKey(masterKey, sessionKeyLength, isSend); + Assert.True(expected.SequenceEqual(result)); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/AuthResult.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/AuthResult.cs new file mode 100644 index 0000000..1f7fe7d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/AuthResult.cs @@ -0,0 +1,8 @@ +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; + +public class AuthResult +{ + public bool IsSuccess { get; set; } + + public string ErrorMessage { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/ILdapAuthProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/ILdapAuthProcessor.cs new file mode 100644 index 0000000..418ebb7 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/ILdapAuthProcessor.cs @@ -0,0 +1,10 @@ +using Multifactor.Radius.Adapter.v2.Core.Radius; +using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; + +public interface ILdapAuthProcessor +{ + Task Auth( IRadiusPipelineExecutionContext context); + AuthenticationType AuthenticationType { get; } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/LdapAuthProvider.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/LdapAuthProvider.cs new file mode 100644 index 0000000..562c9c7 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/LdapAuthProvider.cs @@ -0,0 +1,23 @@ +using Multifactor.Radius.Adapter.v2.Core.Radius; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; + +public class LdapAuthProvider : ILdapAuthProvider +{ + private readonly IEnumerable _processors; + + public LdapAuthProvider(IEnumerable authProcessors) + { + _processors = authProcessors; + } + + public ILdapAuthProcessor? GetLdapAuthProcessor(AuthenticationType authenticationType) + { + return _processors.FirstOrDefault(processor => processor.AuthenticationType == authenticationType); + } +} + +public interface ILdapAuthProvider +{ + ILdapAuthProcessor? GetLdapAuthProcessor(AuthenticationType authenticationType); +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MD4.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MD4.cs new file mode 100644 index 0000000..dc93a9c --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MD4.cs @@ -0,0 +1,364 @@ +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth.MsChapV2; + +public class MD4 +{ + // MD4 specific object variables + //----------------------------------------------------------------------- + + /// + /// The size in bytes of the input block to the transformation algorithm + /// + private const int BLOCK_LENGTH = 64; // = 512 / 8 + + /// + /// 512-bit work buffer = 16 x 32-bit words + /// + private readonly uint[] X = new uint[16]; + + /// + /// 4 32-bit words (interim result) + /// + private readonly uint[] context = new uint[4]; + + /// + /// 512-bit input buffer = 16 x 32-bit words holds until it reaches 512 bits + /// + private byte[] buffer = new byte[BLOCK_LENGTH]; + + /// + /// Number of bytes procesed so far mod. 2 power of 64. + /// + private long count; + + + // Constructors + //------------------------------------------------------------------------ + public MD4() + { + EngineReset(); + } + + /// + /// This constructor is here to implement the clonability of this class + /// + /// + private MD4(MD4 md) : this() + { + //this(); + context = (uint[]) md.context.Clone(); + buffer = (byte[]) md.buffer.Clone(); + count = md.count; + } + + // Clonable method implementation + //------------------------------------------------------------------------- + public object Clone() + { + return new MD4(this); + } + + // JCE methods + //------------------------------------------------------------------------- + + /// + /// Resets this object disregarding any temporary data present at the + /// time of the invocation of this call. + /// + private void EngineReset() + { + // initial values of MD4 i.e. A, B, C, D + // as per rfc-1320; they are low-order byte first + context[0] = 0x67452301; + context[1] = 0xEFCDAB89; + context[2] = 0x98BADCFE; + context[3] = 0x10325476; + count = 0L; + for (int i = 0; i < BLOCK_LENGTH; i++) + buffer[i] = 0; + } + + + /// + /// Continues an MD4 message digest using the input byte + /// + /// byte to input + private void EngineUpdate(byte b) + { + // compute number of bytes still unhashed; ie. present in buffer + var i = (int) (count%BLOCK_LENGTH); + count++; // update number of bytes + buffer[i] = b; + if (i == BLOCK_LENGTH - 1) + Transform(ref buffer, 0); + } + + /// + /// MD4 block update operation + /// + /// + /// Continues an MD4 message digest operation by filling the buffer, + /// transform(ing) data in 512-bit message block(s), updating the variables + /// context and count, and leaving (buffering) the remaining bytes in buffer + /// for the next update or finish. + /// + /// input block + /// start of meaningful bytes in input + /// count of bytes in input blcok to consider + private void EngineUpdate(byte[] input, int offset, int len) + { + // make sure we don't exceed input's allocated size/length + if (offset < 0 || len < 0 || (long) offset + len > input.Length) + throw new ArgumentOutOfRangeException(); + + // compute number of bytes still unhashed; ie. present in buffer + var bufferNdx = (int) (count%BLOCK_LENGTH); + count += len; // update number of bytes + int partLen = BLOCK_LENGTH - bufferNdx; + int i = 0; + if (len >= partLen) + { + Array.Copy(input, offset + i, buffer, bufferNdx, partLen); + + Transform(ref buffer, 0); + + for (i = partLen; i + BLOCK_LENGTH - 1 < len; i += BLOCK_LENGTH) + Transform(ref input, offset + i); + bufferNdx = 0; + } + // buffer remaining input + if (i < len) + Array.Copy(input, offset + i, buffer, bufferNdx, len - i); + } + + /// + /// Completes the hash computation by performing final operations such + /// as padding. At the return of this engineDigest, the MD engine is + /// reset. + /// + /// the array of bytes for the resulting hash value. + private byte[] EngineDigest() + { + // pad output to 56 mod 64; as RFC1320 puts it: congruent to 448 mod 512 + var bufferNdx = (int) (count%BLOCK_LENGTH); + int padLen = (bufferNdx < 56) ? (56 - bufferNdx) : (120 - bufferNdx); + + // padding is always binary 1 followed by binary 0's + var tail = new byte[padLen + 8]; + tail[0] = 0x80; + + // append length before final transform + // save number of bits, casting the long to an array of 8 bytes + // save low-order byte first. + for (int i = 0; i < 8; i++) + tail[padLen + i] = (byte) ((count*8) >> (8*i)); + + EngineUpdate(tail, 0, tail.Length); + + var result = new byte[16]; + // cast this MD4's context (array of 4 uints) into an array of 16 bytes. + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + result[i*4 + j] = (byte) (context[i] >> (8*j)); + + // reset the engine + EngineReset(); + return result; + } + + /// + /// Returns a byte hash from a string + /// + /// string to hash + /// byte-array that contains the hash + public byte[] GetByteHashFromString(string s) + { + byte[] b = Encoding.UTF8.GetBytes(s); + var md4 = new MD4(); + + md4.EngineUpdate(b, 0, b.Length); + + return md4.EngineDigest(); + } + + /// + /// Returns a binary hash from an input byte array + /// + /// byte-array to hash + /// binary hash of input + public byte[] GetByteHashFromBytes(byte[] b) + { + var md4 = new MD4(); + + md4.EngineUpdate(b, 0, b.Length); + + return md4.EngineDigest(); + } + + /// + /// Returns a string that contains the hexadecimal hash + /// + /// byte-array to input + /// String that contains the hex of the hash + public string GetHexHashFromBytes(byte[] b) + { + byte[] e = GetByteHashFromBytes(b); + return BytesToHex(e, e.Length); + } + + /// + /// Returns a byte hash from the input byte + /// + /// byte to hash + /// binary hash of the input byte + public byte[] GetByteHashFromByte(byte b) + { + var md4 = new MD4(); + + md4.EngineUpdate(b); + + return md4.EngineDigest(); + } + + /// + /// Returns a string that contains the hexadecimal hash + /// + /// byte to hash + /// String that contains the hex of the hash + public string GetHexHashFromByte(byte b) + { + byte[] e = GetByteHashFromByte(b); + return BytesToHex(e, e.Length); + } + + /// + /// Returns a string that contains the hexadecimal hash + /// + /// string to hash + /// String that contains the hex of the hash + public string GetHexHashFromString(string s) + { + byte[] b = GetByteHashFromString(s); + return BytesToHex(b, b.Length); + } + + private static string BytesToHex(byte[] a, int len) + { + string temp = BitConverter.ToString(a); + + // We need to remove the dashes that come from the BitConverter + var sb = new StringBuilder((len - 2)/2); // This should be the final size + + for (int i = 0; i < temp.Length; i++) + if (temp[i] != '-') + sb.Append(temp[i]); + + return sb.ToString(); + } + + // own methods + //----------------------------------------------------------------------------------- + + /// + /// MD4 basic transformation + /// + /// + /// Transforms context based on 512 bits from input block starting + /// from the offset'th byte. + /// + /// input sub-array + /// starting position of sub-array + private void Transform(ref byte[] block, int offset) + { + // decodes 64 bytes from input block into an array of 16 32-bit + // entities. Use A as a temp var. + for (int i = 0; i < 16; i++) + X[i] = ((uint) block[offset++] & 0xFF) | + (((uint) block[offset++] & 0xFF) << 8) | + (((uint) block[offset++] & 0xFF) << 16) | + (((uint) block[offset++] & 0xFF) << 24); + + + uint A = context[0]; + uint B = context[1]; + uint C = context[2]; + uint D = context[3]; + + A = FF(A, B, C, D, X[0], 3); + D = FF(D, A, B, C, X[1], 7); + C = FF(C, D, A, B, X[2], 11); + B = FF(B, C, D, A, X[3], 19); + A = FF(A, B, C, D, X[4], 3); + D = FF(D, A, B, C, X[5], 7); + C = FF(C, D, A, B, X[6], 11); + B = FF(B, C, D, A, X[7], 19); + A = FF(A, B, C, D, X[8], 3); + D = FF(D, A, B, C, X[9], 7); + C = FF(C, D, A, B, X[10], 11); + B = FF(B, C, D, A, X[11], 19); + A = FF(A, B, C, D, X[12], 3); + D = FF(D, A, B, C, X[13], 7); + C = FF(C, D, A, B, X[14], 11); + B = FF(B, C, D, A, X[15], 19); + + A = GG(A, B, C, D, X[0], 3); + D = GG(D, A, B, C, X[4], 5); + C = GG(C, D, A, B, X[8], 9); + B = GG(B, C, D, A, X[12], 13); + A = GG(A, B, C, D, X[1], 3); + D = GG(D, A, B, C, X[5], 5); + C = GG(C, D, A, B, X[9], 9); + B = GG(B, C, D, A, X[13], 13); + A = GG(A, B, C, D, X[2], 3); + D = GG(D, A, B, C, X[6], 5); + C = GG(C, D, A, B, X[10], 9); + B = GG(B, C, D, A, X[14], 13); + A = GG(A, B, C, D, X[3], 3); + D = GG(D, A, B, C, X[7], 5); + C = GG(C, D, A, B, X[11], 9); + B = GG(B, C, D, A, X[15], 13); + + A = HH(A, B, C, D, X[0], 3); + D = HH(D, A, B, C, X[8], 9); + C = HH(C, D, A, B, X[4], 11); + B = HH(B, C, D, A, X[12], 15); + A = HH(A, B, C, D, X[2], 3); + D = HH(D, A, B, C, X[10], 9); + C = HH(C, D, A, B, X[6], 11); + B = HH(B, C, D, A, X[14], 15); + A = HH(A, B, C, D, X[1], 3); + D = HH(D, A, B, C, X[9], 9); + C = HH(C, D, A, B, X[5], 11); + B = HH(B, C, D, A, X[13], 15); + A = HH(A, B, C, D, X[3], 3); + D = HH(D, A, B, C, X[11], 9); + C = HH(C, D, A, B, X[7], 11); + B = HH(B, C, D, A, X[15], 15); + + context[0] += A; + context[1] += B; + context[2] += C; + context[3] += D; + } + + // The basic MD4 atomic functions. + + private uint FF(uint a, uint b, uint c, uint d, uint x, int s) + { + uint t = a + ((b & c) | (~b & d)) + x; + return t << s | t >> (32 - s); + } + + private uint GG(uint a, uint b, uint c, uint d, uint x, int s) + { + uint t = a + ((b & (c | d)) | (c & d)) + x + 0x5A827999; + return t << s | t >> (32 - s); + } + + private uint HH(uint a, uint b, uint c, uint d, uint x, int s) + { + uint t = a + (b ^ c ^ d) + x + 0x6ED9EBA1; + return t << s | t >> (32 - s); + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MsChapV2AuthProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MsChapV2AuthProcessor.cs new file mode 100644 index 0000000..4c41371 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/MsChapV2AuthProcessor.cs @@ -0,0 +1,115 @@ +using System.Text; +using Microsoft.Extensions.Logging; +using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.MsChapV2; +using Multifactor.Radius.Adapter.v2.Core.Radius; +using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Services.Ldap; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth.MsChapV2; + +public class MsChapV2AuthProcessor : ILdapAuthProcessor +{ + private readonly ILdapProfileService _ldapProfileService; + private readonly ILogger _logger; + + public AuthenticationType AuthenticationType => AuthenticationType.MSCHAP2; + + public MsChapV2AuthProcessor(ILdapProfileService ldapProfileService, ILogger logger) + { + _ldapProfileService = ldapProfileService; + _logger = logger; + } + + public async Task Auth(IRadiusPipelineExecutionContext context) + { + var radiusPacket = context.RequestPacket; + var userName = radiusPacket.UserName; + + if (string.IsNullOrWhiteSpace(userName)) + { + _logger.LogWarning("Can't find User-Name in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); + return new AuthResult() { IsSuccess = false };; + + } + var challenge = radiusPacket.GetAttribute("MS-CHAP-Challenge"); + var response = radiusPacket.GetAttribute("MS-CHAP2-Response"); + + var pwdBytes = await GetUserPassword(context); //maybe md4 + var password = Encoding.ASCII.GetString(pwdBytes); + + if (challenge.Length != 16) + { + _logger.LogWarning("MS-CHAP-Challenge length is incorrect."); + return new AuthResult() { IsSuccess = false }; + } + + if (response.Length != 50) + { + _logger.LogWarning("MS-CHAP2-Response length is incorrect."); + return new AuthResult() { IsSuccess = false }; + } + + var ident = response[0]; + var peerChallenge = response[2..18]; + var peerResponse = response[26..50]; + var ntResponse = Rfc2759.GenerateNTResponse(challenge, peerChallenge, userName, password); + + if (!ntResponse.SequenceEqual(peerResponse)) + { + _logger.LogWarning("Calculated NT-Response is not equal to peer response."); + return new AuthResult() { IsSuccess = false }; + } + + var recvKey = Rfc3079.MakeKey(ntResponse, password, false); + + var sendKey = Rfc3079.MakeKey(ntResponse, password, true); + + // MS-CHAP2-Success calculation + var authenticatorResponse = Rfc2759.GenerateAuthenticatorResponse(challenge, peerChallenge, ntResponse, userName, password); + var success = new List(43); + success.Add(ident); + var authenticatorResponseBytes = Encoding.ASCII.GetBytes(authenticatorResponse); + success.AddRange(authenticatorResponseBytes); + context.ResponseInformation.Attributes.Add("MS-CHAP2-Success", CreateAttribute("MS-CHAP2-Success", success)); + + // MS-MPPE-Recv-Key calculation + var recvKeyAttr = GetMsMPPEKey(context.RequestPacket, recvKey, context.RadiusSharedSecret.Bytes); + context.ResponseInformation.Attributes.Add("MS-MPPE-Recv-Key", CreateAttribute("MS-MPPE-Recv-Key", recvKeyAttr)); + + // MS-MPPE-Send-Key calculation + var sendKeyAttr = GetMsMPPEKey(context.RequestPacket, sendKey, context.RadiusSharedSecret.Bytes); + context.ResponseInformation.Attributes.Add("MS-MPPE-Send-Key", CreateAttribute("MS-MPPE-Send-Key", sendKeyAttr)); + + context.ResponseInformation.Attributes.Add("MS-MPPE-Encryption-Policy", CreateAttribute("MS-MPPE-Encryption-Policy", 1)); + context.ResponseInformation.Attributes.Add("MS-MPPE-Encryption-Types", CreateAttribute("MS-MPPE-Encryption-Types", 6)); + + context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; + + return new AuthResult() { IsSuccess = true }; + } + + private async Task GetUserPassword(IRadiusPipelineExecutionContext context) + { + ArgumentNullException.ThrowIfNull(context.UserLdapProfile); + ArgumentNullException.ThrowIfNull(context.LdapServerConfiguration); + ArgumentNullException.ThrowIfNull(context.LdapSchema); + + return await _ldapProfileService.GetUserPassword(new GetUserPasswordRequest(context.UserLdapProfile, context.LdapServerConfiguration, context.LdapSchema)); + } + + private RadiusAttribute CreateAttribute(string attributeName, params object[] value) + { + var attribute = new RadiusAttribute(attributeName); + attribute.AddValues(value); + return attribute; + } + + private byte[] GetMsMPPEKey(IRadiusPacket requestPacket, byte[] key, byte[] secret) + { + var salt = Rfc2868.GenerateSalt(); + var tunnelPassword = Rfc2868.NewTunnelPassword(key, salt, secret, requestPacket.Authenticator.Value); + return tunnelPassword; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2759.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2759.cs new file mode 100644 index 0000000..2c0889b --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2759.cs @@ -0,0 +1,140 @@ +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth.MsChapV2; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.MsChapV2; + +public class Rfc2759 +{ + private static readonly byte[] _magic1 = + { + 0x4D, 0x61, 0x67, 0x69, 0x63, 0x20, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x20, 0x74, 0x6F, 0x20, 0x63, 0x6C, 0x69, 0x65, + 0x6E, 0x74, 0x20, 0x73, 0x69, 0x67, 0x6E, 0x69, 0x6E, 0x67, + 0x20, 0x63, 0x6F, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x74, + }; + + private static readonly byte[] _magic2 = + { + 0x50, 0x61, 0x64, 0x20, 0x74, 0x6F, 0x20, 0x6D, 0x61, 0x6B, + 0x65, 0x20, 0x69, 0x74, 0x20, 0x64, 0x6F, 0x20, 0x6D, 0x6F, + 0x72, 0x65, 0x20, 0x74, 0x68, 0x61, 0x6E, 0x20, 0x6F, 0x6E, + 0x65, 0x20, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, + 0x6E + }; + + public static string GenerateAuthenticatorResponse(byte[] authenticatorChallenge, byte[] peerChallenge, byte[] ntResponse, string userName, string password) + { + var utf16Password = Encoding.Unicode.GetBytes(password); + var passwordHash = NTPasswordHash(utf16Password); + var passwordHashHash = NTPasswordHash(passwordHash); + + using SHA1 sha = SHA1.Create(); + var data = new List(); + data.AddRange(passwordHashHash); + data.AddRange(ntResponse); + data.AddRange(_magic1); + + var hash = sha.ComputeHash(data.ToArray()); + + var challenge = ChallengeHash(authenticatorChallenge, peerChallenge, userName); + using SHA1 sha2 = SHA1.Create(); + + data = new List(); + data.AddRange(hash); + data.AddRange(challenge); + data.AddRange(_magic2); + hash = sha2.ComputeHash(data.ToArray()); + return "S=" + Convert.ToHexString(hash).ToUpper(); + } + + public static byte[] NTPasswordHash(byte[] password) + { + var md4 = new MD4(); + var data = new List(); + data.AddRange(password); + var hash = md4.GetByteHashFromBytes(data.ToArray()); + return hash; + } + + public static byte[] GenerateNTResponse(byte[] authenticatorChallenge, byte[] peerChallenge, string userName, string password) + { + var challenge = ChallengeHash(authenticatorChallenge, peerChallenge, userName); + var utf16Password = Encoding.Unicode.GetBytes(password); + + var passwordHash = NTPasswordHash(utf16Password); + + return ChallengeResponse(challenge, passwordHash); + } + + public static byte[] ChallengeHash(byte[] authenticatorChallenge, byte[] peerChallenge, string userName) + { + using SHA1 sha = SHA1.Create(); + var data = new List(); + data.AddRange(peerChallenge); + data.AddRange(authenticatorChallenge); + var userNameBytes = Encoding.ASCII.GetBytes(userName); + data.AddRange(userNameBytes); + + var hash = sha.ComputeHash(data.ToArray()); + return hash[..8]; + } + + public static byte[] ChallengeResponse(byte[] challenge, byte[] passwordHash) + { + var zPasswordHash = new byte[21]; + + Buffer.BlockCopy(passwordHash, 0, zPasswordHash, 0, passwordHash.Length); + + var challengeResponse = new List(24); + challengeResponse.AddRange(DESCrypt(zPasswordHash[..7], challenge)); + challengeResponse.AddRange(DESCrypt(zPasswordHash[7..14], challenge)); + challengeResponse.AddRange(DESCrypt(zPasswordHash[14..21], challenge)); + return challengeResponse.ToArray(); + } + + public static byte[] DESCrypt(byte[] key, byte[] clear) + { + var k = key; + if (k.Length == 7) + k = ParityPadDESKey(key); + + using var des = DES.Create(); + des.Key = k; + des.Mode = CipherMode.ECB; + des.Padding = PaddingMode.None; + + using ICryptoTransform encryptor = des.CreateEncryptor(); + var encrypted = encryptor.TransformFinalBlock(clear, 0, clear.Length); + return encrypted; + } + + public static byte[] ParityPadDESKey(byte[] inBytes) + { + ulong int64 = 0; + var outBytes = new byte[8]; + var inBytesLength = inBytes.Length; + + for (int i = 0; i < inBytesLength; i++) + { + var offset = (8 * (inBytesLength - i - 1)); + int64 |= (ulong)inBytes[i] << offset; + } + + var outBytesLength = outBytes.Length; + + for (int i = 0; i < outBytesLength; i++) + { + var offset = 7 * (outBytesLength - i - 1); + var byteVal = (byte)((int64 >> offset) & 0xFF); + outBytes[i] = (byte)(byteVal << 1); + + if (BitOperations.PopCount(outBytes[i]) % 2 == 0) { + outBytes[i] |= 1; + } + } + + return outBytes; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2868.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2868.cs new file mode 100644 index 0000000..8b0ecfc --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc2868.cs @@ -0,0 +1,133 @@ +using System.Security.Cryptography; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.MsChapV2; + +public class Rfc2868 +{ + public static byte[] NewTunnelPassword(byte[] password, byte[] salt, byte[] secret, byte[] requestAuthenticator) + { + if (password.Length > 249) + throw new Exception("Invalid password length"); + + if (salt.Length != 2) + throw new Exception("Invalid salt length"); + + if ((salt[0] & 0x80) != 0x80) // MSB must be 1 + throw new Exception("Invalid salt"); + + if (secret.Length == 0) + throw new Exception("Invalid secret length"); + + if (requestAuthenticator.Length != 16) + throw new Exception("Invalid request authenticator length"); + + var chunks = (1 + password.Length + 16 - 1) / 16; + + var attr = new byte[2 + chunks * 16]; + Buffer.BlockCopy(salt, 0, attr, 0, salt.Length); + attr[2] = (byte)password.Length; + Buffer.BlockCopy(password, 0, attr, 3, password.Length); + + using var md5 = MD5.Create(); + var b = new byte[MD5.HashSizeInBytes]; + for (int chunk = 0; chunk < chunks; chunk++) + { + var data = new List(); + md5.Initialize(); + data.AddRange(secret); + + if (chunk == 0) + { + data.AddRange(requestAuthenticator); + data.AddRange(salt); + } + else + { + var start = 2 + (chunk - 1) * 16; + var end = 2 + chunk * 16; + data.AddRange(attr[start..end]); + } + + b = md5.ComputeHash(data.ToArray()); + + for (var i = 0; i < 16; i++) + { + attr[2 + chunk * 16 + i] ^= b[i]; + } + } + + return attr; + } + + public static byte[] TunnelPassword(byte[] attrVal, byte[] secret, byte[] requestAuthenticator, out byte[] salt) + { + var attrValLen = attrVal.Length; + if (attrValLen > 252 || attrValLen < 18 || (attrValLen - 2) % 16 != 0) + throw new ArgumentException("Invalid attribute value length"); + + var secretLen = secret.Length; + if (secretLen == 0) + throw new AggregateException("Empty secret"); + + var reqAuthenticatorLen = requestAuthenticator.Length; + if (reqAuthenticatorLen != 16) + throw new AggregateException("invalid requestAuthenticator length"); + + if ((attrVal[0] & 0x80) != 0x80) // salt MSB must be 1 + throw new AggregateException("invalid salt"); + + var saltList = new List(); + saltList.AddRange(attrVal[..2]); + attrVal = attrVal[2..]; + + var chunks = attrValLen / 16; + var plaintext = new byte[chunks * 16]; + + using var md5 = MD5.Create(); + var b = new byte[MD5.HashSizeInBytes]; + + for (var chunk = 0; chunk < chunks; chunk++) + { + var data = new List(); + md5.Initialize(); + + data.AddRange(secret); + if (chunk == 0) + { + data.AddRange(requestAuthenticator); + data.AddRange(saltList); + } + else + { + var start = (chunk - 1) * 16; + var end = chunk * 16; + data.AddRange(attrVal[start..end]); + } + + b = md5.ComputeHash(data.ToArray()); + + for (var i = 0; i < 16; i++) + { + var a = attrVal[chunk * 16 + i]; + plaintext[chunk * 16 + i] = (byte)(a ^ b[i]); + } + } + + var passwordLength = plaintext[0]; + + if (passwordLength > plaintext.Length - 1) + throw new Exception("invalid password length"); + + salt = saltList.ToArray(); + return plaintext[1..(1 + passwordLength)]; + } + + public static byte[] GenerateSalt() + { + var salt = new byte[2]; + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(salt); + salt[0] |= 1 << 7; + return salt; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc3079.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc3079.cs new file mode 100644 index 0000000..bd6a467 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/MsChapV2/Rfc3079.cs @@ -0,0 +1,95 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.MsChapV2; + +public class Rfc3079 +{ + private static readonly byte[] _magic1 = { + 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, + 0x68, 0x65, 0x20, 0x4d, 0x50, 0x50, 0x45, 0x20, 0x4d, + 0x61, 0x73, 0x74, 0x65, 0x72, 0x20, 0x4b, 0x65, 0x79, + }; + + private static readonly byte[] _magic2 = + { + 0x4f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x20, 0x73, 0x69, 0x64, 0x65, 0x2c, 0x20, + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x73, 0x65, 0x6e, 0x64, 0x20, 0x6b, 0x65, 0x79, + 0x3b, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x73, 0x69, 0x64, 0x65, + 0x2c, 0x20, 0x69, 0x74, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x20, + 0x6b, 0x65, 0x79, 0x2e + }; + + private static readonly byte[] _magic3 = + { + 0x4f, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x20, 0x73, 0x69, 0x64, 0x65, 0x2c, 0x20, + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x20, + 0x6b, 0x65, 0x79, 0x3b, 0x20, 0x6f, 0x6e, 0x20, 0x74, 0x68, + 0x65, 0x20, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x20, 0x73, + 0x69, 0x64, 0x65, 0x2c, 0x20, 0x69, 0x74, 0x20, 0x69, 0x73, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x65, 0x6e, 0x64, 0x20, + 0x6b, 0x65, 0x79, 0x2e + }; + + private static readonly byte[] _shaPad1 = + { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + private static readonly byte[] _shaPad2 = + { + 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, + 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, + 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, + 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, 0xf2, + }; + + public static byte[] MakeKey(byte[] ntResponse, string password, bool isSend) + { + if (ntResponse.Length != 24) + throw new ArgumentException("ntResponse must be 24 bytes in size"); + + var ucs2Password = Encoding.Unicode.GetBytes(password); + var passwordHash = Rfc2759.NTPasswordHash(ucs2Password); + var passwordHashHash = Rfc2759.NTPasswordHash(passwordHash); + var masterKey = GetMasterKey(passwordHashHash, ntResponse); + + return GetAsymmetricStartKey(masterKey, 16, isSend); + } + + public static byte[] GetMasterKey(byte[] passwordHashHash, byte[] ntResponse) + { + using SHA1 sha = SHA1.Create(); + var data = new List(); + data.AddRange(passwordHashHash); + data.AddRange(ntResponse); + data.AddRange(_magic1); + var hash = sha.ComputeHash(data.ToArray()); + return hash[..16]; + } + + public static byte[] GetAsymmetricStartKey(byte[] masterKey, int keyLength, bool isSend) + { + if (masterKey.Length != 16) + throw new ArgumentException("masterKey must be 16 bytes in size"); + var data = new List(); + data.AddRange(masterKey); + data.AddRange(_shaPad1); + + data.AddRange(isSend ? _magic3 : _magic2); + data.AddRange(_shaPad2); + using SHA1 sha = SHA1.Create(); + var hash = sha.ComputeHash(data.ToArray()); + + return hash[..keyLength]; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs new file mode 100644 index 0000000..8b3eafd --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs @@ -0,0 +1,115 @@ +using System.DirectoryServices.Protocols; +using Microsoft.Extensions.Logging; +using Multifactor.Core.Ldap; +using Multifactor.Core.Ldap.Connection; +using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; +using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Core.Radius; +using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; + +namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; + +public class PapAuthProcessor : ILdapAuthProcessor +{ + private readonly ILogger _logger; + private ILdapConnectionFactory _ldapConnectionFactory; + + public AuthenticationType AuthenticationType => AuthenticationType.PAP; + + public PapAuthProcessor(ILdapConnectionFactory ldapConnectionFactory, ILogger logger) + { + _ldapConnectionFactory = ldapConnectionFactory; + _logger = logger; + } + + public async Task Auth(IRadiusPipelineExecutionContext context) + { + var radiusPacket = context.RequestPacket; + var passphrase = context.Passphrase; + + if (string.IsNullOrWhiteSpace(radiusPacket.UserName)) + { + _logger.LogWarning("Can't find User-Name in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); + return new AuthResult() { IsSuccess = false };; + } + + if (string.IsNullOrWhiteSpace(passphrase.Raw)) + { + _logger.LogWarning("No User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); + return new AuthResult() { IsSuccess = false }; + } + + if (string.IsNullOrWhiteSpace(passphrase.Password)) + { + _logger.LogWarning("Can't parse User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); + return new AuthResult() { IsSuccess = false }; + } + + var transformedName = UserNameTransformation.Transform(radiusPacket.UserName, context.UserNameTransformRules.BeforeFirstFactor); + var isValid = ValidateUserCredentials(context, transformedName, passphrase.Password); + await Task.CompletedTask; + return new AuthResult() { IsSuccess = isValid }; + } + + private bool ValidateUserCredentials( + IRadiusPipelineExecutionContext context, + string login, + string password) + { + var serverConfig = context.LdapServerConfiguration; + try + { + using var connection = GetConnection( + serverConfig.ConnectionString, + login, + password, + serverConfig.BindTimeoutInSeconds); + + return true; + } + catch (Exception ex) + { + if (ex is not LdapException ldapException) + { + _logger.LogError(ex, "Verification user '{user:l}' at {ldapUri:l} failed", login, serverConfig.ConnectionString); + return false; + } + + var info = GetLdapErrorInfo(ldapException); + if (info != null) + ProcessErrorReason(info, context, serverConfig); + + _logger.LogWarning(ldapException, "Verification user '{user:l}' at {ldapUri:l} failed: {dataReason:l}", login, serverConfig.ConnectionString, info?.ReasonText); + } + + return false; + } + + private ILdapConnection GetConnection(string connectionString, string userName, string password, + int bindTimeoutInSeconds) + { + var connectionOptions = new LdapConnectionOptions( + new LdapConnectionString(connectionString), + AuthType.Basic, + userName, + password, + TimeSpan.FromSeconds(bindTimeoutInSeconds)); + + return _ldapConnectionFactory.CreateConnection(connectionOptions); + } + + private LdapErrorReasonInfo? GetLdapErrorInfo(LdapException exception) + { + if (string.IsNullOrWhiteSpace(exception.ServerErrorMessage)) + return null; + var reason = LdapErrorReasonInfo.Create(exception.ServerErrorMessage); + return reason; + } + + private void ProcessErrorReason(LdapErrorReasonInfo errorInfo, IRadiusPipelineExecutionContext context, ILdapServerConfiguration ldapServerConfiguration) + { + if (errorInfo.Flags.HasFlag(LdapErrorFlag.MustChangePassword)) + context.MustChangePasswordDomain = ldapServerConfiguration.ConnectionString; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs index 711c72d..b2e002c 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapFirstFactorProcessor.cs @@ -1,34 +1,26 @@ -using System.DirectoryServices.Protocols; using Microsoft.Extensions.Logging; -using Multifactor.Core.Ldap; -using Multifactor.Core.Ldap.Connection; using Multifactor.Core.Ldap.LangFeatures; using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; +using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; + namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor; public class LdapFirstFactorProcessor : IFirstFactorProcessor { - private ILdapConnectionFactory _ldapConnectionFactory; - private ILogger _logger; - + private readonly ILogger _logger; + private readonly ILdapAuthProvider _authProvider; public AuthenticationSource AuthenticationSource => AuthenticationSource.Ldap; - public LdapFirstFactorProcessor(ILdapConnectionFactory ldapConnectionFactory, - ILogger logger) + public LdapFirstFactorProcessor(ILdapAuthProvider authProvider, ILogger logger) { - Throw.IfNull(ldapConnectionFactory, nameof(ldapConnectionFactory)); - Throw.IfNull(logger, nameof(logger)); - - _ldapConnectionFactory = ldapConnectionFactory; + _authProvider = authProvider; _logger = logger; } - public Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) + public async Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) { ArgumentNullException.ThrowIfNull(context, nameof(context)); @@ -38,87 +30,21 @@ public Task ProcessFirstFactor(IRadiusPipelineExecutionContext context) if (context.LdapServerConfiguration is null) throw new InvalidOperationException("No Ldap servers configured."); - if (string.IsNullOrWhiteSpace(radiusPacket.UserName)) - { - _logger.LogWarning("Can't find User-Name in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); - Reject(context); - return Task.CompletedTask; - } - - var transformedName = UserNameTransformation.Transform(radiusPacket.UserName, context.UserNameTransformRules.BeforeFirstFactor); - - var passphrase = context.Passphrase; - if (string.IsNullOrWhiteSpace(passphrase.Raw)) - { - _logger.LogWarning("No User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); - Reject(context); - return Task.CompletedTask; - } + var authProcessor = _authProvider.GetLdapAuthProcessor(radiusPacket.AuthenticationType); - if (string.IsNullOrWhiteSpace(passphrase.Password)) - { - _logger.LogWarning("Can't parse User-Password in message id={id} from {host:l}:{port}", radiusPacket.Identifier, context.RemoteEndpoint.Address, context.RemoteEndpoint.Port); - Reject(context); - return Task.CompletedTask; - } - - var isValid = ValidateUserCredentials(context, transformedName, passphrase.Password); - if (!isValid) + if (authProcessor is null) + throw new InvalidOperationException("No Ldap auth processors configured."); + + var isValid = await authProcessor.Auth(context); + + if (!isValid.IsSuccess) { Reject(context); - return Task.CompletedTask; + return; } - _logger.LogInformation("User '{user:l}' credential and status verified successfully at {endpoint:l}", transformedName, context.LdapServerConfiguration.ConnectionString); + _logger.LogInformation("User '{user:l}' credential and status verified successfully at {endpoint:l}", radiusPacket.UserName, context.LdapServerConfiguration.ConnectionString); Accept(context); - return Task.CompletedTask; - } - - private bool ValidateUserCredentials( - IRadiusPipelineExecutionContext context, - string login, - string password) - { - var serverConfig = context.LdapServerConfiguration; - try - { - using var connection = GetConnection( - serverConfig.ConnectionString, - login, - password, - serverConfig.BindTimeoutInSeconds); - - return true; - } - catch (Exception ex) - { - if (ex is not LdapException ldapException) - { - _logger.LogError(ex, "Verification user '{user:l}' at {ldapUri:l} failed", login, serverConfig.ConnectionString); - return false; - } - - var info = GetLdapErrorInfo(ldapException); - if (info != null) - ProcessErrorReason(info, context, serverConfig); - - _logger.LogWarning(ldapException, "Verification user '{user:l}' at {ldapUri:l} failed: {dataReason:l}", login, serverConfig.ConnectionString, info?.ReasonText); - } - - return false; - } - - private ILdapConnection GetConnection(string connectionString, string userName, string password, - int bindTimeoutInSeconds) - { - var connectionOptions = new LdapConnectionOptions( - new LdapConnectionString(connectionString), - AuthType.Basic, - userName, - password, - TimeSpan.FromSeconds(bindTimeoutInSeconds)); - - return _ldapConnectionFactory.CreateConnection(connectionOptions); } private void Reject(IRadiusPipelineExecutionContext context) @@ -130,18 +56,4 @@ private void Accept(IRadiusPipelineExecutionContext context) { context.AuthenticationState.FirstFactorStatus = AuthenticationStatus.Accept; } - - private LdapErrorReasonInfo? GetLdapErrorInfo(LdapException exception) - { - if (string.IsNullOrWhiteSpace(exception.ServerErrorMessage)) - return null; - var reason = LdapErrorReasonInfo.Create(exception.ServerErrorMessage); - return reason; - } - - private void ProcessErrorReason(LdapErrorReasonInfo errorInfo, IRadiusPipelineExecutionContext context, ILdapServerConfiguration ldapServerConfiguration) - { - if (errorInfo.Flags.HasFlag(LdapErrorFlag.MustChangePassword)) - context.MustChangePasswordDomain = ldapServerConfiguration.ConnectionString; - } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs index 28b511d..e3f1ba5 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs +++ b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/IResponseInformation.cs @@ -1,8 +1,12 @@ +using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; + namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; public interface IResponseInformation { string? ReplyMessage { get; set; } - public string? State { get; set; } + string? State { get; set; } + + Dictionary Attributes { get; set; } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs index 629a992..1786988 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs +++ b/src/Multifactor.Radius.Adapter.v2/Core/Pipeline/ResponseInformation.cs @@ -1,3 +1,5 @@ +using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; + namespace Multifactor.Radius.Adapter.v2.Core.Pipeline; public class ResponseInformation : IResponseInformation @@ -5,4 +7,6 @@ public class ResponseInformation : IResponseInformation public string? ReplyMessage { get; set; } public string? State { get; set; } + + public Dictionary Attributes { get; set; } = new(); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs b/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs index fffa69d..2d50568 100644 --- a/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs @@ -11,6 +11,8 @@ using Multifactor.Radius.Adapter.v2.Core.Configuration.Service; using Multifactor.Radius.Adapter.v2.Core.Configuration.Service.Build; using Multifactor.Radius.Adapter.v2.Core.FirstFactor; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth.MsChapV2; using Multifactor.Radius.Adapter.v2.Core.Ldap; using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; @@ -150,6 +152,9 @@ public static void AddFirstFactor(this IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); } private static void AddPipelineSteps(this IServiceCollection services) diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/GetUserPasswordRequest.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/GetUserPasswordRequest.cs new file mode 100644 index 0000000..edf2e4d --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/GetUserPasswordRequest.cs @@ -0,0 +1,23 @@ +using Multifactor.Core.Ldap.Schema; +using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; +using Multifactor.Radius.Adapter.v2.Core.Ldap; + +namespace Multifactor.Radius.Adapter.v2.Services.Ldap; + +public class GetUserPasswordRequest +{ + public ILdapProfile Profile { get; } + public ILdapServerConfiguration ServerConfiguration { get; } + public ILdapSchema Schema { get; } + + public GetUserPasswordRequest(ILdapProfile profile, ILdapServerConfiguration configuration, ILdapSchema schema) + { + ArgumentNullException.ThrowIfNull(profile); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(schema); + + Profile = profile; + ServerConfiguration = configuration; + Schema = schema; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs index 39a0056..d517d1f 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs +++ b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/ILdapProfileService.cs @@ -6,4 +6,5 @@ public interface ILdapProfileService { ILdapProfile? FindUserProfile(FindUserProfileRequest request); Task ChangeUserPasswordAsync(ChangeUserPasswordRequest request); + Task GetUserPassword(GetUserPasswordRequest request); } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs index 7118882..c0a52b9 100644 --- a/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs +++ b/src/Multifactor.Radius.Adapter.v2/Services/Ldap/LdapProfileService.cs @@ -1,4 +1,5 @@ using System.DirectoryServices.Protocols; +using System.Text; using Microsoft.Extensions.Logging; using Multifactor.Core.Ldap; using Multifactor.Core.Ldap.Connection; @@ -56,6 +57,12 @@ public Task ChangeUserPasswordAsync(ChangeUserPasswordRe return passwordChanger.ChangeUserPasswordAsync(request.NewPassword, request.Profile); } + public async Task GetUserPassword(GetUserPasswordRequest request) + { + await Task.CompletedTask; + return Encoding.ASCII.GetBytes("Qwerty123!"); + } + private string GetFilter(UserIdentity identity, ILdapSchema schema) { var identityAttribute = GetIdentityAttribute(identity, schema); From ccd571c2d4b9e0868bdc65145625ea9151f0560c Mon Sep 17 00:00:00 2001 From: "s.naidenov" Date: Tue, 30 Sep 2025 13:42:28 +0700 Subject: [PATCH 2/3] DEV-270 merge fixes --- .../LdapFirstFactorProcessorTests.cs | 100 ------------ .../PapAuthProcessorTests.cs | 100 +----------- .../LdapFirstFactorProcessorTests.cs | 152 ++++++++---------- .../FirstFactorAuth/PapAuthProcessorTests.cs | 105 ++++++++++++ .../FirstFactor/LdapAuth/PapAuthProcessor.cs | 33 +++- .../Extensions/ServiceCollectionExtensions.cs | 3 + 6 files changed, 204 insertions(+), 289 deletions(-) delete mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs create mode 100644 src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs deleted file mode 100644 index 2b23f5c..0000000 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/LdapFirstFactorProcessorTests.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; -using Multifactor.Radius.Adapter.v2.Core.Radius; -using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; -using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; - -namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; - -[Collection("ActiveDirectory")] -public class LdapFirstFactorProcessorTests -{ - [Fact] - public async Task LdapFirstFactorProcessor_NoRequestPacket_ShouldThrow() - { - var authProviderMock = new Mock(); - var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - contextMock.Setup(x => x.RequestPacket).Returns(() => null); - - await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); - } - - [Fact] - public async Task LdapFirstFactorProcessor_NoLdapServerConfiguration_ShouldThrow() - { - var authProviderMock = new Mock(); - var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - contextMock.Setup(x => x.LdapServerConfiguration).Returns(() => null); - contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); - - await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); - } - - [Fact] - public async Task LdapFirstFactorProcessor_NoAuthProcessors_ShouldThrow() - { - var authProviderMock = new Mock(); - authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(() => null); - - var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); - - await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); - } - - [Fact] - public async Task LdapFirstFactorProcessor_AuthFailed_ShouldReject() - { - var authProviderMock = new Mock(); - var authProcessorMock = new Mock(); - authProcessorMock.Setup(x=> x.Auth(It.IsAny())).ReturnsAsync(new AuthResult() { IsSuccess = false }); - authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(authProcessorMock.Object); - - var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - var authState = new AuthenticationState(); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - - await processor.ProcessFirstFactor(contextMock.Object); - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - } - - [Fact] - public async Task LdapFirstFactorProcessor_AuthSucceed_ShouldReject() - { - var authProviderMock = new Mock(); - var authProcessorMock = new Mock(); - authProcessorMock.Setup(x=> x.Auth(It.IsAny())).ReturnsAsync(new AuthResult() { IsSuccess = true }); - authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(authProcessorMock.Object); - - var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - - var contextMock = new Mock(); - - var authState = new AuthenticationState(); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - - await processor.ProcessFirstFactor(contextMock.Object); - Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); - } -} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs index fc73a3e..b9082bd 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs @@ -1,30 +1,26 @@ -using System.DirectoryServices.Protocols; -using System.Net; -using System.Runtime.InteropServices; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Multifactor.Core.Ldap.Connection; using Multifactor.Radius.Adapter.v2.Core; using Multifactor.Radius.Adapter.v2.Core.Auth; using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; using Multifactor.Radius.Adapter.v2.Core.Ldap; using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; using Multifactor.Radius.Adapter.v2.Tests.Fixture; -using ILdapConnectionFactory = Multifactor.Core.Ldap.Connection.LdapConnectionFactory.ILdapConnectionFactory; namespace Multifactor.Radius.Adapter.v2.Tests.FirstFactorAuthTests; -[Collection("ActiveDirectory")] +[Collection("LDAP")] public class PapAuthProcessorTests { [Fact] public async Task LdapFirstFactorProcessor_CorrectCredentials_ShouldAccept() { var sensitiveData = GetConfig(); - var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), NullLogger.Instance); + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), new Mock().Object, NullLogger.Instance); var contextMock = new Mock(); var packetMock = new Mock(); @@ -52,7 +48,7 @@ public async Task LdapFirstFactorProcessor_CorrectCredentials_ShouldAccept() public async Task LdapFirstFactorProcessor_IncorrectPassword_ShouldReject() { var sensitiveData = GetConfig(); - var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), NullLogger.Instance); + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), new Mock().Object, NullLogger.Instance); var contextMock = new Mock(); var packetMock = new Mock(); var serverSettings = new Mock(); @@ -77,7 +73,7 @@ public async Task LdapFirstFactorProcessor_IncorrectPassword_ShouldReject() public async Task LdapFirstFactorProcessor_IncorrectLogin_ShouldReject() { var sensitiveData = GetConfig(); - var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), NullLogger.Instance); + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), new Mock().Object, NullLogger.Instance); var contextMock = new Mock(); var packetMock = new Mock(); var serverSettings = new Mock(); @@ -98,92 +94,6 @@ public async Task LdapFirstFactorProcessor_IncorrectLogin_ShouldReject() Assert.False(result.IsSuccess); } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyLogin_ShouldReject(string login) - { - var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), - NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns(login); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("correctLogin"); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - - var result = await processor.Auth(contextMock.Object); - Assert.False(result.IsSuccess); - } - - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyPassword_ShouldReject(string pwd) - { - var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), - NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("correctLogin"); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(pwd, PreAuthModeDescriptor.Default)); - - var result = await processor.Auth(contextMock.Object); - Assert.False(result.IsSuccess); - } - - [Fact] - public async Task LdapFirstFactorProcessor_MustChangePasswordResponse_ShouldReject() - { - var factoryMock = new Mock(); - factoryMock.Setup(x => x.CreateConnection(It.IsAny())).Throws(GetLdapException); - factoryMock.Setup(x => x.TargetPlatform).Returns(OSPlatform.Windows); - var factory = new CustomLdapConnectionFactory([factoryMock.Object]); - var processor = new PapAuthProcessor(factory, NullLogger.Instance); - var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("user"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns("your.domain"); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.SetupProperty(x => x.MustChangePasswordDomain); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - var context = contextMock.Object; - - var result = await processor.Auth(context); - Assert.False(result.IsSuccess); - Assert.Equal("your.domain", context.MustChangePasswordDomain); - } - - private LdapException GetLdapException() - { - var ex = new LdapException(1, "message", "data 773"); - return ex; - } - private Dictionary GetConfig() { return ConfigUtils.GetConfigSensitiveData("LdapFirstFactorProcessorTests.txt", "|"); diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs index 7e50abb..bd64fe2 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/LdapFirstFactorProcessorTests.cs @@ -1,123 +1,99 @@ -using System.DirectoryServices.Protocols; -using System.Net; -using System.Runtime.InteropServices; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Multifactor.Core.Ldap.Connection; -using Multifactor.Core.Ldap.Schema; -using Multifactor.Radius.Adapter.v2.Core; using Multifactor.Radius.Adapter.v2.Core.Auth; -using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; using Multifactor.Radius.Adapter.v2.Core.FirstFactor; -using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; -using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; +using Multifactor.Radius.Adapter.v2.Core.Radius; using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; -using Multifactor.Radius.Adapter.v2.Tests.Fixture; -using ILdapConnectionFactory = Multifactor.Core.Ldap.Connection.LdapConnectionFactory.ILdapConnectionFactory; namespace Multifactor.Radius.Adapter.v2.Tests.Unit.FirstFactorAuth; public class LdapFirstFactorProcessorTests { - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyLogin_ShouldReject(string login) + [Fact] + public async Task LdapFirstFactorProcessor_NoRequestPacket_ShouldThrow() { - //Arrange - var formatterProviderMock = new LdapBindNameFormatterProvider([]); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), formatterProviderMock, NullLogger.Instance); + var authProviderMock = new Mock(); + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); + var contextMock = new Mock(); - var packetMock = new Mock(); - var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns(login); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("correctLogin"); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); - contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + + contextMock.Setup(x => x.RequestPacket).Returns(() => null); + + await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); + } + + [Fact] + public async Task LdapFirstFactorProcessor_NoLdapServerConfiguration_ShouldThrow() + { + var authProviderMock = new Mock(); + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - //Act - await processor.ProcessFirstFactor(contextMock.Object); + var contextMock = new Mock(); - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(() => null); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); + + await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); } + + [Fact] + public async Task LdapFirstFactorProcessor_NoAuthProcessors_ShouldThrow() + { + var authProviderMock = new Mock(); + authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(() => null); + + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); - [Theory] - [ClassData(typeof(EmptyStringsListInput))] - public async Task LdapFirstFactorProcessor_EmptyPassword_ShouldReject(string pwd) + var contextMock = new Mock(); + + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); + + await Assert.ThrowsAsync(() => processor.ProcessFirstFactor(contextMock.Object)); + } + + [Fact] + public async Task LdapFirstFactorProcessor_AuthFailed_ShouldReject() { - //Arrange - var formatterProviderMock = new LdapBindNameFormatterProvider([]); - var processor = new LdapFirstFactorProcessor(new CustomLdapConnectionFactory(), formatterProviderMock, NullLogger.Instance); + var authProviderMock = new Mock(); + var authProcessorMock = new Mock(); + authProcessorMock.Setup(x=> x.Auth(It.IsAny())).ReturnsAsync(new AuthResult() { IsSuccess = false }); + authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(authProcessorMock.Object); + + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); + var contextMock = new Mock(); - var packetMock = new Mock(); + var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("correctLogin"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns(pwd); - packetMock.Setup(x => x.Identifier).Returns(0); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - //Act await processor.ProcessFirstFactor(contextMock.Object); - - //Assert Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); } - + [Fact] - public async Task LdapFirstFactorProcessor_MustChangePasswordResponse_ShouldReject() + public async Task LdapFirstFactorProcessor_AuthSucceed_ShouldReject() { - //Arrange - var factoryMock = new Mock(); - factoryMock.Setup(x => x.CreateConnection(It.IsAny())).Throws(GetLdapException); - factoryMock.Setup(x => x.TargetPlatform).Returns(OSPlatform.Windows); - var factory = new CustomLdapConnectionFactory([factoryMock.Object]); - var formatterProviderMock = new Mock(); - var processor = new LdapFirstFactorProcessor(factory, formatterProviderMock.Object, NullLogger.Instance); + var authProviderMock = new Mock(); + var authProcessorMock = new Mock(); + authProcessorMock.Setup(x=> x.Auth(It.IsAny())).ReturnsAsync(new AuthResult() { IsSuccess = true }); + authProviderMock.Setup(x => x.GetLdapAuthProcessor(It.IsAny())).Returns(authProcessorMock.Object); + + var processor = new LdapFirstFactorProcessor(authProviderMock.Object, NullLogger.Instance); + var contextMock = new Mock(); - var packetMock = new Mock(); - var serverSettings = new Mock(); + var authState = new AuthenticationState(); - var transformRules = new UserNameTransformRules(); - packetMock.Setup(x => x.UserName).Returns("user"); - packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); - contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); - serverSettings.Setup(x => x.ConnectionString).Returns("your.domain"); - serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); - contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.RequestPacket).Returns(new Mock().Object); contextMock.Setup(x => x.AuthenticationState).Returns(authState); - contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); - contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - contextMock.SetupProperty(x => x.MustChangePasswordDomain); - contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); - contextMock.Setup(x => x.LdapSchema.LdapServerImplementation).Returns(LdapImplementation.ActiveDirectory); - var context = contextMock.Object; - - //Act - await processor.ProcessFirstFactor(context); - //Assert - Assert.Equal(AuthenticationStatus.Reject, authState.FirstFactorStatus); - Assert.Equal("your.domain", context.MustChangePasswordDomain); - } - - private LdapException GetLdapException() - { - var ex = new LdapException(1, "message", "data 773"); - return ex; + await processor.ProcessFirstFactor(contextMock.Object); + Assert.Equal(AuthenticationStatus.Accept, authState.FirstFactorStatus); } } \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs new file mode 100644 index 0000000..a6bbae6 --- /dev/null +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs @@ -0,0 +1,105 @@ +using System.DirectoryServices.Protocols; +using System.Net; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Multifactor.Core.Ldap.Connection; +using Multifactor.Radius.Adapter.v2.Core; +using Multifactor.Radius.Adapter.v2.Core.Auth; +using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; +using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; +using Multifactor.Radius.Adapter.v2.Core.Ldap; +using Multifactor.Radius.Adapter.v2.Core.Radius.Packet; +using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using Multifactor.Radius.Adapter.v2.Tests.Fixture; +using ILdapConnectionFactory = Multifactor.Core.Ldap.Connection.LdapConnectionFactory.ILdapConnectionFactory; + +namespace Multifactor.Radius.Adapter.v2.Tests.Unit.FirstFactorAuth; + +public class PapAuthProcessorTests +{ + [Theory] + [ClassData(typeof(EmptyStringsListInput))] + public async Task LdapFirstFactorProcessor_EmptyLogin_ShouldReject(string login) + { + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), new Mock().Object, NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns(login); + packetMock.Setup(x => x.TryGetUserPassword()).Returns("correctLogin"); + packetMock.Setup(x => x.Identifier).Returns(0); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + + var result = await processor.Auth(contextMock.Object); + Assert.False(result.IsSuccess); + } + + [Theory] + [ClassData(typeof(EmptyStringsListInput))] + public async Task LdapFirstFactorProcessor_EmptyPassword_ShouldReject(string pwd) + { + var processor = new PapAuthProcessor(new CustomLdapConnectionFactory(), new Mock().Object, NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns("correctLogin"); + packetMock.Setup(x => x.Identifier).Returns(0); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + contextMock.Setup(x => x.RemoteEndpoint).Returns(IPEndPoint.Parse("127.0.0.1:8080")); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(new Mock().Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(pwd, PreAuthModeDescriptor.Default)); + + var result = await processor.Auth(contextMock.Object); + Assert.False(result.IsSuccess); + } + + [Fact] + public async Task LdapFirstFactorProcessor_MustChangePasswordResponse_ShouldReject() + { + var factoryMock = new Mock(); + factoryMock.Setup(x => x.CreateConnection(It.IsAny())).Throws(GetLdapException); + factoryMock.Setup(x => x.TargetPlatform).Returns(OSPlatform.Windows); + var factory = new CustomLdapConnectionFactory([factoryMock.Object]); + var processor = new PapAuthProcessor(factory, new Mock().Object, NullLogger.Instance); + var contextMock = new Mock(); + var packetMock = new Mock(); + var serverSettings = new Mock(); + var authState = new AuthenticationState(); + var transformRules = new UserNameTransformRules(); + packetMock.Setup(x => x.UserName).Returns("user"); + packetMock.Setup(x => x.TryGetUserPassword()).Returns("pwd"); + contextMock.Setup(x => x.RequestPacket).Returns(packetMock.Object); + serverSettings.Setup(x => x.ConnectionString).Returns("your.domain"); + serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); + contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + contextMock.Setup(x => x.AuthenticationState).Returns(authState); + contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); + contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); + contextMock.SetupProperty(x => x.MustChangePasswordDomain); + contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + var context = contextMock.Object; + + var result = await processor.Auth(context); + Assert.False(result.IsSuccess); + Assert.Equal("your.domain", context.MustChangePasswordDomain); + } + + private LdapException GetLdapException() + { + var ex = new LdapException(1, "message", "data 773"); + return ex; + } +} \ No newline at end of file diff --git a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs index 8b3eafd..b5d8093 100644 --- a/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs +++ b/src/Multifactor.Radius.Adapter.v2/Core/FirstFactor/LdapAuth/PapAuthProcessor.cs @@ -3,9 +3,11 @@ using Multifactor.Core.Ldap; using Multifactor.Core.Ldap.Connection; using Multifactor.Radius.Adapter.v2.Core.Configuration.Client; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; using Multifactor.Radius.Adapter.v2.Core.Ldap; using Multifactor.Radius.Adapter.v2.Core.Radius; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Context; +using ILdapConnection = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnection; using ILdapConnectionFactory = Multifactor.Radius.Adapter.v2.Core.Ldap.ILdapConnectionFactory; namespace Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; @@ -14,12 +16,14 @@ public class PapAuthProcessor : ILdapAuthProcessor { private readonly ILogger _logger; private ILdapConnectionFactory _ldapConnectionFactory; + private readonly ILdapBindNameFormatterProvider _ldapBindNameFormatterProvider; public AuthenticationType AuthenticationType => AuthenticationType.PAP; - public PapAuthProcessor(ILdapConnectionFactory ldapConnectionFactory, ILogger logger) + public PapAuthProcessor(ILdapConnectionFactory ldapConnectionFactory, ILdapBindNameFormatterProvider ldapBindNameFormatterProvider, ILogger logger) { _ldapConnectionFactory = ldapConnectionFactory; + _ldapBindNameFormatterProvider = ldapBindNameFormatterProvider; _logger = logger; } @@ -58,11 +62,29 @@ private bool ValidateUserCredentials( string password) { var serverConfig = context.LdapServerConfiguration; + if (serverConfig is null) + throw new InvalidOperationException("No Ldap servers configured."); + + var bindName = string.Empty; + try { + var ldapImpl = context.LdapSchema!.LdapServerImplementation; + var formatter = _ldapBindNameFormatterProvider.GetLdapBindNameFormatter(ldapImpl); + if (formatter is null) + _logger.LogWarning("No LDAP bind name formatter configured for '{implementation}' implementation.", ldapImpl); + + var formatted = string.Empty; + if (context.UserLdapProfile is not null) + formatted = formatter?.FormatName(login, context.UserLdapProfile); + + bindName = string.IsNullOrWhiteSpace(formatted) ? login : formatted; + + _logger.LogDebug("Use '{name}' for LDAP bind.", bindName); + using var connection = GetConnection( serverConfig.ConnectionString, - login, + bindName, password, serverConfig.BindTimeoutInSeconds); @@ -72,7 +94,7 @@ private bool ValidateUserCredentials( { if (ex is not LdapException ldapException) { - _logger.LogError(ex, "Verification user '{user:l}' at {ldapUri:l} failed", login, serverConfig.ConnectionString); + _logger.LogError(ex, "Verification user '{user:l}' at {ldapUri:l} failed", bindName, serverConfig.ConnectionString); return false; } @@ -80,14 +102,13 @@ private bool ValidateUserCredentials( if (info != null) ProcessErrorReason(info, context, serverConfig); - _logger.LogWarning(ldapException, "Verification user '{user:l}' at {ldapUri:l} failed: {dataReason:l}", login, serverConfig.ConnectionString, info?.ReasonText); + _logger.LogWarning(ldapException, "Verification user '{user:l}' at {ldapUri:l} failed: {dataReason:l}", bindName, serverConfig.ConnectionString, info?.ReasonText); } return false; } - private ILdapConnection GetConnection(string connectionString, string userName, string password, - int bindTimeoutInSeconds) + private ILdapConnection GetConnection(string connectionString, string userName, string password, int bindTimeoutInSeconds) { var connectionOptions = new LdapConnectionOptions( new LdapConnectionString(connectionString), diff --git a/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs b/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs index 37a62e3..0958187 100644 --- a/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs +++ b/src/Multifactor.Radius.Adapter.v2/Extensions/ServiceCollectionExtensions.cs @@ -13,6 +13,8 @@ using Multifactor.Radius.Adapter.v2.Core.Configuration.Service.Build; using Multifactor.Radius.Adapter.v2.Core.FirstFactor; using Multifactor.Radius.Adapter.v2.Core.FirstFactor.BindNameFormat; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth; +using Multifactor.Radius.Adapter.v2.Core.FirstFactor.LdapAuth.MsChapV2; using Multifactor.Radius.Adapter.v2.Core.Ldap; using Multifactor.Radius.Adapter.v2.Core.Radius.Attributes; using Multifactor.Radius.Adapter.v2.Infrastructure.Configuration.RadiusAdapter.Build; @@ -21,6 +23,7 @@ using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Builder; using Multifactor.Radius.Adapter.v2.Infrastructure.Pipeline.Steps; +using Multifactor.Radius.Adapter.v2.Server; using Multifactor.Radius.Adapter.v2.Server.Udp; using Multifactor.Radius.Adapter.v2.Services.AuthenticatedClientCache; using Multifactor.Radius.Adapter.v2.Services.Cache; From 55cdda4686baa0856208b17eacb6ddae0c4e7acd Mon Sep 17 00:00:00 2001 From: "s.naidenov" Date: Tue, 30 Sep 2025 14:05:19 +0700 Subject: [PATCH 3/3] DEV-270 tests fix --- .../FirstFactorAuthTests/PapAuthProcessorTests.cs | 4 ++++ .../Unit/FirstFactorAuth/PapAuthProcessorTests.cs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs index b9082bd..43d8cfb 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/FirstFactorAuthTests/PapAuthProcessorTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; +using Multifactor.Core.Ldap.Schema; using Multifactor.Radius.Adapter.v2.Core; using Multifactor.Radius.Adapter.v2.Core.Auth; using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; @@ -32,6 +33,7 @@ public async Task LdapFirstFactorProcessor_CorrectCredentials_ShouldAccept() serverSettings.Setup(x => x.ConnectionString).Returns(sensitiveData["ConnectionString"]); serverSettings.Setup(x => x.BindTimeoutInSeconds).Returns(30); contextMock.Setup(x => x.LdapServerConfiguration).Returns(serverSettings.Object); + contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); var authState = new AuthenticationState(); contextMock.Setup(x => x.AuthenticationState).Returns(authState); @@ -64,6 +66,7 @@ public async Task LdapFirstFactorProcessor_IncorrectPassword_ShouldReject() contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); var result = await processor.Auth(contextMock.Object); Assert.False(result.IsSuccess); @@ -89,6 +92,7 @@ public async Task LdapFirstFactorProcessor_IncorrectLogin_ShouldReject() contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); var result = await processor.Auth(contextMock.Object); Assert.False(result.IsSuccess); diff --git a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs index a6bbae6..a240378 100644 --- a/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs +++ b/src/Multifactor.Radius.Adapter.v2.Tests/Unit/FirstFactorAuth/PapAuthProcessorTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Multifactor.Core.Ldap.Connection; +using Multifactor.Core.Ldap.Schema; using Multifactor.Radius.Adapter.v2.Core; using Multifactor.Radius.Adapter.v2.Core.Auth; using Multifactor.Radius.Adapter.v2.Core.Auth.PreAuthMode; @@ -38,7 +39,8 @@ public async Task LdapFirstFactorProcessor_EmptyLogin_ShouldReject(string login) contextMock.Setup(x => x.AuthenticationState).Returns(authState); contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); - + contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); + var result = await processor.Auth(contextMock.Object); Assert.False(result.IsSuccess); } @@ -61,6 +63,7 @@ public async Task LdapFirstFactorProcessor_EmptyPassword_ShouldReject(string pwd contextMock.Setup(x => x.UserNameTransformRules).Returns(transformRules); contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse(pwd, PreAuthModeDescriptor.Default)); + contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); var result = await processor.Auth(contextMock.Object); Assert.False(result.IsSuccess); @@ -90,6 +93,7 @@ public async Task LdapFirstFactorProcessor_MustChangePasswordResponse_ShouldReje contextMock.Setup(x => x.PreAuthnMode).Returns(PreAuthModeDescriptor.Default); contextMock.SetupProperty(x => x.MustChangePasswordDomain); contextMock.Setup(x => x.Passphrase).Returns(UserPassphrase.Parse("123456", PreAuthModeDescriptor.Default)); + contextMock.Setup(x => x.LdapSchema).Returns(new Mock().Object); var context = contextMock.Object; var result = await processor.Auth(context);