Commit e3032a9d authored by Eugene Bekker's avatar Eugene Bekker Committed by Geovanni Perez

Adding changes to implement pure LDAP provider (#196)

* adding a pure LDAP-based Password Change Provider

* woops!

* adding some logging to help troubleshoot

* correcting bug in TLS config

* async behavior bug

* passcode forces a domain part on the username

* adding conditional compilation to include LDAP provider

* typos and clarifications

* options validations and warnings

* cleaning up and delcaring latest lang ver to fix ci issues

* careless mistake
parent abb1d28a
......@@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unosquare.PassCore.Password
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unosquare.PassCore.Common", "src\Unosquare.PassCore.Common\Unosquare.PassCore.Common.csproj", "{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zyborg.PassCore.PasswordProvider.LDAP", "src\Zyborg.PassCore.PasswordProvider.LDAP\Zyborg.PassCore.PasswordProvider.LDAP.csproj", "{EA6E34EB-4528-48D5-8B4E-CBD54F24F945}"
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -37,6 +39,10 @@ Global
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}.Release|Any CPU.Build.0 = Release|Any CPU
{EA6E34EB-4528-48D5-8B4E-CBD54F24F945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA6E34EB-4528-48D5-8B4E-CBD54F24F945}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA6E34EB-4528-48D5-8B4E-CBD54F24F945}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA6E34EB-4528-48D5-8B4E-CBD54F24F945}.Release|Any CPU.Build.0 = Release|Any CPU
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
......@@ -45,6 +51,7 @@ Global
{22E2F79B-7816-4FAB-894D-112759551796} = {0A003964-77CA-4779-BD97-BADDD710A745}
{4329B3BC-3CD4-4A19-AAAC-6EB7200DA29A} = {0A003964-77CA-4779-BD97-BADDD710A745}
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A} = {0A003964-77CA-4779-BD97-BADDD710A745}
{EA6E34EB-4528-48D5-8B4E-CBD54F24F945} = {0A003964-77CA-4779-BD97-BADDD710A745}
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {30FE6198-367B-44D3-97CC-50927E101F3E}
......@@ -9,11 +9,15 @@ namespace Unosquare.PassCore.Web
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PasswordProvider;
using Models;
#if !DEBUG
using Microsoft.AspNetCore.Rewrite;
using Zyborg.PassCore.PasswordProvider.LDAP;
using PasswordProvider;
/// <summary>
/// Represents this application's main class
......@@ -70,6 +74,9 @@ namespace Unosquare.PassCore.Web
services.AddSingleton<IPasswordChangeProvider, DebugPasswordChangeProvider>();
services.AddSingleton<IPasswordChangeProvider, LdapPasswordChangeProvider>();
services.AddSingleton<IPasswordChangeProvider, PasswordChangeProvider>();
<Project Sdk="Microsoft.NET.Sdk.Web">
......@@ -12,6 +13,7 @@
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.*" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.*" />
......@@ -23,9 +25,11 @@
<PackageReference Include="Newtonsoft.Json" Version="11.0.*" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.*" />
<Content Remove="ClientApp\**" />
<Content Include="ClientApp\manifest.json" />
......@@ -35,10 +39,24 @@
<ProjectReference Include="..\Unosquare.PassCore.Common\Unosquare.PassCore.Common.csproj" />
<ItemGroup Condition="'$(PASSCORE_PROVIDER)' == ''">
<!-- Default Password Provider - Win32 -->
<ProjectReference Include="..\Unosquare.PassCore.PasswordProvider\Unosquare.PassCore.PasswordProvider.csproj" />
<ItemGroup Condition="'$(PASSCORE_PROVIDER)' == 'LDAP'">
<!-- Pure LDAP provider -->
<ProjectReference Include="..\Zyborg.PassCore.PasswordProvider.LDAP\Zyborg.PassCore.PasswordProvider.LDAP.csproj" />
<PropertyGroup Condition="'$(PASSCORE_PROVIDER)' == 'LDAP'">
<Target Name="NpmInstall" BeforeTargets="Build" Condition="!Exists('.\package-lock.json')">
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
......@@ -47,14 +65,17 @@
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from, and then restart your command prompt or IDE." />
<Message Importance="high" Text="Performing first-run npm install..." />
<Target Name="Webpack" DependsOnTargets="NpmInstall" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
<Message Importance="high" Text="Performing run Webpack build..." />
<Exec Command="node_modules/.bin/webpack" />
<Target Name="PublishRunWebpack" DependsOnTargets="NpmInstall" BeforeTargets="ComputeFilesToPublish" Condition=" '$(Configuration)' == 'Release' ">
<Message Importance="high" Text="Performing run Webpack publish..." />
<Exec Command="node_modules/.bin/webpack -p" />
<Target Name="CopyFilestoPublish" DependsOnTargets="PublishRunWebpack" AfterTargets="PublishRunWebpack">
<Message Importance="high" Text="Coping files to publish..." />
......@@ -65,4 +86,5 @@
using Novell.Directory.Ldap;
namespace Zyborg.PassCore.PasswordProvider.LDAP
public class LdapPasswordChangeOptions
/// Required, one or more hostnames or IP addresses which expose an LDAP/LDAPS
/// service endpoint that will be connected to. If more than one host is
/// specified, then each will be tried in turn until a successful, secure
/// connection is established.
public string[] LdapHostnames { get; set; }
/// Optional, defaults to 636 -- the default port for LDAPS (i.e. LDAP over TLS).
/// A common alternative is to use the default LDAP port, 389, however this port
/// typically is not-secured and requires the "StartTLS" flag enabled.
public int LdapPort { get; set; } = LdapConnection.DEFAULT_SSL_PORT;
/// Optional, if 'true', then the specified port is a non-secured port by default
/// and requires the use of the "StartTLS" command over LDAP to enable TLS.
public bool LdapStartTls { get; set; } = false;
/// Optional, if 'true', then server certificates will be ignored for expiration
/// or common name mismatch. Note this is a SUPERSET of the LdapIgnoreTlsValidation
/// options, so you don't have to set both.
public bool LdapIgnoreTlsErrors { get; set; } = false;
/// Optional, if 'true', then server certificates will be accepted regardless
/// of being signed by a trusted CA or intermediary (e.g. self-signed).
public bool LdapIgnoreTlsValidation { get; set; } = false;
/// The distinguished name (DN) of the privileged user account in the
/// target directory with permission to reset user passwords.
public string LdapBindUserDN { get; set; }
/// The password of the privileged user account in the
/// target directory with permission to reset user passwords.
public string LdapBindPassword { get; set; }
/// Distinguished Name (DN) of the base OU from which to search for
/// the target users by their username (SAM Account Name)
public string LdapSearchBase { get; set; }
/// When the user cannot be located in the directory, you can
/// either expose that error, or hide it and treat like an arbitrary
/// bad credential -- in order to prevent brute force attack to
/// discover the presence or absence of a username.
public bool HideUserNotFound { get; set; } = true;
\ No newline at end of file
using System;
using System.Collections.Generic;
namespace Zyborg.PassCore.PasswordProvider.LDAP
public class Win32ErrorCode
/// Based on
/// <a href="">docs</a>
/// provides a list of commonly anticipated error codes from a password change request.
public static readonly IEnumerable<Win32ErrorCode> Codes = new[]
new Win32ErrorCode(0x00000005, "ERROR_ACCESS_DENIED",
"Access is denied."),
new Win32ErrorCode(0x00000056, "ERROR_INVALID_PASSWORD",
"The specified network password is not correct."),
new Win32ErrorCode(0x00000523, "ERROR_INVALID_ACCOUNT_NAME",
"The name provided is not a properly formed account name."),
new Win32ErrorCode(0x00000524, "ERROR_USER_EXISTS",
"The specified account already exists."),
new Win32ErrorCode(0x00000525, "ERROR_NO_SUCH_USER",
"The specified account does not exist."),
new Win32ErrorCode(0x0000052B, "ERROR_WRONG_PASSWORD",
"Unable to update the password. The value provided as the current password is incorrect."),
new Win32ErrorCode(0x0000052C, "ERROR_ILL_FORMED_PASSWORD",
"Unable to update the password. The value provided for the new password contains values that are not allowed in passwords."),
new Win32ErrorCode(0x0000052D, "ERROR_PASSWORD_RESTRICTION",
"Unable to update the password. The value provided for the new password does not meet the length, complexity, or history requirements of the domain."),
new Win32ErrorCode(0x0000052E, "ERROR_LOGON_FAILURE",
"Logon failure: Unknown user name or bad password."),
new Win32ErrorCode(0x0000052F, "ERROR_ACCOUNT_RESTRICTION",
"Logon failure: User account restriction. Possible reasons are blank passwords not allowed, logon hour restrictions, or a policy restriction has been enforced."),
new Win32ErrorCode(0x00000530, "ERROR_INVALID_LOGON_HOURS",
"Logon failure: Account logon time restriction violation."),
new Win32ErrorCode(0x00000531, "ERROR_INVALID_WORKSTATION",
"Logon failure: User not allowed to log on to this computer."),
new Win32ErrorCode(0x00000532, "ERROR_PASSWORD_EXPIRED",
"Logon failure: The specified account password has expired."),
new Win32ErrorCode(0x00000533, "ERROR_ACCOUNT_DISABLED",
"Logon failure: Account currently disabled."),
new Win32ErrorCode(0x00000773, "ERROR_PASSWORD_MUST_CHANGE",
"The user's password must be changed before logging on the first time."),
new Win32ErrorCode(0x00000774, "ERROR_DOMAIN_CONTROLLER_NOT_FOUND",
"Could not find the domain controller for this domain."),
new Win32ErrorCode(0x00000775, "ERROR_ACCOUNT_LOCKED_OUT",
"The referenced account is currently locked out and cannot be logged on to."),
private static Dictionary<int, Win32ErrorCode> _errorByCode =
new Dictionary<int, Win32ErrorCode>();
private static Dictionary<string, Win32ErrorCode> _errorByCodeName =
new Dictionary<string, Win32ErrorCode>(StringComparer.InvariantCultureIgnoreCase);
static Win32ErrorCode()
foreach (var c in Codes)
_errorByCode[c.Code] = c;
_errorByCodeName[c.CodeName] = c;
private Win32ErrorCode(int code, string codeName, string desc)
Code = code;
CodeName = codeName;
Description = desc;
public int Code { get; }
public string CodeName { get; }
public string Description { get; }
public override int GetHashCode() => Code;
public override bool Equals(object obj) => obj != null
&& obj is Win32ErrorCode err && Code == err.Code;
public static Win32ErrorCode ByCode(int code) =>
_errorByCode.TryGetValue(code, out var err) ? err : null;
public static Win32ErrorCode ByCodeName(string codeName) =>
_errorByCodeName.TryGetValue(codeName, out var err) ? err : null;
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.1" />
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="2.3.8" />
<ProjectReference Include="..\Unosquare.PassCore.Common\Unosquare.PassCore.Common.csproj" />
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment