diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..8e3fa284
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,252 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:suggestion
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:suggestion
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression =true:error
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_local_function = true:suggestion
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement =outside_namespace:error
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+dotnet_diagnostic.IDE0052.severity=error
+dotnet_diagnostic.IDE0064.severity=error
+dotnet_diagnostic.CA1507.severity=error
+dotnet_diagnostic.IDE0019.severity=suggestion
+dotnet_diagnostic.IDE0020.severity=suggestion
+dotnet_diagnostic.IDE0021.severity=suggestion
+dotnet_diagnostic.IDE0022.severity=suggestion
+dotnet_diagnostic.IDE0029.severity=warning
+dotnet_diagnostic.IDE0090.severity=error
+dotnet_diagnostic.IDE0120.severity=error
+dotnet_diagnostic.IDE0005.severity=error
+dotnet_diagnostic.IDE0005_gen.severity=error
+dotnet_diagnostic.IDE0028.severity=error
+dotnet_diagnostic.IDE0031.severity=error
+dotnet_diagnostic.IDE0035.severity=error
+dotnet_diagnostic.IDE0045.severity=error
+dotnet_diagnostic.IDE0056.severity=suggestion
+dotnet_diagnostic.IDE0057.severity=suggestion
+dotnet_diagnostic.IDE0063.severity=suggestion
+dotnet_diagnostic.IDE0082.severity=error
+dotnet_diagnostic.IDE0083.severity=suggestion
+[*.{cs,vb}]
+dotnet_diagnostic.CA2007.severity=silent
+dotnet_code_quality_unused_parameters=all:warning
+dotnet_style_coalesce_expression=true:error
+
+# Defining the 'public_symbols' symbol group
+dotnet_naming_symbols.public_symbols.applicable_kinds = property,method,field,event,delegate
+dotnet_naming_symbols.public_symbols.applicable_accessibilities = public
+dotnet_naming_symbols.public_symbols.required_modifiers = readonly
+
+# Defining the `first_word_upper_case_style` naming style
+dotnet_naming_style.first_word_upper_case_style.capitalization = first_word_upper
+
+# Defining the `public_members_must_be_capitalized` naming rule, by setting the symbol group to the 'public symbols' symbol group,
+dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols
+# setting the naming style to the `first_word_upper_case_style` naming style,
+dotnet_naming_rule.public_members_must_be_capitalized.style = first_word_upper_case_style
+# and setting the severity.
+dotnet_naming_rule.public_members_must_be_capitalized.severity = suggestion
+dotnet_style_null_propagation=true:error
+
+dotnet_diagnostic.IDE0051.severity = error
+dotnet_diagnostic.IDE0055.severity = error
+dotnet_diagnostic.CA1819.severity=suggestion
+dotnet_diagnostic.CA1836.severity=error
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9deda086..dc0b6ab0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,15 +4,16 @@ on: [push]
jobs:
build:
+ runs-on: windows-latest
runs-on: windows-latest
-
+
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Build everything
run: dotnet publish --configuration Release --runtime win-x64 --output "build/"
- name: Upload it !
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: build
path: build/
diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml
new file mode 100644
index 00000000..dd133d59
--- /dev/null
+++ b/.github/workflows/build_windows.yml
@@ -0,0 +1,48 @@
+name: Build Windows Binaries
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: Windows Binaries on Windows Latest
+ runs-on: windows-latest
+
+ steps:
+ - name: Cancel previous runs on the same branch
+ if: ${{ github.ref != 'refs/heads/main' }}
+ uses: styfle/cancel-workflow-action@0.9.1
+ with:
+ access_token: ${{ github.token }}
+
+ - name: Checkout Code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Set git urls to https instead of ssh
+ run: |
+ git config --global url."https://github.com/".insteadOf ssh://git@github.com/
+
+ - name: Setup .NET Core SDK
+ uses: actions/setup-dotnet@v2.0.0
+
+ - name: Build Windows binaries with build_scripts\build_windows.ps1
+ run: |
+ $env:path="C:\Program` Files` (x86)\Microsoft` Visual` Studio\2019\Enterprise\SDK\ScopeCppSDK\vc15\VC\bin\;$env:path"
+ dotnet publish --configuration Release --runtime win-x64 --output ".\passcore_output\"
+ ls ${{ github.workspace }}\passcore_output\
+
+ - name: Upload Windows binaries to artifacts
+ uses: actions/upload-artifact@v2.2.2
+ with:
+ name: passcore
+ path: ${{ github.workspace }}\passcore_output\
+
+ - name: Get tag name
+ if: startsWith(github.ref, 'refs/tags/')
+ id: tag-name
+ run: |
+ echo "::set-output name=TAG_NAME::$(echo ${{ github.ref }} | cut -d'/' -f 3)"
+ echo "::set-output name=REPO_NAME::$(echo ${{ github.repository }} | cut -d'/' -f 2)"
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index f1633adf..88f33ad9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@ FROM node:latest AS node_base
RUN echo "NODE Version:" && node --version
RUN echo "NPM Version:" && npm --version
-FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
COPY --from=node_base . .
@@ -16,7 +16,7 @@ RUN dotnet restore
RUN dotnet publish -c Release -o /app /p:PASSCORE_PROVIDER=LDAP --no-restore
# final stage/image
-FROM mcr.microsoft.com/dotnet/aspnet:5.0
+FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app ./
EXPOSE 80
diff --git a/StyleCop.Analyzers.ruleset b/StyleCop.Analyzers.ruleset
deleted file mode 100644
index 1c1bf34b..00000000
--- a/StyleCop.Analyzers.ruleset
+++ /dev/null
@@ -1,107 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Unosquare.PassCore.sln b/Unosquare.PassCore.sln
index 92f164c3..1d92648f 100644
--- a/Unosquare.PassCore.sln
+++ b/Unosquare.PassCore.sln
@@ -1,19 +1,17 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30011.22
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.32328.378
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0A003964-77CA-4779-BD97-BADDD710A745}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3EA980E1-D295-4E61-B2EE-29CFE1EDB2F1}"
ProjectSection(SolutionItems) = preProject
- appveyor.yml = appveyor.yml
.github\workflows\build.yml = .github\workflows\build.yml
Dockerfile = Dockerfile
src\Unosquare.PassCore.Web\ClientApp\package.json = src\Unosquare.PassCore.Web\ClientApp\package.json
preview.png = preview.png
README.md = README.md
- StyleCop.Analyzers.ruleset = StyleCop.Analyzers.ruleset
src\Unosquare.PassCore.Web\ClientApp\tsconfig.json = src\Unosquare.PassCore.Web\ClientApp\tsconfig.json
src\Unosquare.PassCore.Web\ClientApp\tslint.json = src\Unosquare.PassCore.Web\ClientApp\tslint.json
EndProjectSection
diff --git a/src/PwnedPasswordsSearch/PwnedPasswordsSearch.csproj b/src/PwnedPasswordsSearch/PwnedPasswordsSearch.csproj
index 353a6056..cca33ef9 100644
--- a/src/PwnedPasswordsSearch/PwnedPasswordsSearch.csproj
+++ b/src/PwnedPasswordsSearch/PwnedPasswordsSearch.csproj
@@ -1,9 +1,10 @@
-
-
-
- net5.0
- https://github.com/mikepound/pwned-search/blob/master/csharp/pwned-search.cs
- true
-
-
+
+
+ net6.0
+ https://github.com/mikepound/pwned-search/blob/master/csharp/pwned-search.cs
+ true
+ true
+ AllEnabledByDefault
+ true
+
diff --git a/src/PwnedPasswordsSearch/PwnedSearch.cs b/src/PwnedPasswordsSearch/PwnedSearch.cs
index 24d4779d..4e4bd0a7 100644
--- a/src/PwnedPasswordsSearch/PwnedSearch.cs
+++ b/src/PwnedPasswordsSearch/PwnedSearch.cs
@@ -4,65 +4,63 @@
using System.Security.Cryptography;
using System.Text;
-namespace PwnedPasswordsSearch
-{
- // Based on https://github.com/mikepound/pwned-search/blob/master/csharp/pwned-search.cs
+namespace PwnedPasswordsSearch;
+// Based on https://github.com/mikepound/pwned-search/blob/master/csharp/pwned-search.cs
- public static class PwnedSearch
+public static class PwnedSearch
+{
+ ///
+ /// Makes a call to Pwned Passwords API, asking for a set of hashes of publicly known passwords that match a partial hash of a given password.
+ /// If any of the hashes returned by the API call fully matches the hash of the plaintext, it would mean that the password has been exposed
+ /// in publicly known data breaches and thus is not safe to use.
+ /// See https://haveibeenpwned.com/API/v2#PwnedPasswords
+ ///
+ /// Password to check against Pwned Passwords API
+ /// True when the password has been Pwned
+ public static bool IsPwnedPassword(string plaintext)
{
- ///
- /// Makes a call to Pwned Passwords API, asking for a set of hashes of publicly known passwords that match a partial hash of a given password.
- /// If any of the hashes returned by the API call fully matches the hash of the plaintext, it would mean that the password has been exposed
- /// in publicly known data breaches and thus is not safe to use.
- /// See https://haveibeenpwned.com/API/v2#PwnedPasswords
- ///
- /// Password to check against Pwned Passwords API
- /// True when the password has been Pwned
- public static bool IsPwnedPassword(string plaintext)
+ try
{
- try
- {
- SHA1 sha = new SHA1CryptoServiceProvider();
- byte[] data = sha.ComputeHash(Encoding.UTF8.GetBytes(plaintext));
+ SHA1 sha = new SHA1CryptoServiceProvider();
+ byte[] data = sha.ComputeHash(Encoding.UTF8.GetBytes(plaintext));
- // Loop through each byte of the hashed data and format each one as a hexadecimal string.
- var sBuilder = new StringBuilder();
- for (int i = 0; i < data.Length; i++)
- sBuilder.Append(data[i].ToString("x2"));
+ // Loop through each byte of the hashed data and format each one as a hexadecimal string.
+ var sBuilder = new StringBuilder();
+ foreach (var t in data)
+ sBuilder.Append(t.ToString("x2"));
- var result = sBuilder.ToString().ToUpper();
+ var result = sBuilder.ToString().ToUpper();
- // Get a list of all the possible password hashes where the first 5 bytes of the hash are the same
- var url = "https://api.pwnedpasswords.com/range/" + result.Substring(0, 5);
- WebRequest request = WebRequest.Create(url);
- using var response = request.GetResponse().GetResponseStream();
- using var reader = new StreamReader(response);
- // Iterate through all possible matches and compare the rest of the hash to see if there is a full match
- // TODO: optimize-async this
- string hashToCheck = result.Substring(5);
- string line = null;
- do
+ // Get a list of all the possible password hashes where the first 5 bytes of the hash are the same
+ var url = $"https://api.pwnedpasswords.com/range/{result[..5]}";
+ WebRequest request = WebRequest.Create(url);
+ using var response = request.GetResponse().GetResponseStream();
+ using var reader = new StreamReader(response);
+ // Iterate through all possible matches and compare the rest of the hash to see if there is a full match
+ // TODO: optimize-async this
+ string hashToCheck = result[5..];
+ string line;
+ do
+ {
+ line = reader.ReadLine();
+ if (line != null)
{
- line = reader.ReadLine();
- if (line != null)
+ string[] parts = line.Split(':');
+ if (parts[0] == hashToCheck) // This is a full match: plaintext compromised!!!!
{
- string[] parts = line.Split(':');
- if (parts[0] == hashToCheck) // This is a full match: plaintext compromised!!!!
- {
- System.Diagnostics.Debug.Print("The password '{plaintext}' is publicly known and can be used in dictionary attacks");
- return true;
- }
+ System.Diagnostics.Debug.Print("The password '{plaintext}' is publicly known and can be used in dictionary attacks");
+ return true;
}
- } while (line != null);
+ }
+ } while (line != null);
- // We've run through all the candidates and none of them is a full match
- return false; // This plaintext is not publicly known
- }
- catch (Exception)
- {
- // If any weird things happens, it is safer to suppose this plaintext is compromised (hence not to be used).
- return true; // Better safe than sorry.
- }
+ // We've run through all the candidates and none of them is a full match
+ return false; // This plaintext is not publicly known
+ }
+ catch (Exception)
+ {
+ // If any weird things happens, it is safer to suppose this plaintext is compromised (hence not to be used).
+ return true; // Better safe than sorry.
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Common/ApiErrorCode.cs b/src/Unosquare.PassCore.Common/ApiErrorCode.cs
index f1e12011..c8fc0977 100644
--- a/src/Unosquare.PassCore.Common/ApiErrorCode.cs
+++ b/src/Unosquare.PassCore.Common/ApiErrorCode.cs
@@ -1,73 +1,72 @@
-namespace Unosquare.PassCore.Common
+namespace Unosquare.PassCore.Common;
+
+///
+/// Represents API error codes.
+///
+public enum ApiErrorCode
{
///
- /// Represents API error codes.
+ /// The generic
///
- public enum ApiErrorCode
- {
- ///
- /// The generic
- ///
- Generic = 0,
+ Generic = 0,
- ///
- /// The field required
- ///
- FieldRequired = 1,
+ ///
+ /// The field required
+ ///
+ FieldRequired = 1,
- ///
- /// The field mismatch
- ///
- FieldMismatch = 2,
+ ///
+ /// The field mismatch
+ ///
+ FieldMismatch = 2,
- ///
- /// The user not found
- ///
- UserNotFound = 3,
+ ///
+ /// The user not found
+ ///
+ UserNotFound = 3,
- ///
- /// The invalid credentials
- ///
- InvalidCredentials = 4,
+ ///
+ /// The invalid credentials
+ ///
+ InvalidCredentials = 4,
- ///
- /// The invalid captcha
- ///
- InvalidCaptcha = 5,
+ ///
+ /// The invalid captcha
+ ///
+ InvalidCaptcha = 5,
- ///
- /// The change not permitted
- ///
- ChangeNotPermitted = 6,
+ ///
+ /// The change not permitted
+ ///
+ ChangeNotPermitted = 6,
- ///
- /// The invalid domain
- ///
- InvalidDomain = 7,
+ ///
+ /// The invalid domain
+ ///
+ InvalidDomain = 7,
- ///
- /// LDAP problem connection
- ///
- LdapProblem = 8,
+ ///
+ /// LDAP problem connection
+ ///
+ LdapProblem = 8,
- ///
- /// Complex password issue
- ///
- ComplexPassword = 9,
+ ///
+ /// Complex password issue
+ ///
+ ComplexPassword = 9,
- ///
- /// The score is minor than the Minimum Score
- ///
- MinimumScore = 10,
+ ///
+ /// The score is minor than the Minimum Score
+ ///
+ MinimumScore = 10,
- ///
- /// The score is minor than the Minimum Score
- ///
- MinimumDistance = 11,
+ ///
+ /// The score is minor than the Minimum Score
+ ///
+ MinimumDistance = 11,
- ///
- /// The password is in Pwned database
- ///
- PwnedPassword = 12,
- }
+ ///
+ /// The password is in Pwned database
+ ///
+ PwnedPassword = 12,
}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Common/ApiErrorException.cs b/src/Unosquare.PassCore.Common/ApiErrorException.cs
index a278d8a2..39c0ad1f 100644
--- a/src/Unosquare.PassCore.Common/ApiErrorException.cs
+++ b/src/Unosquare.PassCore.Common/ApiErrorException.cs
@@ -1,36 +1,35 @@
-namespace Unosquare.PassCore.Common
-{
- using System;
+using System;
- ///
+namespace Unosquare.PassCore.Common;
+
+///
+///
+/// Special Exception to transport the ApiErrorItem.
+///
+public class ApiErrorException : Exception
+{
///
- /// Special Exception to transport the ApiErrorItem.
+ /// Initializes a new instance of the class.
///
- public class ApiErrorException : Exception
- {
- ///
- /// Initializes a new instance of the class.
- ///
- /// The message.
- /// The error code.
- public ApiErrorException(string message, ApiErrorCode errorCode = ApiErrorCode.Generic)
+ /// The message.
+ /// The error code.
+ public ApiErrorException(string message, ApiErrorCode errorCode = ApiErrorCode.Generic)
: base(message) => ErrorCode = errorCode;
- ///
- /// Gets or sets the error code.
- ///
- ///
- /// The error code.
- ///
- public ApiErrorCode ErrorCode { get; }
+ ///
+ /// Gets or sets the error code.
+ ///
+ ///
+ /// The error code.
+ ///
+ public ApiErrorCode ErrorCode { get; }
- ///
- public override string Message => $"Error Code: {ErrorCode}\r\n{base.Message}";
+ ///
+ public override string Message => $"Error Code: {ErrorCode}\r\n{base.Message}";
- ///
- /// To the API error item.
- ///
- /// An API Error Item.
- public ApiErrorItem ToApiErrorItem() => new ApiErrorItem(ErrorCode, base.Message);
- }
-}
+ ///
+ /// To the API error item.
+ ///
+ /// An API Error Item.
+ public ApiErrorItem ToApiErrorItem() => new(ErrorCode, base.Message);
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Common/ApiErrorItem.cs b/src/Unosquare.PassCore.Common/ApiErrorItem.cs
index c87d162e..6ce1f840 100644
--- a/src/Unosquare.PassCore.Common/ApiErrorItem.cs
+++ b/src/Unosquare.PassCore.Common/ApiErrorItem.cs
@@ -1,43 +1,42 @@
-namespace Unosquare.PassCore.Common
+namespace Unosquare.PassCore.Common;
+
+///
+/// Defines the fields contained in one of the items of Api Errors.
+///
+public class ApiErrorItem
{
///
- /// Defines the fields contained in one of the items of Api Errors.
+ /// Initializes a new instance of the class.
///
- public class ApiErrorItem
+ /// The error code.
+ /// The message.
+ public ApiErrorItem(ApiErrorCode errorCode, string? message = null)
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The error code.
- /// The message.
- public ApiErrorItem(ApiErrorCode errorCode, string? message = null)
- {
- ErrorCode = errorCode;
- Message = message;
- }
+ ErrorCode = errorCode;
+ Message = message;
+ }
- ///
- /// Gets or sets the error code.
- ///
- ///
- /// The error code.
- ///
- public ApiErrorCode ErrorCode { get; }
+ ///
+ /// Gets or sets the error code.
+ ///
+ ///
+ /// The error code.
+ ///
+ public ApiErrorCode ErrorCode { get; }
- ///
- /// Gets or sets the name of the field.
- ///
- ///
- /// The name of the field.
- ///
- public string? FieldName { get; set; }
+ ///
+ /// Gets or sets the name of the field.
+ ///
+ ///
+ /// The name of the field.
+ ///
+ public string? FieldName { get; set; }
- ///
- /// Gets the message.
- ///
- ///
- /// The message.
- ///
- public string? Message { get; }
- }
-}
+ ///
+ /// Gets the message.
+ ///
+ ///
+ /// The message.
+ ///
+ public string? Message { get; }
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Common/IAppSettings.cs b/src/Unosquare.PassCore.Common/IAppSettings.cs
index 0f781206..b81f07b7 100644
--- a/src/Unosquare.PassCore.Common/IAppSettings.cs
+++ b/src/Unosquare.PassCore.Common/IAppSettings.cs
@@ -1,59 +1,58 @@
-namespace Unosquare.PassCore.Common
+namespace Unosquare.PassCore.Common;
+
+///
+/// Interface for any Application provider.
+///
+public interface IAppSettings
{
///
- /// Interface for any Application provider.
+ /// Gets or sets the default domain.
///
- public interface IAppSettings
- {
- ///
- /// Gets or sets the default domain.
- ///
- ///
- /// The default domain.
- ///
- string DefaultDomain { get; set; }
+ ///
+ /// The default domain.
+ ///
+ string DefaultDomain { get; set; }
- ///
- /// Gets or sets the LDAP port.
- ///
- ///
- /// 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.
- ///
- ///
- /// The LDAP port.
- ///
- int LdapPort { get; set; }
+ ///
+ /// Gets or sets the LDAP port.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// The LDAP port.
+ ///
+ int LdapPort { get; set; }
- ///
- /// Gets or sets the LDAP hostnames.
- ///
- ///
- /// 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.
- ///
- ///
- /// The LDAP hostnames.
- ///
- string[] LdapHostnames { get; set; }
+ ///
+ /// Gets or sets the LDAP hostnames.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// The LDAP hostnames.
+ ///
+ string[] LdapHostnames { get; set; }
- ///
- /// Gets or sets the LDAP password.
- ///
- ///
- /// The LDAP password.
- ///
- string LdapPassword { get; set; }
+ ///
+ /// Gets or sets the LDAP password.
+ ///
+ ///
+ /// The LDAP password.
+ ///
+ string LdapPassword { get; set; }
- ///
- /// Gets or sets the LDAP username.
- ///
- ///
- /// The LDAP username.
- ///
- string LdapUsername { get; set; }
- }
-}
+ ///
+ /// Gets or sets the LDAP username.
+ ///
+ ///
+ /// The LDAP username.
+ ///
+ string LdapUsername { get; set; }
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Common/IPasswordChangeProvider.cs b/src/Unosquare.PassCore.Common/IPasswordChangeProvider.cs
index 4aeb43b6..a8185782 100644
--- a/src/Unosquare.PassCore.Common/IPasswordChangeProvider.cs
+++ b/src/Unosquare.PassCore.Common/IPasswordChangeProvider.cs
@@ -1,65 +1,63 @@
-
-namespace Unosquare.PassCore.Common
+using System;
+
+namespace Unosquare.PassCore.Common;
+
+///
+/// Represents a interface for a password change provider.
+///
+public interface IPasswordChangeProvider
{
- using System;
+ ///
+ /// Performs the password change using the credentials provided.
+ ///
+ /// The username.
+ /// The current password.
+ /// The new password.
+ /// The API error item or null if the change password operation was successful.
+ ApiErrorItem? PerformPasswordChange(string username, string currentPassword, string newPassword);
///
- /// Represents a interface for a password change provider.
+ /// Compute the distance between two strings.
+ /// Take it from https://www.csharpstar.com/csharp-string-distance-algorithm/.
///
- public interface IPasswordChangeProvider
+ /// The current password.
+ /// The new password.
+ ///
+ /// The distance between strings.
+ ///
+ int MeasureNewPasswordDistance(string currentPassword, string newPassword)
{
- ///
- /// Performs the password change using the credentials provided.
- ///
- /// The username.
- /// The current password.
- /// The new password.
- /// The API error item or null if the change password operation was successful.
- ApiErrorItem? PerformPasswordChange(string username, string currentPassword, string newPassword);
+ var n = currentPassword.Length;
+ var m = newPassword.Length;
+ var d = new int[n + 1, m + 1];
- ///
- /// Compute the distance between two strings.
- /// Take it from https://www.csharpstar.com/csharp-string-distance-algorithm/.
- ///
- /// The current password.
- /// The new password.
- ///
- /// The distance between strings.
- ///
- int MeasureNewPasswordDistance(string currentPassword, string newPassword)
- {
- var n = currentPassword.Length;
- var m = newPassword.Length;
- var d = new int[n + 1, m + 1];
+ // Step 1
+ if (n == 0)
+ return m;
- // Step 1
- if (n == 0)
- return m;
+ if (m == 0)
+ return n;
- if (m == 0)
- return n;
+ // Step 2
+ for (int i = 0; i <= n; d[i, 0] = i++) { }
- // Step 2
- for (int i = 0; i <= n; d[i, 0] = i++) { }
+ for (int j = 0; j <= m; d[0, j] = j++) { }
- for (int j = 0; j <= m; d[0, j] = j++) { }
-
- // Step 3
- for (int i = 1; i <= n; i++)
+ // Step 3
+ for (int i = 1; i <= n; i++)
+ {
+ //Step 4
+ for (int j = 1; j <= m; j++)
{
- //Step 4
- for (int j = 1; j <= m; j++)
- {
- // Step 5
- int cost = (newPassword[j - 1] == currentPassword[i - 1]) ? 0 : 1;
- // Step 6
- d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
- }
+ // Step 5
+ int cost = (newPassword[j - 1] == currentPassword[i - 1]) ? 0 : 1;
+ // Step 6
+ d[i, j] = Math.Min(Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost);
}
+ }
- // Step 7
- return d[n, m];
+ // Step 7
+ return d[n, m];
- }
}
}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Common/Unosquare.PassCore.Common.csproj b/src/Unosquare.PassCore.Common/Unosquare.PassCore.Common.csproj
index 821e1afc..e5d5edad 100644
--- a/src/Unosquare.PassCore.Common/Unosquare.PassCore.Common.csproj
+++ b/src/Unosquare.PassCore.Common/Unosquare.PassCore.Common.csproj
@@ -1,16 +1,17 @@
-
-
- PassCore abstraction classes to implement custom providers.
- Copyright (c) 2018-2021 - Unosquare
- net5.0
- enable
- true
- 1.0.0
- ..\..\StyleCop.Analyzers.ruleset
- true
- Unosquare
- https://github.com/unosquare/passcore
- https://raw.githubusercontent.com/unosquare/passcore/master/LICENSE
-
+
+ PassCore abstraction classes to implement custom providers.
+ Copyright (c) 2018-2022 - Unosquare
+ net6.0
+ enable
+ true
+ true
+ true
+ AllEnabledByDefault
+ 1.0.0
+ true
+ Unosquare
+ https://github.com/unosquare/passcore
+ https://raw.githubusercontent.com/unosquare/passcore/master/LICENSE
+
diff --git a/src/Unosquare.PassCore.PasswordProvider/NativeMethods.cs b/src/Unosquare.PassCore.PasswordProvider/NativeMethods.cs
index b2465b0d..710becc4 100644
--- a/src/Unosquare.PassCore.PasswordProvider/NativeMethods.cs
+++ b/src/Unosquare.PassCore.PasswordProvider/NativeMethods.cs
@@ -1,53 +1,52 @@
-namespace Unosquare.PassCore.PasswordProvider
+using System;
+
+namespace Unosquare.PassCore.PasswordProvider;
+
+///
+/// This code is taken from the answer https://stackoverflow.com/a/1766203
+/// from https://stackoverflow.com/questions/1394025/active-directory-ldap-check-account-locked-out-password-expired.
+///
+public class NativeMethods
{
- using System;
+ // See http://support.microsoft.com/kb/155012
+ internal const int ErrorPasswordMustChange = 1907;
+
+ // It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
+ internal const int ErrorPasswordExpired = 1330;
- ///
- /// This code is taken from the answer https://stackoverflow.com/a/1766203
- /// from https://stackoverflow.com/questions/1394025/active-directory-ldap-check-account-locked-out-password-expired.
- ///
- public partial class NativeMethods
+ // here are enums
+ internal enum LogonTypes : uint
{
- // See http://support.microsoft.com/kb/155012
- internal const int ErrorPasswordMustChange = 1907;
-
- // It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
- internal const int ErrorPasswordExpired = 1330;
-
- // here are enums
- internal enum LogonTypes : uint
- {
- ///
- /// The interactive
- ///
- Interactive = 2,
-
- ///
- /// The network
- ///
- Network = 3,
-
- ///
- /// The service
- ///
- Service = 5,
- }
-
- internal enum LogonProviders : uint
- {
- ///
- /// The default for platform (use this!)
- ///
- Default = 0,
- }
-
- [System.Runtime.InteropServices.DllImport("advapi32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
- internal static extern bool LogonUser(
- string principal,
- string authority,
- string password,
- LogonTypes logonType,
- LogonProviders logonProvider,
- out IntPtr token);
+ ///
+ /// The interactive
+ ///
+ Interactive = 2,
+
+ ///
+ /// The network
+ ///
+ Network = 3,
+
+ ///
+ /// The service
+ ///
+ Service = 5,
}
-}
+
+ internal enum LogonProviders : uint
+ {
+ ///
+ /// The default for platform (use this!)
+ ///
+ Default = 0,
+ }
+
+ [System.Runtime.InteropServices.DllImport("advapi32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
+ internal static extern bool LogonUser(
+ string principal,
+ string authority,
+ string password,
+ LogonTypes logonType,
+ LogonProviders logonProvider,
+ out IntPtr token);
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.PasswordProvider/PasswordChangeOptions.cs b/src/Unosquare.PassCore.PasswordProvider/PasswordChangeOptions.cs
index 404a43c3..f9a9ec9b 100644
--- a/src/Unosquare.PassCore.PasswordProvider/PasswordChangeOptions.cs
+++ b/src/Unosquare.PassCore.PasswordProvider/PasswordChangeOptions.cs
@@ -1,88 +1,87 @@
-namespace Unosquare.PassCore.PasswordProvider
+using System.Collections.Generic;
+using Unosquare.PassCore.Common;
+
+namespace Unosquare.PassCore.PasswordProvider;
+
+///
+/// Represents the options of this provider.
+///
+///
+public class PasswordChangeOptions : IAppSettings
{
- using Common;
- using System.Collections.Generic;
+ private string? _defaultDomain;
+ private string? _ldapPassword;
+ private string[]? _ldapHostnames;
+ private string? _ldapUsername;
///
- /// Represents the options of this provider.
+ /// Gets or sets a value indicating whether [use automatic context].
///
- ///
- public class PasswordChangeOptions : IAppSettings
- {
- private string? defaultDomain;
- private string? ldapPassword;
- private string[]? ldapHostnames;
- private string? ldapUsername;
+ ///
+ /// true if [use automatic context]; otherwise, false.
+ ///
+ public bool UseAutomaticContext { get; set; } = true;
- ///
- /// Gets or sets a value indicating whether [use automatic context].
- ///
- ///
- /// true if [use automatic context]; otherwise, false.
- ///
- public bool UseAutomaticContext { get; set; } = true;
-
- ///
- /// Gets or sets the restricted ad groups.
- ///
- ///
- /// The restricted ad groups.
- ///
- public List? RestrictedADGroups { get; set; }
+ ///
+ /// Gets or sets the restricted ad groups.
+ ///
+ ///
+ /// The restricted ad groups.
+ ///
+ public List? RestrictedAdGroups { get; set; }
- ///
- /// Gets or sets the allowed ad groups.
- ///
- ///
- /// The allowed ad groups.
- ///
- public List? AllowedADGroups { get; set; }
+ ///
+ /// Gets or sets the allowed ad groups.
+ ///
+ ///
+ /// The allowed ad groups.
+ ///
+ public List? AllowedAdGroups { get; set; }
- ///
- /// Gets or sets the identifier type for user.
- ///
- ///
- /// The identifier type for user.
- ///
- public string? IdTypeForUser { get; set; }
+ ///
+ /// Gets or sets the identifier type for user.
+ ///
+ ///
+ /// The identifier type for user.
+ ///
+ public string? IdTypeForUser { get; set; }
- ///
- /// Gets or sets a value indicating whether [update last password].
- ///
- ///
- /// true if [update last password]; otherwise, false.
- ///
- public bool UpdateLastPassword { get; set; }
+ ///
+ /// Gets or sets a value indicating whether [update last password].
+ ///
+ ///
+ /// true if [update last password]; otherwise, false.
+ ///
+ public bool UpdateLastPassword { get; set; }
- ///
- public string DefaultDomain
- {
- get => defaultDomain ?? string.Empty;
- set => defaultDomain = value;
- }
+ ///
+ public string DefaultDomain
+ {
+ get => _defaultDomain ?? string.Empty;
+ set => _defaultDomain = value;
+ }
- ///
- public int LdapPort { get; set; }
+ ///
+ public int LdapPort { get; set; }
- ///
- public string[] LdapHostnames
- {
- get => ldapHostnames ?? new string[] { };
- set => ldapHostnames = value;
- }
+ ///
+ public string[] LdapHostnames
+ {
+ get => _ldapHostnames ?? new string[] { };
+ set => _ldapHostnames = value;
+ }
- ///
- public string LdapPassword
- {
- get => ldapPassword ?? string.Empty;
- set => ldapPassword = value;
- }
+ ///
+ public string LdapPassword
+ {
+ get => _ldapPassword ?? string.Empty;
+ set => _ldapPassword = value;
+ }
- ///
- public string LdapUsername
- {
- get => ldapUsername ?? string.Empty;
- set => ldapUsername = value;
- }
+ ///
+ public string LdapUsername
+ {
+ get => _ldapUsername ?? string.Empty;
+ set => _ldapUsername = value;
}
-}
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.PasswordProvider/PasswordChangeProvider.cs b/src/Unosquare.PassCore.PasswordProvider/PasswordChangeProvider.cs
index cf49394e..bf907e44 100644
--- a/src/Unosquare.PassCore.PasswordProvider/PasswordChangeProvider.cs
+++ b/src/Unosquare.PassCore.PasswordProvider/PasswordChangeProvider.cs
@@ -1,309 +1,309 @@
-namespace Unosquare.PassCore.PasswordProvider
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.DirectoryServices;
+using System.DirectoryServices.AccountManagement;
+using System.DirectoryServices.ActiveDirectory;
+using System.Linq;
+using Unosquare.PassCore.Common;
+
+namespace Unosquare.PassCore.PasswordProvider;
+
+///
+///
+/// Default Change Password Provider using 'System.DirectoryServices' from Microsoft.
+///
+///
+public class PasswordChangeProvider : IPasswordChangeProvider
{
- using Common;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- using System;
- using System.DirectoryServices;
- using System.DirectoryServices.AccountManagement;
- using System.DirectoryServices.ActiveDirectory;
- using System.Linq;
+ private readonly PasswordChangeOptions _options;
+ private readonly ILogger _logger;
+ private IdentityType _idType = IdentityType.UserPrincipalName;
- ///
///
- /// Default Change Password Provider using 'System.DirectoryServices' from Microsoft.
+ /// Initializes a new instance of the class.
///
- ///
- public partial class PasswordChangeProvider : IPasswordChangeProvider
+ /// The logger.
+ /// The options.
+ public PasswordChangeProvider(
+ ILogger logger,
+ IOptions options)
{
- private readonly PasswordChangeOptions _options;
- private readonly ILogger _logger;
- private IdentityType _idType = IdentityType.UserPrincipalName;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The options.
- public PasswordChangeProvider(
- ILogger logger,
- IOptions options)
- {
- _logger = logger;
- _options = options.Value;
- SetIdType();
- }
+ _logger = logger;
+ _options = options.Value;
+ SetIdType();
+ }
- ///
- public ApiErrorItem? PerformPasswordChange(string username, string currentPassword, string newPassword)
+ ///
+ public ApiErrorItem? PerformPasswordChange(string username, string currentPassword, string newPassword)
+ {
+ try
{
- try
- {
- var fixedUsername = FixUsernameWithDomain(username);
- using var principalContext = AcquirePrincipalContext();
- var userPrincipal = UserPrincipal.FindByIdentity(principalContext, _idType, fixedUsername);
-
- // Check if the user principal exists
- if (userPrincipal == null)
- {
- _logger.LogWarning($"The User principal ({fixedUsername}) doesn't exist");
-
- return new ApiErrorItem(ApiErrorCode.UserNotFound);
- }
+ var fixedUsername = FixUsernameWithDomain(username);
+ using var principalContext = AcquirePrincipalContext();
+ var userPrincipal = UserPrincipal.FindByIdentity(principalContext, _idType, fixedUsername);
- var minPwdLength = AcquireDomainPasswordLength();
+ // Check if the user principal exists
+ if (userPrincipal == null)
+ {
+ _logger.LogWarning($"The User principal ({fixedUsername}) doesn't exist");
- if (newPassword.Length < minPwdLength)
- {
- _logger.LogError("Failed due to password complex policies: New password length is shorter than AD minimum password length");
+ return new ApiErrorItem(ApiErrorCode.UserNotFound);
+ }
- return new ApiErrorItem(ApiErrorCode.ComplexPassword);
- }
+ var minPwdLength = AcquireDomainPasswordLength();
- // Check if the newPassword is Pwned
- if (PwnedPasswordsSearch.PwnedSearch.IsPwnedPassword(newPassword))
- {
- _logger.LogError("Failed due to pwned password: New password is publicly known and can be used in dictionary attacks");
+ if (newPassword.Length < minPwdLength)
+ {
+ _logger.LogError("Failed due to password complex policies: New password length is shorter than AD minimum password length");
- return new ApiErrorItem(ApiErrorCode.PwnedPassword);
- }
+ return new ApiErrorItem(ApiErrorCode.ComplexPassword);
+ }
- _logger.LogInformation($"PerformPasswordChange for user {fixedUsername}");
+ // Check if the newPassword is Pwned
+ if (PwnedPasswordsSearch.PwnedSearch.IsPwnedPassword(newPassword))
+ {
+ _logger.LogError("Failed due to pwned password: New password is publicly known and can be used in dictionary attacks");
- var item = ValidateGroups(userPrincipal);
+ return new ApiErrorItem(ApiErrorCode.PwnedPassword);
+ }
- if (item != null)
- return item;
+ _logger.LogInformation($"PerformPasswordChange for user {fixedUsername}");
- // Check if password change is allowed
- if (userPrincipal.UserCannotChangePassword)
- {
- _logger.LogWarning("The User principal cannot change the password");
+ var item = ValidateGroups(userPrincipal);
- return new ApiErrorItem(ApiErrorCode.ChangeNotPermitted);
- }
+ if (item != null)
+ return item;
- // Check if password expired or must be changed
- if (_options.UpdateLastPassword && userPrincipal.LastPasswordSet == null)
- {
- SetLastPassword(userPrincipal);
- }
+ // Check if password change is allowed
+ if (userPrincipal.UserCannotChangePassword)
+ {
+ _logger.LogWarning("The User principal cannot change the password");
- // Use always UPN for password check.
- if (!ValidateUserCredentials(userPrincipal.UserPrincipalName, currentPassword, principalContext))
- {
- _logger.LogWarning("The User principal password is not valid");
+ return new ApiErrorItem(ApiErrorCode.ChangeNotPermitted);
+ }
- return new ApiErrorItem(ApiErrorCode.InvalidCredentials);
- }
+ // Check if password expired or must be changed
+ if (_options.UpdateLastPassword && userPrincipal.LastPasswordSet == null)
+ {
+ SetLastPassword(userPrincipal);
+ }
- // Change the password via 2 different methods. Try SetPassword if ChangePassword fails.
- ChangePassword(currentPassword, newPassword, userPrincipal);
+ // Use always UPN for password check.
+ if (!ValidateUserCredentials(userPrincipal.UserPrincipalName, currentPassword, principalContext))
+ {
+ _logger.LogWarning("The User principal password is not valid");
- userPrincipal.Save();
- _logger.LogDebug("The User principal password updated with setPassword");
+ return new ApiErrorItem(ApiErrorCode.InvalidCredentials);
}
- catch (PasswordException passwordEx)
- {
- var item = new ApiErrorItem(ApiErrorCode.ComplexPassword, passwordEx.Message);
- _logger.LogWarning(item.Message, passwordEx);
+ // Change the password via 2 different methods. Try SetPassword if ChangePassword fails.
+ ChangePassword(currentPassword, newPassword, userPrincipal);
- return item;
- }
- catch (Exception ex)
- {
- var item = ex is ApiErrorException apiError
- ? apiError.ToApiErrorItem()
- : new ApiErrorItem(ApiErrorCode.Generic, ex.InnerException?.Message ?? ex.Message);
+ userPrincipal.Save();
+ _logger.LogDebug("The User principal password updated with setPassword");
+ }
+ catch (PasswordException passwordEx)
+ {
+ var item = new ApiErrorItem(ApiErrorCode.ComplexPassword, passwordEx.Message);
- _logger.LogWarning(item.Message, ex);
+ _logger.LogWarning(item.Message, passwordEx);
- return item;
- }
+ return item;
+ }
+ catch (Exception ex)
+ {
+ var item = ex is ApiErrorException apiError
+ ? apiError.ToApiErrorItem()
+ : new ApiErrorItem(ApiErrorCode.Generic, ex.InnerException?.Message ?? ex.Message);
+
+ _logger.LogWarning(item.Message, ex);
- return null;
+ return item;
}
- private bool ValidateUserCredentials(
- string upn,
- string currentPassword,
- PrincipalContext principalContext)
- {
- if (principalContext.ValidateCredentials(upn, currentPassword))
- return true;
+ return null;
+ }
- if (NativeMethods.LogonUser(upn, string.Empty, currentPassword, NativeMethods.LogonTypes.Network, NativeMethods.LogonProviders.Default, out _))
- return true;
+ private bool ValidateUserCredentials(
+ string upn,
+ string currentPassword,
+ PrincipalContext principalContext)
+ {
+ if (principalContext.ValidateCredentials(upn, currentPassword))
+ return true;
- var errorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
+ if (NativeMethods.LogonUser(upn, string.Empty, currentPassword, NativeMethods.LogonTypes.Network, NativeMethods.LogonProviders.Default, out _))
+ return true;
- _logger.LogDebug($"ValidateUserCredentials GetLastWin32Error {errorCode}");
+ var errorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
- // Both of these means that the password CAN change and that we got the correct password
- return errorCode == NativeMethods.ErrorPasswordMustChange || errorCode == NativeMethods.ErrorPasswordExpired;
- }
+ _logger.LogDebug($"ValidateUserCredentials GetLastWin32Error {errorCode}");
- private string FixUsernameWithDomain(string username)
- {
- if (_idType != IdentityType.UserPrincipalName) return username;
+ // Both of these means that the password CAN change and that we got the correct password
+ return errorCode is NativeMethods.ErrorPasswordMustChange or NativeMethods.ErrorPasswordExpired;
+ }
+
+ private string FixUsernameWithDomain(string username)
+ {
+ if (_idType != IdentityType.UserPrincipalName) return username;
- // Check for default domain: if none given, ensure EFLD can be used as an override.
- var parts = username.Split(new[] { '@' }, StringSplitOptions.RemoveEmptyEntries);
- var domain = parts.Length > 1 ? parts[1] : _options.DefaultDomain;
+ // Check for default domain: if none given, ensure EFLD can be used as an override.
+ var parts = username.Split(new[] { '@' }, StringSplitOptions.RemoveEmptyEntries);
+ var domain = parts.Length > 1 ? parts[1] : _options.DefaultDomain;
- return string.IsNullOrWhiteSpace(domain) || parts.Length > 1 ? username : $"{username}@{domain}";
- }
+ return string.IsNullOrWhiteSpace(domain) || parts.Length > 1 ? username : $"{username}@{domain}";
+ }
- private ApiErrorItem? ValidateGroups(UserPrincipal userPrincipal)
+ private ApiErrorItem? ValidateGroups(UserPrincipal userPrincipal)
+ {
+ try
{
+ PrincipalSearchResult groups;
+
try
{
- PrincipalSearchResult groups;
-
- try
- {
- groups = userPrincipal.GetGroups();
- }
- catch (Exception exception)
- {
- _logger.LogError(new EventId(887), exception, nameof(ValidateGroups));
-
- groups = userPrincipal.GetAuthorizationGroups();
- }
-
- if (_options.RestrictedADGroups != null)
- if (groups.Any(x => _options.RestrictedADGroups.Contains(x.Name)))
- {
- return new ApiErrorItem(ApiErrorCode.ChangeNotPermitted,
- "The User principal is listed as restricted");
- }
-
- return groups?.Any(x => _options.AllowedADGroups?.Contains(x.Name) != false) == true
- ? null
- : new ApiErrorItem(ApiErrorCode.ChangeNotPermitted, "The User principal is not listed as allowed");
+ groups = userPrincipal.GetGroups();
}
catch (Exception exception)
{
- _logger.LogError(new EventId(888), exception, nameof(ValidateGroups));
- }
+ _logger.LogError(new EventId(887), exception, nameof(ValidateGroups));
- return null;
- }
-
- private void SetLastPassword(Principal userPrincipal)
- {
- var directoryEntry = (DirectoryEntry)userPrincipal.GetUnderlyingObject();
- var prop = directoryEntry.Properties["pwdLastSet"];
-
- if (prop == null)
- {
- _logger.LogWarning("The User principal password have no last password, but the property is missing");
- return;
+ groups = userPrincipal.GetAuthorizationGroups();
}
- try
+ if (_options.RestrictedAdGroups == null)
{
- prop.Value = -1;
- directoryEntry.CommitChanges();
- _logger.LogWarning("The User principal last password was updated");
+ return groups.Any(x => _options.AllowedAdGroups?.Contains(x.Name) == true)
+ ? null
+ : new ApiErrorItem(ApiErrorCode.ChangeNotPermitted, "The User principal is not listed as allowed");
}
- catch (Exception ex)
+
+ if (groups.Any(x => _options.RestrictedAdGroups.Contains(x.Name)))
{
- throw new ApiErrorException($"Failed to update password: {ex.Message}",
- ApiErrorCode.ChangeNotPermitted);
+ return new ApiErrorItem(ApiErrorCode.ChangeNotPermitted,
+ "The User principal is listed as restricted");
}
- }
- private void ChangePassword(
- string currentPassword,
- string newPassword,
- AuthenticablePrincipal userPrincipal)
+ return groups.Any(x => _options.AllowedAdGroups?.Contains(x.Name) == true)
+ ? null
+ : new ApiErrorItem(ApiErrorCode.ChangeNotPermitted, "The User principal is not listed as allowed");
+ }
+ catch (Exception exception)
{
- try
- {
- // Try by regular ChangePassword method
- userPrincipal.ChangePassword(currentPassword, newPassword);
- }
- catch
- {
- if (_options.UseAutomaticContext)
- {
- _logger.LogWarning("The User principal password cannot be changed and setPassword won't be called");
+ _logger.LogError(new EventId(888), exception, nameof(ValidateGroups));
+ }
- throw;
- }
+ return null;
+ }
- // If the previous attempt failed, use the SetPassword method.
- userPrincipal.SetPassword(newPassword);
+ private void SetLastPassword(Principal userPrincipal)
+ {
+ var directoryEntry = (DirectoryEntry)userPrincipal.GetUnderlyingObject();
+ var prop = directoryEntry.Properties["pwdLastSet"];
- _logger.LogDebug("The User principal password updated with setPassword");
- }
+ if (prop == null)
+ {
+ _logger.LogWarning("The User principal password have no last password, but the property is missing");
+ return;
}
- ///
- /// Use the values from appsettings.IdTypeForUser as fault-tolerant as possible.
- ///
- private void SetIdType()
+ try
{
- _idType = _options.IdTypeForUser?.Trim().ToLower() switch
- {
- "distinguishedname" => IdentityType.DistinguishedName,
- "distinguished name" => IdentityType.DistinguishedName,
- "dn" => IdentityType.DistinguishedName,
- "globally unique identifier" => IdentityType.Guid,
- "globallyuniqueidentifier" => IdentityType.Guid,
- "guid" => IdentityType.Guid,
- "name" => IdentityType.Name,
- "nm" => IdentityType.Name,
- "samaccountname" => IdentityType.SamAccountName,
- "accountname" => IdentityType.SamAccountName,
- "sam account" => IdentityType.SamAccountName,
- "sam account name" => IdentityType.SamAccountName,
- "sam" => IdentityType.SamAccountName,
- "securityidentifier" => IdentityType.Sid,
- "securityid" => IdentityType.Sid,
- "secid" => IdentityType.Sid,
- "security identifier" => IdentityType.Sid,
- "sid" => IdentityType.Sid,
- _ => IdentityType.UserPrincipalName
- };
+ prop.Value = -1;
+ directoryEntry.CommitChanges();
+ _logger.LogWarning("The User principal last password was updated");
+ }
+ catch (Exception ex)
+ {
+ throw new ApiErrorException($"Failed to update password: {ex.Message}",
+ ApiErrorCode.ChangeNotPermitted);
}
+ }
- private PrincipalContext AcquirePrincipalContext()
+ private void ChangePassword(
+ string currentPassword,
+ string newPassword,
+ AuthenticablePrincipal userPrincipal)
+ {
+ try
+ {
+ // Try by regular ChangePassword method
+ userPrincipal.ChangePassword(currentPassword, newPassword);
+ }
+ catch
{
if (_options.UseAutomaticContext)
{
- _logger.LogWarning("Using AutomaticContext");
- return new PrincipalContext(ContextType.Domain);
+ _logger.LogWarning("The User principal password cannot be changed and setPassword won't be called");
+
+ throw;
}
- var domain = $"{_options.LdapHostnames.First()}:{_options.LdapPort}";
- _logger.LogWarning($"Not using AutomaticContext {domain}");
+ // If the previous attempt failed, use the SetPassword method.
+ userPrincipal.SetPassword(newPassword);
- return new PrincipalContext(
- ContextType.Domain,
- domain,
- _options.LdapUsername,
- _options.LdapPassword);
+ _logger.LogDebug("The User principal password updated with setPassword");
}
+ }
- private int AcquireDomainPasswordLength()
+ ///
+ /// Use the values from appsettings.IdTypeForUser as fault-tolerant as possible.
+ ///
+ private void SetIdType()
+ {
+ _idType = _options.IdTypeForUser?.Trim().ToLower() switch
{
- DirectoryEntry entry;
- if (_options.UseAutomaticContext)
- {
- entry = Domain.GetCurrentDomain().GetDirectoryEntry();
- }
- else
- {
- entry = new DirectoryEntry(
- $"{_options.LdapHostnames.First()}:{_options.LdapPort}",
- _options.LdapUsername,
- _options.LdapPassword
- );
- }
- return (int)entry.Properties["minPwdLength"].Value;
+ "distinguishedname" => IdentityType.DistinguishedName,
+ "distinguished name" => IdentityType.DistinguishedName,
+ "dn" => IdentityType.DistinguishedName,
+ "globally unique identifier" => IdentityType.Guid,
+ "globallyuniqueidentifier" => IdentityType.Guid,
+ "guid" => IdentityType.Guid,
+ "name" => IdentityType.Name,
+ "nm" => IdentityType.Name,
+ "samaccountname" => IdentityType.SamAccountName,
+ "accountname" => IdentityType.SamAccountName,
+ "sam account" => IdentityType.SamAccountName,
+ "sam account name" => IdentityType.SamAccountName,
+ "sam" => IdentityType.SamAccountName,
+ "securityidentifier" => IdentityType.Sid,
+ "securityid" => IdentityType.Sid,
+ "secid" => IdentityType.Sid,
+ "security identifier" => IdentityType.Sid,
+ "sid" => IdentityType.Sid,
+ _ => IdentityType.UserPrincipalName
+ };
+ }
+
+ private PrincipalContext AcquirePrincipalContext()
+ {
+ if (_options.UseAutomaticContext)
+ {
+ _logger.LogWarning("Using AutomaticContext");
+ return new PrincipalContext(ContextType.Domain);
}
+
+ var domain = $"{_options.LdapHostnames.First()}:{_options.LdapPort}";
+ _logger.LogWarning($"Not using AutomaticContext {domain}");
+
+ return new PrincipalContext(
+ ContextType.Domain,
+ domain,
+ _options.LdapUsername,
+ _options.LdapPassword);
+ }
+
+ private int AcquireDomainPasswordLength()
+ {
+ DirectoryEntry entry = _options.UseAutomaticContext
+ ? Domain.GetCurrentDomain().GetDirectoryEntry()
+ : new DirectoryEntry(
+ $"{_options.LdapHostnames.First()}:{_options.LdapPort}",
+ _options.LdapUsername,
+ _options.LdapPassword
+ );
+
+ return (int)entry.Properties["minPwdLength"].Value;
}
}
diff --git a/src/Unosquare.PassCore.PasswordProvider/Unosquare.PassCore.PasswordProvider.csproj b/src/Unosquare.PassCore.PasswordProvider/Unosquare.PassCore.PasswordProvider.csproj
index c04fea13..efabba16 100644
--- a/src/Unosquare.PassCore.PasswordProvider/Unosquare.PassCore.PasswordProvider.csproj
+++ b/src/Unosquare.PassCore.PasswordProvider/Unosquare.PassCore.PasswordProvider.csproj
@@ -1,28 +1,27 @@
-
- Copyright (c) 2018-2021 - Unosquare
- true
- net5.0
- enable
- true
- ..\..\StyleCop.Analyzers.ruleset
-
+
+ Copyright (c) 2018-2022 - Unosquare
+ true
+ net6.0
+ enable
+ true
+ true
+ AllEnabledByDefault
+ true
+
-
-
-
-
-
-
- All
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/src/Unosquare.PassCore.Web/ClientApp/Components/Footer.tsx b/src/Unosquare.PassCore.Web/ClientApp/Components/Footer.tsx
index 69cceb01..1077e188 100644
--- a/src/Unosquare.PassCore.Web/ClientApp/Components/Footer.tsx
+++ b/src/Unosquare.PassCore.Web/ClientApp/Components/Footer.tsx
@@ -25,10 +25,10 @@ export const Footer: React.FunctionComponent = () => (
- Powered by PassCore v4.3.0 - Open Source Initiative and MIT Licensed
+ Powered by PassCore v4.5.0 - Open Source Initiative and MIT Licensed
- Copyright © 2016-2020 Unosquare
+ Copyright © 2016-2022 Unosquare
diff --git a/src/Unosquare.PassCore.Web/ClientApp/package-lock.json b/src/Unosquare.PassCore.Web/ClientApp/package-lock.json
index fcc495e6..2883a65f 100644
--- a/src/Unosquare.PassCore.Web/ClientApp/package-lock.json
+++ b/src/Unosquare.PassCore.Web/ClientApp/package-lock.json
@@ -6278,9 +6278,9 @@
}
},
"minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
+ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
},
"mixin-deep": {
@@ -7900,9 +7900,9 @@
"dev": true
},
"react": {
- "version": "16.13.0",
- "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz",
- "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==",
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
+ "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -7910,14 +7910,14 @@
}
},
"react-dom": {
- "version": "16.13.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.0.tgz",
- "integrity": "sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg==",
+ "version": "16.14.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
+ "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
- "scheduler": "^0.19.0"
+ "scheduler": "^0.19.1"
}
},
"react-form-validator-core": {
@@ -8317,9 +8317,9 @@
}
},
"scheduler": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.0.tgz",
- "integrity": "sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==",
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
+ "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
diff --git a/src/Unosquare.PassCore.Web/ClientApp/package.json b/src/Unosquare.PassCore.Web/ClientApp/package.json
index d6e6c416..924c676f 100644
--- a/src/Unosquare.PassCore.Web/ClientApp/package.json
+++ b/src/Unosquare.PassCore.Web/ClientApp/package.json
@@ -23,8 +23,8 @@
"dependencies": {
"@material-ui/core": "^4.9.5",
"@material-ui/icons": "^4.9.1",
- "react": "^16.13.0",
- "react-dom": "^16.13.0",
+ "react": "^16.14.0",
+ "react-dom": "^16.14.0",
"uno-material-ui": "^1.7.40",
"uno-react": "^0.14.4",
"zxcvbn": "^4.4.2"
@@ -44,7 +44,7 @@
"eslint-plugin-prettier": "3.2.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-react": "7.21.5",
- "minimist": "^1.2.5",
+ "minimist": "^1.2.6",
"node-forge": "^0.10.0",
"parcel-bundler": "^1.12.5",
"parcel-plugin-clean-easy-unsafe": "^1.0.2",
diff --git a/src/Unosquare.PassCore.Web/Controllers/HomeController.cs b/src/Unosquare.PassCore.Web/Controllers/HomeController.cs
index d015c9f2..d90ded8b 100644
--- a/src/Unosquare.PassCore.Web/Controllers/HomeController.cs
+++ b/src/Unosquare.PassCore.Web/Controllers/HomeController.cs
@@ -1,15 +1,14 @@
-namespace Unosquare.PassCore.Web.Controllers
-{
- using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Unosquare.PassCore.Web.Controllers;
- ///
- /// This controller is simply a placeholder to redirect any non-matching URL
- /// to provide the context of the SPA (single page application) index
- /// Examine the routing configuration in the Startup class.
- ///
- public class HomeController : Controller
- {
- // GET: //
- public IActionResult Index() => File("~/index.html", "text/html");
- }
+///
+/// This controller is simply a placeholder to redirect any non-matching URL
+/// to provide the context of the SPA (single page application) index
+/// Examine the routing configuration in the Startup class.
+///
+public class HomeController : Controller
+{
+ // GET: //
+ public IActionResult Index() => File("~/index.html", "text/html");
}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Controllers/PasswordController.cs b/src/Unosquare.PassCore.Web/Controllers/PasswordController.cs
index 4413e7c4..d5599656 100644
--- a/src/Unosquare.PassCore.Web/Controllers/PasswordController.cs
+++ b/src/Unosquare.PassCore.Web/Controllers/PasswordController.cs
@@ -1,156 +1,143 @@
-namespace Unosquare.PassCore.Web.Controllers
+using Unosquare.PassCore.Web.Helpers;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Unosquare.PassCore.Web.Models;
+using Swan.Net;
+using Zxcvbn;
+
+namespace Unosquare.PassCore.Web.Controllers;
+
+///
+/// Represents a controller class holding all of the server-side functionality of this tool.
+///
+[Route("api/[controller]")]
+public class PasswordController : Controller
{
- using Common;
- using Helpers;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- using Models;
- using Swan.Net;
- using System;
- using System.Collections.Generic;
- using System.Threading.Tasks;
- using Zxcvbn;
+ private readonly ILogger _logger;
+ private readonly ClientSettings _options;
+ private readonly IPasswordChangeProvider _passwordChangeProvider;
///
- /// Represents a controller class holding all of the server-side functionality of this tool.
+ /// Initializes a new instance of the class.
///
- [Route("api/[controller]")]
- public class PasswordController : Controller
+ /// The logger.
+ /// The options accessor.
+ /// The password change provider.
+ public PasswordController(
+ ILogger logger,
+ IOptions optionsAccessor,
+ IPasswordChangeProvider passwordChangeProvider)
{
- private readonly ILogger _logger;
- private readonly ClientSettings _options;
- private readonly IPasswordChangeProvider _passwordChangeProvider;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The options accessor.
- /// The password change provider.
- public PasswordController(
- ILogger logger,
- IOptions optionsAccessor,
- IPasswordChangeProvider passwordChangeProvider)
- {
- _logger = logger;
- _options = optionsAccessor.Value;
- _passwordChangeProvider = passwordChangeProvider;
- }
+ _logger = logger;
+ _options = optionsAccessor.Value;
+ _passwordChangeProvider = passwordChangeProvider;
+ }
+
+ ///
+ /// Returns the ClientSettings object as a JSON string.
+ ///
+ /// A Json representation of the ClientSettings object.
+ [HttpGet]
+ public IActionResult Get() => Json(_options);
+
+ ///
+ /// Returns generated password as a JSON string.
+ ///
+ /// A Json with a password property which contains a random generated password.
+ [HttpGet]
+ [Route("generated")]
+ public IActionResult GetGeneratedPassword()
+ {
+ using var generator = new PasswordGenerator();
+ return Json(new { password = generator.Generate(_options.PasswordEntropy) });
+ }
- ///
- /// Returns the ClientSettings object as a JSON string.
- ///
- /// A Json representation of the ClientSettings object.
- [HttpGet]
- public IActionResult Get() => Json(_options);
-
- ///
- /// Returns generated password as a JSON string.
- ///
- /// A Json with a password property which contains a random generated password.
- [HttpGet]
- [Route("generated")]
- public IActionResult GetGeneratedPassword()
+ ///
+ /// Given a POST request, processes and changes a User's password.
+ ///
+ /// The value.
+ /// A task representing the async operation.
+ [HttpPost]
+ public async Task Post([FromBody] ChangePasswordModel model)
+ {
+ if (model.NewPassword != model.NewPasswordVerify)
{
- using var generator = new PasswordGenerator();
- return Json(new { password = generator.Generate(_options.PasswordEntropy) });
+ _logger.LogWarning("Invalid model, passwords don't match");
+
+ return BadRequest(ApiResult.InvalidRequest());
}
- ///
- /// Given a POST request, processes and changes a User's password.
- ///
- /// The value.
- /// A task representing the async operation.
- [HttpPost]
- public async Task Post([FromBody] ChangePasswordModel model)
+ // Validate the model
+ if (ModelState.IsValid == false)
{
- // Validate the request
- if (model == null)
- {
- _logger.LogWarning("Null model");
+ _logger.LogWarning("Invalid model, validation failed");
- return BadRequest(ApiResult.InvalidRequest());
- }
+ return BadRequest(ApiResult.FromModelStateErrors(ModelState));
+ }
- if (model.NewPassword != model.NewPasswordVerify)
- {
- _logger.LogWarning("Invalid model, passwords don't match");
+ // Validate the Captcha
+ try
+ {
+ if (await ValidateRecaptcha(model.Recaptcha).ConfigureAwait(false) == false)
+ throw new InvalidOperationException("Invalid Recaptcha response");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Invalid Recaptcha");
+ return BadRequest(ApiResult.InvalidCaptcha());
+ }
- return BadRequest(ApiResult.InvalidRequest());
- }
+ var result = new ApiResult();
- // Validate the model
- if (ModelState.IsValid == false)
+ try
+ {
+ if (_options.MinimumDistance > 0 &&
+ _passwordChangeProvider.MeasureNewPasswordDistance(model.CurrentPassword, model.NewPassword) < _options.MinimumDistance)
{
- _logger.LogWarning("Invalid model, validation failed");
-
- return BadRequest(ApiResult.FromModelStateErrors(ModelState));
+ result.Errors.Add(new ApiErrorItem(ApiErrorCode.MinimumDistance));
+ return BadRequest(result);
}
- // Validate the Captcha
- try
- {
- if (await ValidateRecaptcha(model.Recaptcha).ConfigureAwait(false) == false)
- throw new InvalidOperationException("Invalid Recaptcha response");
- }
- catch (Exception ex)
+ if (_options.MinimumScore > 0 && Core.EvaluatePassword(model.NewPassword).Score < _options.MinimumScore)
{
- _logger.LogWarning(ex, "Invalid Recaptcha");
- return BadRequest(ApiResult.InvalidCaptcha());
+ result.Errors.Add(new ApiErrorItem(ApiErrorCode.MinimumScore));
+ return BadRequest(result);
}
- var result = new ApiResult();
+ var resultPasswordChange = _passwordChangeProvider.PerformPasswordChange(
+ model.Username,
+ model.CurrentPassword,
+ model.NewPassword);
- try
- {
- if (_options.MinimumDistance > 0 &&
- _passwordChangeProvider.MeasureNewPasswordDistance(model.CurrentPassword, model.NewPassword) < _options.MinimumDistance)
- {
- result.Errors.Add(new ApiErrorItem(ApiErrorCode.MinimumDistance));
- return BadRequest(result);
- }
-
- if (_options.MinimumScore > 0 && Core.EvaluatePassword(model.NewPassword).Score < _options.MinimumScore)
- {
- result.Errors.Add(new ApiErrorItem(ApiErrorCode.MinimumScore));
- return BadRequest(result);
- }
-
- var resultPasswordChange = _passwordChangeProvider.PerformPasswordChange(
- model.Username,
- model.CurrentPassword,
- model.NewPassword);
-
- if (resultPasswordChange == null)
- return Json(result);
-
- result.Errors.Add(resultPasswordChange);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to update password");
+ if (resultPasswordChange == null)
+ return Json(result);
- result.Errors.Add(new ApiErrorItem(ApiErrorCode.Generic, ex.Message));
- }
+ result.Errors.Add(resultPasswordChange);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to update password");
- return BadRequest(result);
+ result.Errors.Add(new ApiErrorItem(ApiErrorCode.Generic, ex.Message));
}
- private async Task ValidateRecaptcha(string? recaptchaResponse)
- {
- // skip validation if we don't enable recaptcha
- if ((_options.Recaptcha != null) && string.IsNullOrWhiteSpace(_options.Recaptcha.PrivateKey))
- return true;
- else if ((_options.Recaptcha != null) && (string.IsNullOrEmpty(recaptchaResponse) != true))
- {
- var requestUrl = new Uri(
- $"https://www.google.com/recaptcha/api/siteverify?secret={_options.Recaptcha.PrivateKey}&response={recaptchaResponse}");
- var validationResponse = await JsonClient.Get>(requestUrl)
- .ConfigureAwait(false);
- return Convert.ToBoolean(validationResponse["success"], System.Globalization.CultureInfo.InvariantCulture);
- }
+ return BadRequest(result);
+ }
+
+ private async Task ValidateRecaptcha(string? recaptchaResponse)
+ {
+ // skip validation if we don't enable recaptcha
+ if (_options.Recaptcha != null && string.IsNullOrWhiteSpace(_options.Recaptcha.PrivateKey))
+ return true;
+
+ if (_options.Recaptcha == null || string.IsNullOrEmpty(recaptchaResponse) == true)
return false;
- }
+
+ var requestUrl = new Uri(
+ $"https://www.google.com/recaptcha/api/siteverify?secret={_options.Recaptcha.PrivateKey}&response={recaptchaResponse}");
+ var validationResponse = await JsonClient.Get>(requestUrl);
+
+ return Convert.ToBoolean(validationResponse["success"], System.Globalization.CultureInfo.InvariantCulture);
}
}
diff --git a/src/Unosquare.PassCore.Web/GlobalUsings.cs b/src/Unosquare.PassCore.Web/GlobalUsings.cs
new file mode 100644
index 00000000..7ee5c93f
--- /dev/null
+++ b/src/Unosquare.PassCore.Web/GlobalUsings.cs
@@ -0,0 +1,5 @@
+global using System.Collections.Generic;
+global using System.Linq;
+global using System;
+global using System.Threading.Tasks;
+global using Unosquare.PassCore.Common;
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Helpers/DebugAppSettings.cs b/src/Unosquare.PassCore.Web/Helpers/DebugAppSettings.cs
index 27504d65..cbc2a947 100644
--- a/src/Unosquare.PassCore.Web/Helpers/DebugAppSettings.cs
+++ b/src/Unosquare.PassCore.Web/Helpers/DebugAppSettings.cs
@@ -1,34 +1,31 @@
-namespace Unosquare.PassCore.Web.Helpers
+namespace Unosquare.PassCore.Web.Helpers;
+
+public class DebugAppSettings : IAppSettings
{
- using Common;
+ private string? defaultDomain;
+ private string[]? ldapHostnames;
+ private string? ldapPassword;
+ private string? ldapUsername;
- public class DebugAppSettings : IAppSettings
+ public string DefaultDomain
{
- private string? defaultDomain;
- private string[]? ldapHostnames;
- private string? ldapPassword;
- private string? ldapUsername;
-
- public string DefaultDomain
- {
- get => defaultDomain ?? string.Empty;
- set => defaultDomain = value;
- }
- public int LdapPort { get; set; }
- public string[] LdapHostnames
- {
- get => ldapHostnames ?? new string[] { };
- set => ldapHostnames = value;
- }
- public string LdapPassword
- {
- get => ldapPassword ?? string.Empty;
- set => ldapPassword = value;
- }
- public string LdapUsername
- {
- get => ldapUsername ?? string.Empty;
- set => ldapUsername = value;
- }
+ get => defaultDomain ?? string.Empty;
+ set => defaultDomain = value;
+ }
+ public int LdapPort { get; set; }
+ public string[] LdapHostnames
+ {
+ get => ldapHostnames ?? new string[] { };
+ set => ldapHostnames = value;
+ }
+ public string LdapPassword
+ {
+ get => ldapPassword ?? string.Empty;
+ set => ldapPassword = value;
+ }
+ public string LdapUsername
+ {
+ get => ldapUsername ?? string.Empty;
+ set => ldapUsername = value;
}
-}
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Helpers/DebugPasswordChangeProvider.cs b/src/Unosquare.PassCore.Web/Helpers/DebugPasswordChangeProvider.cs
index 1f623455..7c1ecfae 100644
--- a/src/Unosquare.PassCore.Web/Helpers/DebugPasswordChangeProvider.cs
+++ b/src/Unosquare.PassCore.Web/Helpers/DebugPasswordChangeProvider.cs
@@ -1,34 +1,30 @@
-namespace Unosquare.PassCore.Web.Helpers
-{
- using System;
- using Common;
+namespace Unosquare.PassCore.Web.Helpers;
- internal class DebugPasswordChangeProvider : IPasswordChangeProvider
+internal class DebugPasswordChangeProvider : IPasswordChangeProvider
+{
+ public ApiErrorItem? PerformPasswordChange(string username, string currentPassword, string newPassword)
{
- public ApiErrorItem? PerformPasswordChange(string username, string currentPassword, string newPassword)
- {
- var currentUsername = username.IndexOf("@", StringComparison.Ordinal) > 0
- ? username.Substring(0, username.IndexOf("@", StringComparison.Ordinal))
- : username;
+ var currentUsername = username.IndexOf("@", StringComparison.Ordinal) > 0
+ ? username[..username.IndexOf("@", StringComparison.Ordinal)]
+ : username;
- // Even in DEBUG, it is safe to make this call and check the password anyway
- if (PwnedPasswordsSearch.PwnedSearch.IsPwnedPassword(newPassword))
- return new ApiErrorItem(ApiErrorCode.PwnedPassword);
+ // Even in DEBUG, it is safe to make this call and check the password anyway
+ if (PwnedPasswordsSearch.PwnedSearch.IsPwnedPassword(newPassword))
+ return new ApiErrorItem(ApiErrorCode.PwnedPassword);
- return currentUsername switch
- {
- "error" => new ApiErrorItem(ApiErrorCode.Generic, "Error"),
- "changeNotPermitted" => new ApiErrorItem(ApiErrorCode.ChangeNotPermitted),
- "fieldMismatch" => new ApiErrorItem(ApiErrorCode.FieldMismatch),
- "fieldRequired" => new ApiErrorItem(ApiErrorCode.FieldRequired),
- "invalidCaptcha" => new ApiErrorItem(ApiErrorCode.InvalidCaptcha),
- "invalidCredentials" => new ApiErrorItem(ApiErrorCode.InvalidCredentials),
- "invalidDomain" => new ApiErrorItem(ApiErrorCode.InvalidDomain),
- "userNotFound" => new ApiErrorItem(ApiErrorCode.UserNotFound),
- "ldapProblem" => new ApiErrorItem(ApiErrorCode.LdapProblem),
- "pwnedPassword" => new ApiErrorItem(ApiErrorCode.PwnedPassword),
- _ => null
- };
- }
+ return currentUsername switch
+ {
+ "error" => new ApiErrorItem(ApiErrorCode.Generic, "Error"),
+ "changeNotPermitted" => new ApiErrorItem(ApiErrorCode.ChangeNotPermitted),
+ "fieldMismatch" => new ApiErrorItem(ApiErrorCode.FieldMismatch),
+ "fieldRequired" => new ApiErrorItem(ApiErrorCode.FieldRequired),
+ "invalidCaptcha" => new ApiErrorItem(ApiErrorCode.InvalidCaptcha),
+ "invalidCredentials" => new ApiErrorItem(ApiErrorCode.InvalidCredentials),
+ "invalidDomain" => new ApiErrorItem(ApiErrorCode.InvalidDomain),
+ "userNotFound" => new ApiErrorItem(ApiErrorCode.UserNotFound),
+ "ldapProblem" => new ApiErrorItem(ApiErrorCode.LdapProblem),
+ "pwnedPassword" => new ApiErrorItem(ApiErrorCode.PwnedPassword),
+ _ => null
+ };
}
-}
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Helpers/PasswordGenerator.cs b/src/Unosquare.PassCore.Web/Helpers/PasswordGenerator.cs
index c3165687..dfbd6fe5 100644
--- a/src/Unosquare.PassCore.Web/Helpers/PasswordGenerator.cs
+++ b/src/Unosquare.PassCore.Web/Helpers/PasswordGenerator.cs
@@ -1,21 +1,20 @@
-namespace Unosquare.PassCore.Web.Helpers
-{
- using SimpleBase;
- using System.Security.Cryptography;
+using SimpleBase;
+using System.Security.Cryptography;
- internal class PasswordGenerator : System.IDisposable
- {
- private readonly RNGCryptoServiceProvider _rngCsp = new RNGCryptoServiceProvider();
+namespace Unosquare.PassCore.Web.Helpers;
- public string Generate(int entropy)
- {
- var pswBytes = new byte[entropy];
- _rngCsp.GetBytes(pswBytes);
+internal class PasswordGenerator : System.IDisposable
+{
+ private readonly RNGCryptoServiceProvider _rngCsp = new();
- var encoder = new Base85(Base85Alphabet.Ascii85);
- return encoder.Encode(pswBytes);
- }
+ public string Generate(int entropy)
+ {
+ var pswBytes = new byte[entropy];
+ _rngCsp.GetBytes(pswBytes);
- public void Dispose() => _rngCsp.Dispose();
+ var encoder = new Base85(Base85Alphabet.Ascii85);
+ return encoder.Encode(pswBytes);
}
-}
+
+ public void Dispose() => _rngCsp.Dispose();
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Models/ApiResult.cs b/src/Unosquare.PassCore.Web/Models/ApiResult.cs
index 4330b15a..6e9ea1d5 100644
--- a/src/Unosquare.PassCore.Web/Models/ApiResult.cs
+++ b/src/Unosquare.PassCore.Web/Models/ApiResult.cs
@@ -1,124 +1,120 @@
-namespace Unosquare.PassCore.Web.Models
-{
- using System.Collections.Generic;
- using System.Linq;
- using Microsoft.AspNetCore.Mvc.ModelBinding;
- using Common;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Unosquare.PassCore.Web.Models;
+///
+/// Represent a generic response from a REST API call.
+///
+public class ApiResult
+{
///
- /// Represent a generic response from a REST API call.
+ /// Initializes a new instance of the class.
///
- public class ApiResult
+ /// The payload.
+ public ApiResult(object? payload = null)
{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The payload.
- public ApiResult(object? payload = null)
- {
- Errors = new List();
- Payload = payload;
- }
+ Errors = new List();
+ Payload = payload;
+ }
- ///
- /// Gets or sets the errors.
- ///
- public List Errors { get; }
+ ///
+ /// Gets or sets the errors.
+ ///
+ public List Errors { get; }
- ///
- /// Gets or sets the payload.
- ///
- public object? Payload { get; }
+ ///
+ /// Gets or sets the payload.
+ ///
+ public object? Payload { get; }
- ///
- /// Creates a generic invalid request response.
- ///
- /// The ApiResult wih Invalid request error.
- public static ApiResult InvalidRequest()
- {
- var result = new ApiResult("Invalid Request");
- result.Errors.Add(new ApiErrorItem(ApiErrorCode.Generic, "Invalid Request"));
+ ///
+ /// Creates a generic invalid request response.
+ ///
+ /// The ApiResult wih Invalid request error.
+ public static ApiResult InvalidRequest()
+ {
+ var result = new ApiResult("Invalid Request");
+ result.Errors.Add(new ApiErrorItem(ApiErrorCode.Generic, "Invalid Request"));
- return result;
- }
+ return result;
+ }
- ///
- /// Invalids the captcha.
- ///
- /// The ApiResult from Invalid Recaptcha.
- public static ApiResult InvalidCaptcha()
- {
- var result = new ApiResult("Invalid Recaptcha");
- result.Errors.Add(new ApiErrorItem(ApiErrorCode.InvalidCaptcha));
+ ///
+ /// Invalids the captcha.
+ ///
+ /// The ApiResult from Invalid Recaptcha.
+ public static ApiResult InvalidCaptcha()
+ {
+ var result = new ApiResult("Invalid Recaptcha");
+ result.Errors.Add(new ApiErrorItem(ApiErrorCode.InvalidCaptcha));
- return result;
- }
+ return result;
+ }
- ///
- /// Adds the model state errors.
- ///
- /// State of the model.
- /// The ApiResult from Model State.
- public static ApiResult FromModelStateErrors(ModelStateDictionary modelState)
+ ///
+ /// Adds the model state errors.
+ ///
+ /// State of the model.
+ /// The ApiResult from Model State.
+ public static ApiResult FromModelStateErrors(ModelStateDictionary modelState)
+ {
+ var result = new ApiResult();
+
+ foreach (var (key, value) in modelState.Where(x => x.Value.Errors.Any()))
{
- var result = new ApiResult();
+ var error = value.Errors.First();
- foreach (var (key, value) in modelState.Where(x => x.Value.Errors.Any()))
+ switch (error.ErrorMessage)
{
- var error = value.Errors.First();
-
- switch (error.ErrorMessage)
- {
- case nameof(ApiErrorCode.FieldRequired):
- result.AddFieldRequiredValidationError(key);
- break;
- case nameof(ApiErrorCode.FieldMismatch):
- result.AddFieldMismatchValidationError(key);
- break;
- default:
- result.AddGenericFieldValidationError(key, error.ErrorMessage);
- break;
- }
+ case nameof(ApiErrorCode.FieldRequired):
+ result.AddFieldRequiredValidationError(key);
+ break;
+ case nameof(ApiErrorCode.FieldMismatch):
+ result.AddFieldMismatchValidationError(key);
+ break;
+ default:
+ result.AddGenericFieldValidationError(key, error.ErrorMessage);
+ break;
}
-
- return result;
}
- ///
- /// Adds the field required validation error.
- ///
- /// Name of the field.
- private void AddFieldRequiredValidationError(string fieldName)
+ return result;
+ }
+
+ ///
+ /// Adds the field required validation error.
+ ///
+ /// Name of the field.
+ private void AddFieldRequiredValidationError(string fieldName)
+ {
+ Errors.Add(new ApiErrorItem(ApiErrorCode.FieldRequired, nameof(ApiErrorCode.FieldRequired))
{
- Errors.Add(new ApiErrorItem(ApiErrorCode.FieldRequired, nameof(ApiErrorCode.FieldRequired))
- {
- FieldName = fieldName,
- });
- }
+ FieldName = fieldName,
+ });
+ }
- ///
- /// Adds the field mismatch validation error.
- ///
- /// Name of the field.
- private void AddFieldMismatchValidationError(string fieldName)
+ ///
+ /// Adds the field mismatch validation error.
+ ///
+ /// Name of the field.
+ private void AddFieldMismatchValidationError(string fieldName)
+ {
+ Errors.Add(new ApiErrorItem(ApiErrorCode.FieldMismatch, nameof(ApiErrorCode.FieldMismatch))
{
- Errors.Add(new ApiErrorItem(ApiErrorCode.FieldMismatch, nameof(ApiErrorCode.FieldMismatch))
- {
- FieldName = fieldName,
- });
- }
+ FieldName = fieldName,
+ });
+ }
- ///
- /// Adds the generic field validation error.
- ///
- /// Name of the field.
- /// The message.
- private void AddGenericFieldValidationError(string fieldName, string message)
+ ///
+ /// Adds the generic field validation error.
+ ///
+ /// Name of the field.
+ /// The message.
+ private void AddGenericFieldValidationError(string fieldName, string message)
+ {
+ Errors.Add(new ApiErrorItem(ApiErrorCode.Generic, message)
{
- Errors.Add(new ApiErrorItem(ApiErrorCode.Generic, message)
- {
- FieldName = fieldName,
- });
- }
+ FieldName = fieldName,
+ });
}
}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Models/ChangePasswordForm.cs b/src/Unosquare.PassCore.Web/Models/ChangePasswordForm.cs
index 143b94da..b1557f35 100644
--- a/src/Unosquare.PassCore.Web/Models/ChangePasswordForm.cs
+++ b/src/Unosquare.PassCore.Web/Models/ChangePasswordForm.cs
@@ -1,17 +1,16 @@
-namespace Unosquare.PassCore.Web.Models
+namespace Unosquare.PassCore.Web.Models;
+
+public class ChangePasswordForm
{
- public class ChangePasswordForm
- {
- public string? ChangePasswordButtonLabel { get; set; }
- public string? CurrentPasswordHelpblock { get; set; }
- public string? CurrentPasswordLabel { get; set; }
- public string? HelpText { get; set; }
- public string? NewPasswordHelpblock { get; set; }
- public string? NewPasswordLabel { get; set; }
- public string? NewPasswordVerifyHelpblock { get; set; }
- public string? NewPasswordVerifyLabel { get; set; }
- public string? UsernameDefaultDomainHelperBlock { get; set; }
- public string? UsernameHelpblock { get; set; }
- public string? UsernameLabel { get; set; }
- }
+ public string? ChangePasswordButtonLabel { get; set; }
+ public string? CurrentPasswordHelpblock { get; set; }
+ public string? CurrentPasswordLabel { get; set; }
+ public string? HelpText { get; set; }
+ public string? NewPasswordHelpblock { get; set; }
+ public string? NewPasswordLabel { get; set; }
+ public string? NewPasswordVerifyHelpblock { get; set; }
+ public string? NewPasswordVerifyLabel { get; set; }
+ public string? UsernameDefaultDomainHelperBlock { get; set; }
+ public string? UsernameHelpblock { get; set; }
+ public string? UsernameLabel { get; set; }
}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Models/ChangePasswordModel.cs b/src/Unosquare.PassCore.Web/Models/ChangePasswordModel.cs
index 2c894367..c843eb97 100644
--- a/src/Unosquare.PassCore.Web/Models/ChangePasswordModel.cs
+++ b/src/Unosquare.PassCore.Web/Models/ChangePasswordModel.cs
@@ -1,49 +1,47 @@
-namespace Unosquare.PassCore.Web.Models
+using System.ComponentModel.DataAnnotations;
+
+namespace Unosquare.PassCore.Web.Models;
+
+public class ChangePasswordModel
{
- using Common;
- using System.ComponentModel.DataAnnotations;
+ private string? _username;
+ private string? _currentPassword;
+ private string? _newPassword;
+ private string? _newPasswordVerify;
+ private string? _recaptcha;
- public class ChangePasswordModel
+ [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
+ public string Username
{
- private string? username;
- private string? currentPassword;
- private string? newPassword;
- private string? newPasswordVerify;
- private string? recaptcha;
-
- [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
- public string Username
- {
- get => username ?? string.Empty;
- set => username = value;
- }
+ get => _username ?? string.Empty;
+ set => _username = value;
+ }
- [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
- public string CurrentPassword
- {
- get => currentPassword ?? string.Empty;
- set => currentPassword = value;
- }
+ [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
+ public string CurrentPassword
+ {
+ get => _currentPassword ?? string.Empty;
+ set => _currentPassword = value;
+ }
- [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
- public string NewPassword
- {
- get => newPassword ?? string.Empty;
- set => newPassword = value;
- }
+ [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
+ public string NewPassword
+ {
+ get => _newPassword ?? string.Empty;
+ set => _newPassword = value;
+ }
- [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
- [Compare(nameof(NewPassword), ErrorMessage = nameof(ApiErrorCode.FieldMismatch))]
- public string NewPasswordVerify
- {
- get => newPasswordVerify ?? string.Empty;
- set => newPasswordVerify = value;
- }
+ [Required(ErrorMessage = nameof(ApiErrorCode.FieldRequired))]
+ [Compare(nameof(NewPassword), ErrorMessage = nameof(ApiErrorCode.FieldMismatch))]
+ public string NewPasswordVerify
+ {
+ get => _newPasswordVerify ?? string.Empty;
+ set => _newPasswordVerify = value;
+ }
- public string Recaptcha
- {
- get => recaptcha ?? string.Empty;
- set => recaptcha = value;
- }
+ public string Recaptcha
+ {
+ get => _recaptcha ?? string.Empty;
+ set => _recaptcha = value;
}
-}
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Models/ClientSettings.cs b/src/Unosquare.PassCore.Web/Models/ClientSettings.cs
index 86dcd4cc..187bc3cc 100644
--- a/src/Unosquare.PassCore.Web/Models/ClientSettings.cs
+++ b/src/Unosquare.PassCore.Web/Models/ClientSettings.cs
@@ -1,65 +1,64 @@
-namespace Unosquare.PassCore.Web.Models
-{
- using System.Text.Json.Serialization;
+using System.Text.Json.Serialization;
- ///
- /// Represents all of the strongly-typed application settings loaded from a JSON file.
- ///
- public class ClientSettings
- {
- public Alerts? Alerts { get; set; }
- public bool UsePasswordGeneration { get; set; }
- public int MinimumDistance { get; set; }
- public int PasswordEntropy { get; set; }
- public int MinimumScore { get; set; }
- public bool ShowPasswordMeter { get; set; }
- public bool UseEmail { get; set; }
- public ChangePasswordForm? ChangePasswordForm { get; set; }
- public ErrorsPasswordForm? ErrorsPasswordForm { get; set; }
- public Recaptcha? Recaptcha { get; set; }
- public string? ApplicationTitle { get; set; }
- public string? ChangePasswordTitle { get; set; }
- public ValidationRegex? ValidationRegex { get; set; }
- }
+namespace Unosquare.PassCore.Web.Models;
- public class Recaptcha
- {
- public string? LanguageCode { get; set; }
- public string? SiteKey { get; set; }
+///
+/// Represents all of the strongly-typed application settings loaded from a JSON file.
+///
+public class ClientSettings
+{
+ public Alerts? Alerts { get; set; }
+ public bool UsePasswordGeneration { get; set; }
+ public int MinimumDistance { get; set; }
+ public int PasswordEntropy { get; set; }
+ public int MinimumScore { get; set; }
+ public bool ShowPasswordMeter { get; set; }
+ public bool UseEmail { get; set; }
+ public ChangePasswordForm? ChangePasswordForm { get; set; }
+ public ErrorsPasswordForm? ErrorsPasswordForm { get; set; }
+ public Recaptcha? Recaptcha { get; set; }
+ public string? ApplicationTitle { get; set; }
+ public string? ChangePasswordTitle { get; set; }
+ public ValidationRegex? ValidationRegex { get; set; }
+}
- [JsonIgnore]
- public string? PrivateKey { get; set; }
- }
+public class Recaptcha
+{
+ public string? LanguageCode { get; set; }
+ public string? SiteKey { get; set; }
- public class Alerts
- {
- public string? ErrorInvalidCredentials { get; set; }
- public string? ErrorInvalidDomain { get; set; }
- public string? ErrorPasswordChangeNotAllowed { get; set; }
- public string? SuccessAlertBody { get; set; }
- public string? SuccessAlertTitle { get; set; }
- public string? ErrorInvalidUser { get; set; }
- public string? ErrorCaptcha { get; set; }
- public string? ErrorFieldRequired { get; set; }
- public string? ErrorFieldMismatch { get; set; }
- public string? ErrorComplexPassword { get; set; }
- public string? ErrorConnectionLdap { get; set; }
- public string? ErrorScorePassword { get; set; }
- public string? ErrorDistancePassword { get; set; }
- public string? ErrorPwnedPassword { get; set; }
- }
+ [JsonIgnore]
+ public string? PrivateKey { get; set; }
+}
- public class ErrorsPasswordForm
- {
- public string? FieldRequired { get; set; }
- public string? PasswordMatch { get; set; }
- public string? UsernameEmailPattern { get; set; }
- public string? UsernamePattern { get; set; }
- }
+public class Alerts
+{
+ public string? ErrorInvalidCredentials { get; set; }
+ public string? ErrorInvalidDomain { get; set; }
+ public string? ErrorPasswordChangeNotAllowed { get; set; }
+ public string? SuccessAlertBody { get; set; }
+ public string? SuccessAlertTitle { get; set; }
+ public string? ErrorInvalidUser { get; set; }
+ public string? ErrorCaptcha { get; set; }
+ public string? ErrorFieldRequired { get; set; }
+ public string? ErrorFieldMismatch { get; set; }
+ public string? ErrorComplexPassword { get; set; }
+ public string? ErrorConnectionLdap { get; set; }
+ public string? ErrorScorePassword { get; set; }
+ public string? ErrorDistancePassword { get; set; }
+ public string? ErrorPwnedPassword { get; set; }
+}
- public class ValidationRegex
- {
- public string? EmailRegex { get; set; }
- public string? UsernameRegex { get; set; }
- }
+public class ErrorsPasswordForm
+{
+ public string? FieldRequired { get; set; }
+ public string? PasswordMatch { get; set; }
+ public string? UsernameEmailPattern { get; set; }
+ public string? UsernamePattern { get; set; }
+}
+
+public class ValidationRegex
+{
+ public string? EmailRegex { get; set; }
+ public string? UsernameRegex { get; set; }
}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Models/WebSettings.cs b/src/Unosquare.PassCore.Web/Models/WebSettings.cs
index 655abffc..c0ea117d 100644
--- a/src/Unosquare.PassCore.Web/Models/WebSettings.cs
+++ b/src/Unosquare.PassCore.Web/Models/WebSettings.cs
@@ -1,16 +1,15 @@
-namespace Unosquare.PassCore.Web.Models
+namespace Unosquare.PassCore.Web.Models;
+
+///
+/// Represents the Web server settings.
+///
+public class WebSettings
{
///
- /// Represents the Web server settings.
+ /// Gets or sets a value indicating whether [enable HTTPS redirect].
///
- public class WebSettings
- {
- ///
- /// Gets or sets a value indicating whether [enable HTTPS redirect].
- ///
- ///
- /// true if [enable HTTPS redirect]; otherwise, false.
- ///
- public bool EnableHttpsRedirect { get; set; }
- }
-}
+ ///
+ /// true if [enable HTTPS redirect]; otherwise, false.
+ ///
+ public bool EnableHttpsRedirect { get; set; }
+}
\ No newline at end of file
diff --git a/src/Unosquare.PassCore.Web/Startup.cs b/src/Unosquare.PassCore.Web/Startup.cs
index 2c55bf28..c20badcd 100644
--- a/src/Unosquare.PassCore.Web/Startup.cs
+++ b/src/Unosquare.PassCore.Web/Startup.cs
@@ -1,110 +1,107 @@
-namespace Unosquare.PassCore.Web
-{
- using Common;
- using Microsoft.AspNetCore;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Options;
- using Models;
- using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Unosquare.PassCore.Web.Models;
#if DEBUG
- using Helpers;
+using Unosquare.PassCore.Web.Helpers;
#elif PASSCORE_LDAP_PROVIDER
- using Zyborg.PassCore.PasswordProvider.LDAP;
- using Microsoft.Extensions.Logging;
+using Zyborg.PassCore.PasswordProvider.LDAP;
+using Microsoft.Extensions.Logging;
#else
- using PasswordProvider;
+using Unosquare.PassCore.PasswordProvider;
#endif
+namespace Unosquare.PassCore.Web;
+
+///
+/// Represents this application's main class.
+///
+public class Startup
+{
+ private const string AppSettingsSectionName = "AppSettings";
+
///
- /// Represents this application's main class.
+ /// Initializes a new instance of the class.
+ /// This class gets instantiated by the Main method. The hosting environment gets provided via DI.
///
- public class Startup
- {
- private const string AppSettingsSectionName = "AppSettings";
-
- ///
- /// Initializes a new instance of the class.
- /// This class gets instantiated by the Main method. The hosting environment gets provided via DI.
- ///
- /// The configuration.
- public Startup(IConfiguration config) => Configuration = config;
+ /// The configuration.
+ public Startup(IConfiguration config) => Configuration = config;
- ///
- /// Gets or sets the configuration.
- ///
- ///
- /// The configuration.
- ///
- public IConfiguration Configuration { get; }
+ ///
+ /// Gets or sets the configuration.
+ ///
+ ///
+ /// The configuration.
+ ///
+ public IConfiguration Configuration { get; }
- ///
- /// Application's entry point.
- ///
- /// The arguments.
- public static async Task Main(string[] args) => await WebHost.CreateDefaultBuilder(args).UseStartup().Build().RunAsync();
+ ///
+ /// Application's entry point.
+ ///
+ /// The arguments.
+ public static async Task Main(string[] args) => await WebHost.CreateDefaultBuilder(args).UseStartup().Build().RunAsync();
- ///
- /// Creates the web host builder.
- ///
- /// The arguments.
- /// The web host builder.
- public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
- WebHost.CreateDefaultBuilder(args)
+ ///
+ /// Creates the web host builder.
+ ///
+ /// The arguments.
+ /// The web host builder.
+ public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
+ WebHost.CreateDefaultBuilder(args)
.UseStartup();
- ///
- /// This method gets called by the runtime. Use this method to add services to the container.
- /// All arguments are provided through dependency injection.
- ///
- /// The services.
- public void ConfigureServices(IServiceCollection services)
- {
- services.Configure(Configuration.GetSection(nameof(ClientSettings)));
- services.Configure(Configuration.GetSection(nameof(WebSettings)));
+ ///
+ /// This method gets called by the runtime. Use this method to add services to the container.
+ /// All arguments are provided through dependency injection.
+ ///
+ /// The services.
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.Configure(Configuration.GetSection(nameof(ClientSettings)));
+ services.Configure(Configuration.GetSection(nameof(WebSettings)));
#if DEBUG
- services.Configure(Configuration.GetSection(AppSettingsSectionName));
- services.AddSingleton();
+ services.Configure(Configuration.GetSection(AppSettingsSectionName));
+ services.AddSingleton();
#elif PASSCORE_LDAP_PROVIDER
- services.Configure(Configuration.GetSection(AppSettingsSectionName));
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton(typeof(ILogger), sp =>
- {
- var loggerFactory = sp.GetService();
- return loggerFactory.CreateLogger("PassCoreLDAPProvider");
- });
+ services.Configure(Configuration.GetSection(AppSettingsSectionName));
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(typeof(ILogger), sp =>
+ {
+ var loggerFactory = sp.GetService();
+ return loggerFactory.CreateLogger("PassCoreLDAPProvider");
+ });
#else
- services.Configure(Configuration.GetSection(AppSettingsSectionName));
- services.AddSingleton();
+ services.Configure(Configuration.GetSection(AppSettingsSectionName));
+ services.AddSingleton();
#endif
- // Add framework services.
- services.AddControllers();
- }
+ // Add framework services.
+ services.AddControllers();
+ }
- ///
- /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- /// All arguments are provided through dependency injection.
- ///
- /// The application.
- /// The settings.
- public void Configure(IApplicationBuilder app, IOptions settings)
- {
- if (settings.Value.EnableHttpsRedirect)
- app.UseHttpsRedirection();
+ ///
+ /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ /// All arguments are provided through dependency injection.
+ ///
+ /// The application.
+ /// The settings.
+ public void Configure(IApplicationBuilder app, IOptions settings)
+ {
+ if (settings.Value.EnableHttpsRedirect)
+ app.UseHttpsRedirection();
- app.UseDefaultFiles();
- app.UseStaticFiles();
+ app.UseDefaultFiles();
+ app.UseStaticFiles();
- app.UseRouting();
+ app.UseRouting();
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- });
- }
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
}
}
diff --git a/src/Unosquare.PassCore.Web/Unosquare.PassCore.Web.csproj b/src/Unosquare.PassCore.Web/Unosquare.PassCore.Web.csproj
index c1c9ed73..3bce6b1a 100644
--- a/src/Unosquare.PassCore.Web/Unosquare.PassCore.Web.csproj
+++ b/src/Unosquare.PassCore.Web/Unosquare.PassCore.Web.csproj
@@ -1,66 +1,64 @@
-
- $(DefaultItemExcludes);**\node_modules\**;node_modules\**
- Copyright (c) 2018-2021 - Unosquare
- net5.0
- Unosquare.PassCore.Web
- Unosquare.PassCore.Web
- true
- false
- ..\..\StyleCop.Analyzers.ruleset
- 4.2
- true
- Unosquare.PassCore.Web
- 8.0
- enable
- true
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
- all
-
-
-
-
-
-
-
-
-
-
- Always
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PASSCORE_LDAP_PROVIDER
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ $(DefaultItemExcludes);**\node_modules\**;node_modules\**
+ Copyright (c) 2018-2021 - Unosquare
+ net6.0
+ Unosquare.PassCore.Web
+ Unosquare.PassCore.Web
+ true
+ false
+ 4.2
+ true
+ Unosquare.PassCore.Web
+ true
+ AllEnabledByDefault
+ true
+ enable
+ true
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PASSCORE_LDAP_PROVIDER
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeOptions.cs b/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeOptions.cs
index c0e81075..a59d80fa 100644
--- a/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeOptions.cs
+++ b/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeOptions.cs
@@ -1,139 +1,138 @@
-namespace Zyborg.PassCore.PasswordProvider.LDAP
+using Novell.Directory.Ldap;
+using Unosquare.PassCore.Common;
+
+namespace Zyborg.PassCore.PasswordProvider.LDAP;
+
+///
+/// Represents the options of this provider.
+///
+///
+public class LdapPasswordChangeOptions : IAppSettings
{
- using Novell.Directory.Ldap;
- using Unosquare.PassCore.Common;
+ private string[]? ldapHostnames;
+ private string? ldapPassword;
+ private string? ldapUsername;
+ private string? defaultDomain;
- ///
- /// Represents the options of this provider.
- ///
- ///
- public class LdapPasswordChangeOptions : IAppSettings
+ ///
+ public string[] LdapHostnames
{
- private string[]? ldapHostnames;
- private string? ldapPassword;
- private string? ldapUsername;
- private string? defaultDomain;
-
- ///
- public string[] LdapHostnames
- {
- get => ldapHostnames ?? new string[] { };
- set => ldapHostnames = value;
- }
+ get => ldapHostnames ?? new string[] { };
+ set => ldapHostnames = value;
+ }
- ///
- public string LdapPassword
- {
- get => ldapPassword ?? string.Empty;
- set => ldapPassword = value;
- }
+ ///
+ public string LdapPassword
+ {
+ get => ldapPassword ?? string.Empty;
+ set => ldapPassword = value;
+ }
- ///
- public string LdapUsername
- {
- get => ldapUsername ?? string.Empty;
- set => ldapUsername = value;
- }
+ ///
+ public string LdapUsername
+ {
+ get => ldapUsername ?? string.Empty;
+ set => ldapUsername = value;
+ }
- ///
- public string DefaultDomain
- {
- get => defaultDomain ?? string.Empty;
- set => defaultDomain = value;
- }
+ ///
+ public string DefaultDomain
+ {
+ get => defaultDomain ?? string.Empty;
+ set => defaultDomain = value;
+ }
- ///
- public int LdapPort { get; set; } = LdapConnection.DefaultSslPort;
+ ///
+ public int LdapPort { get; set; } = LdapConnection.DefaultSslPort;
- ///
- /// Gets or sets a value indicating whether [LDAP uses SSL].
- ///
- ///
- /// Optional, if 'true', then the specified port is using SSL encryption.
- /// By default this should set to 'true' when using port 636.
- ///
- ///
- /// true if [LDAP uses SSL]; otherwise, false.
- ///
- public bool LdapSecureSocketLayer { get; set; }
+ ///
+ /// Gets or sets a value indicating whether [LDAP uses SSL].
+ ///
+ ///
+ /// Optional, if 'true', then the specified port is using SSL encryption.
+ /// By default this should set to 'true' when using port 636.
+ ///
+ ///
+ /// true if [LDAP uses SSL]; otherwise, false.
+ ///
+ public bool LdapSecureSocketLayer { get; set; }
- ///
- /// Gets or sets a value indicating whether [LDAP start TLS].
- ///
- ///
- /// 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.
- ///
- ///
- /// true if [LDAP start TLS]; otherwise, false.
- ///
- public bool LdapStartTls { get; set; }
+ ///
+ /// Gets or sets a value indicating whether [LDAP start TLS].
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// true if [LDAP start TLS]; otherwise, false.
+ ///
+ public bool LdapStartTls { get; set; }
- ///
- /// Gets or sets a value indicating whether [LDAP ignore TLS errors].
- ///
- ///
- /// 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.
- ///
- ///
- /// true if [LDAP ignore TLS errors]; otherwise, false.
- ///
- public bool LdapIgnoreTlsErrors { get; set; }
+ ///
+ /// Gets or sets a value indicating whether [LDAP ignore TLS errors].
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// true if [LDAP ignore TLS errors]; otherwise, false.
+ ///
+ public bool LdapIgnoreTlsErrors { get; set; }
- ///
- /// Gets or sets a value indicating whether [LDAP ignore TLS validation].
- ///
- ///
- /// Optional, if 'true', then server certificates will be accepted regardless
- /// of being signed by a trusted CA or intermediary (e.g. self-signed).
- ///
- ///
- /// true if [LDAP ignore TLS validation]; otherwise, false.
- ///
- public bool LdapIgnoreTlsValidation { get; set; }
+ ///
+ /// Gets or sets a value indicating whether [LDAP ignore TLS validation].
+ ///
+ ///
+ /// Optional, if 'true', then server certificates will be accepted regardless
+ /// of being signed by a trusted CA or intermediary (e.g. self-signed).
+ ///
+ ///
+ /// true if [LDAP ignore TLS validation]; otherwise, false.
+ ///
+ public bool LdapIgnoreTlsValidation { get; set; }
- ///
- /// Gets or sets the LDAP search base.
- ///
- ///
- /// Distinguished Name (DN) of the base OU from which to search for
- /// the target users by their username (SAM Account Name).
- ///
- ///
- /// The LDAP search base.
- ///
- public string? LdapSearchBase { get; set; }
+ ///
+ /// Gets or sets the LDAP search base.
+ ///
+ ///
+ /// Distinguished Name (DN) of the base OU from which to search for
+ /// the target users by their username (SAM Account Name).
+ ///
+ ///
+ /// The LDAP search base.
+ ///
+ public string? LdapSearchBase { get; set; }
- ///
- /// Gets or sets a value indicating whether [hide user not found].
- ///
- ///
- /// 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.
- ///
- ///
- /// true if [hide user not found]; otherwise, false.
- ///
- public bool HideUserNotFound { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether [hide user not found].
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// true if [hide user not found]; otherwise, false.
+ ///
+ public bool HideUserNotFound { get; set; } = true;
- ///
- /// Gets or sets a value indicating whether [LDAP change password with delete add].
- ///
- ///
- /// true if [LDAP change password with delete add]; otherwise, false.
- ///
- public bool LdapChangePasswordWithDelAdd { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether [LDAP change password with delete add].
+ ///
+ ///
+ /// true if [LDAP change password with delete add]; otherwise, false.
+ ///
+ public bool LdapChangePasswordWithDelAdd { get; set; } = true;
- ///
- /// Gets or sets the LDAP search filter.
- ///
- ///
- /// The LDAP search filter.
- ///
- public string LdapSearchFilter { get; set; } = "(sAMAccountName={Username})";
- }
-}
+ ///
+ /// Gets or sets the LDAP search filter.
+ ///
+ ///
+ /// The LDAP search filter.
+ ///
+ public string LdapSearchFilter { get; set; } = "(sAMAccountName={Username})";
+}
\ No newline at end of file
diff --git a/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeProvider.cs b/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeProvider.cs
index e5645ce2..34aa04eb 100644
--- a/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeProvider.cs
+++ b/src/Zyborg.PassCore.PasswordProvider.LDAP/LdapPasswordChangeProvider.cs
@@ -1,364 +1,361 @@
-namespace Zyborg.PassCore.PasswordProvider.LDAP
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Novell.Directory.Ldap;
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Text.RegularExpressions;
+using Unosquare.PassCore.Common;
+using LdapRemoteCertificateValidationCallback =
+ Novell.Directory.Ldap.RemoteCertificateValidationCallback;
+
+namespace Zyborg.PassCore.PasswordProvider.LDAP;
+
+///
+/// Represents a LDAP password change provider using Novell LDAP Connection.
+///
+///
+public class LdapPasswordChangeProvider : IPasswordChangeProvider
{
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- using Novell.Directory.Ldap;
- using System;
- using System.Globalization;
- using System.Linq;
- using System.Net.Security;
- using System.Security.Cryptography.X509Certificates;
- using System.Text;
- using System.Text.RegularExpressions;
- using Unosquare.PassCore.Common;
- using LdapRemoteCertificateValidationCallback =
- Novell.Directory.Ldap.RemoteCertificateValidationCallback;
+ private readonly LdapPasswordChangeOptions _options;
+ private readonly ILogger _logger;
+
+ // First find user DN by username (SAM Account Name)
+ private readonly LdapSearchConstraints _searchConstraints = new(
+ 0,
+ 0,
+ LdapSearchConstraints.DerefNever,
+ 1000,
+ true,
+ 1,
+ null,
+ 10);
+
+ // TODO: is this related to https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard/issues/101 at all???
+ // Had to mark this as nullable.
+ private LdapRemoteCertificateValidationCallback? _ldapRemoteCertValidator;
///
- /// Represents a LDAP password change provider using Novell LDAP Connection.
+ /// Initializes a new instance of the class.
///
- ///
- public class LdapPasswordChangeProvider : IPasswordChangeProvider
+ /// The logger.
+ /// The _options.
+ public LdapPasswordChangeProvider(
+ ILogger logger,
+ IOptions options)
{
- private readonly LdapPasswordChangeOptions _options;
- private readonly ILogger _logger;
-
- // First find user DN by username (SAM Account Name)
- private readonly LdapSearchConstraints _searchConstraints = new LdapSearchConstraints(
- 0,
- 0,
- LdapSearchConstraints.DerefNever,
- 1000,
- true,
- 1,
- null,
- 10);
-
- // TODO: is this related to https://github.com/dsbenghe/Novell.Directory.Ldap.NETStandard/issues/101 at all???
- // Had to mark this as nullable.
- private LdapRemoteCertificateValidationCallback? _ldapRemoteCertValidator;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The _options.
- public LdapPasswordChangeProvider(
- ILogger logger,
- IOptions options)
- {
- _logger = logger;
- _options = options.Value;
- Init();
- }
+ _logger = logger;
+ _options = options.Value;
+ Init();
+ }
- ///
- ///
- /// Based on:
- /// * https://www.cs.bham.ac.uk/~smp/resources/ad-passwds/
- /// * https://support.microsoft.com/en-us/help/269190/how-to-change-a-windows-active-directory-and-lds-user-password-through
- /// * https://ltb-project.org/documentation/self-service-password/latest/config_ldap#active_directory
- /// * https://technet.microsoft.com/en-us/library/ff848710.aspx?f=255&MSPPError=-2147217396
- ///
- /// Check the above links for more information.
- ///
- [Obsolete]
- public ApiErrorItem? PerformPasswordChange(
- string username,
- string currentPassword,
- string newPassword)
+ ///
+ ///
+ /// Based on:
+ /// * https://www.cs.bham.ac.uk/~smp/resources/ad-passwds/
+ /// * https://support.microsoft.com/en-us/help/269190/how-to-change-a-windows-active-directory-and-lds-user-password-through
+ /// * https://ltb-project.org/documentation/self-service-password/latest/config_ldap#active_directory
+ /// * https://technet.microsoft.com/en-us/library/ff848710.aspx?f=255&MSPPError=-2147217396
+ ///
+ /// Check the above links for more information.
+ ///
+ [Obsolete]
+ public ApiErrorItem? PerformPasswordChange(
+ string username,
+ string currentPassword,
+ string newPassword)
+ {
+ try
{
- try
+ var cleanUsername = CleaningUsername(username);
+
+ var searchFilter = _options.LdapSearchFilter.Replace("{Username}", cleanUsername);
+
+ _logger.LogWarning("LDAP query: {0}", searchFilter);
+
+ using var ldap = BindToLdap();
+ var search = ldap.Search(
+ _options.LdapSearchBase,
+ LdapConnection.ScopeSub,
+ searchFilter,
+ new[] { "distinguishedName" },
+ false,
+ _searchConstraints);
+
+ // We cannot use search.Count here -- apparently it does not
+ // wait for the results to return before resolving the count
+ // but fortunately hasMore seems to block until final result
+ if (!search.HasMore())
{
- var cleanUsername = CleaningUsername(username);
-
- var searchFilter = _options.LdapSearchFilter.Replace("{Username}", cleanUsername);
-
- _logger.LogWarning("LDAP query: {0}", searchFilter);
-
- using var ldap = BindToLdap();
- var search = ldap.Search(
- _options.LdapSearchBase,
- LdapConnection.ScopeSub,
- searchFilter,
- new[] { "distinguishedName" },
- false,
- _searchConstraints);
-
- // We cannot use search.Count here -- apparently it does not
- // wait for the results to return before resolving the count
- // but fortunately hasMore seems to block until final result
- if (!search.HasMore())
- {
- _logger.LogWarning("Unable to find username: [{0}]", cleanUsername);
-
- return new ApiErrorItem(
- _options.HideUserNotFound ? ApiErrorCode.InvalidCredentials : ApiErrorCode.UserNotFound,
- _options.HideUserNotFound ? "Invalid credentials" : "Username could not be located");
- }
-
- if (search.Count > 1)
- {
- _logger.LogWarning("Found multiple with same username: [{0}] - Count {1}", cleanUsername, search.Count);
-
- // Hopefully this should not ever happen if AD is preserving SAM Account Name
- // uniqueness constraint, but just in case, handling this corner case
- return new ApiErrorItem(ApiErrorCode.UserNotFound, "Multiple matching user entries resolved");
- }
-
- var userDN = search.Next().Dn;
-
- if (_options.LdapChangePasswordWithDelAdd)
- {
- ChangePasswordDelAdd(currentPassword, newPassword, ldap, userDN);
- }
- else
- {
- ChangePasswordReplace(newPassword, ldap, userDN);
- }
-
- if (_options.LdapStartTls)
- ldap.StopTls();
-
- ldap.Disconnect();
+ _logger.LogWarning("Unable to find username: [{0}]", cleanUsername);
+
+ return new ApiErrorItem(
+ _options.HideUserNotFound ? ApiErrorCode.InvalidCredentials : ApiErrorCode.UserNotFound,
+ _options.HideUserNotFound ? "Invalid credentials" : "Username could not be located");
}
- catch (LdapException ex)
- {
- var item = ParseLdapException(ex);
- _logger.LogWarning(item.Message, ex);
+ if (search.Count > 1)
+ {
+ _logger.LogWarning("Found multiple with same username: [{0}] - Count {1}", cleanUsername, search.Count);
- return item;
+ // Hopefully this should not ever happen if AD is preserving SAM Account Name
+ // uniqueness constraint, but just in case, handling this corner case
+ return new ApiErrorItem(ApiErrorCode.UserNotFound, "Multiple matching user entries resolved");
}
- catch (Exception ex)
- {
- var item = ex is ApiErrorException apiError
- ? apiError.ToApiErrorItem()
- : new ApiErrorItem(ApiErrorCode.InvalidCredentials, $"Failed to update password: {ex.Message}");
- _logger.LogWarning(item.Message, ex);
+ var userDN = search.Next().Dn;
- return item;
+ if (_options.LdapChangePasswordWithDelAdd)
+ {
+ ChangePasswordDelAdd(currentPassword, newPassword, ldap, userDN);
+ }
+ else
+ {
+ ChangePasswordReplace(newPassword, ldap, userDN);
}
- // Everything seems to have worked:
- return null;
- }
+ if (_options.LdapStartTls)
+ ldap.StopTls();
- private static void ChangePasswordReplace(string newPassword, ILdapConnection ldap, string userDN)
- {
- // If you don't have the rights to Add and/or Delete the Attribute, you might have the right to change the password-attribute.
- // In this case uncomment the next 2 lines and comment the region 'Change Password by Delete/Add'
- var attribute = new LdapAttribute("userPassword", newPassword);
- var ldapReplace = new LdapModification(LdapModification.Replace, attribute);
- ldap.Modify(userDN, new[] { ldapReplace }); // Change with Replace
+ ldap.Disconnect();
}
+ catch (LdapException ex)
+ {
+ var item = ParseLdapException(ex);
- private static void ChangePasswordDelAdd(string currentPassword, string newPassword, ILdapConnection ldap, string userDN)
+ _logger.LogWarning(item.Message, ex);
+
+ return item;
+ }
+ catch (Exception ex)
{
- var oldPassBytes = Encoding.Unicode.GetBytes($@"""{currentPassword}""");
- var newPassBytes = Encoding.Unicode.GetBytes($@"""{newPassword}""");
+ var item = ex is ApiErrorException apiError
+ ? apiError.ToApiErrorItem()
+ : new ApiErrorItem(ApiErrorCode.InvalidCredentials, $"Failed to update password: {ex.Message}");
- var oldAttr = new LdapAttribute("unicodePwd", oldPassBytes);
- var newAttr = new LdapAttribute("unicodePwd", newPassBytes);
+ _logger.LogWarning(item.Message, ex);
- var ldapDel = new LdapModification(LdapModification.Delete, oldAttr);
- var ldapAdd = new LdapModification(LdapModification.Add, newAttr);
- ldap.Modify(userDN, new[] { ldapDel, ldapAdd }); // Change with Delete/Add
+ return item;
}
- private static ApiErrorItem ParseLdapException(LdapException ex)
- {
- // If the LDAP server returned an error, it will be formatted
- // similar to this:
- // "0000052D: AtrErr: DSID-03191083, #1:\n\t0: 0000052D: DSID-03191083, problem 1005 (CONSTRAINT_ATT_TYPE), data 0, Att 9005a (unicodePwd)\n\0"
- //
- // The leading number before the ':' is the Win32 API Error Code in HEX
- if (ex.LdapErrorMessage == null)
- {
- return new ApiErrorItem(ApiErrorCode.LdapProblem, "Unexpected null exception");
- }
+ // Everything seems to have worked:
+ return null;
+ }
- var m = Regex.Match(ex.LdapErrorMessage, "([0-9a-fA-F]+):");
+ private static void ChangePasswordReplace(string newPassword, ILdapConnection ldap, string userDN)
+ {
+ // If you don't have the rights to Add and/or Delete the Attribute, you might have the right to change the password-attribute.
+ // In this case uncomment the next 2 lines and comment the region 'Change Password by Delete/Add'
+ var attribute = new LdapAttribute("userPassword", newPassword);
+ var ldapReplace = new LdapModification(LdapModification.Replace, attribute);
+ ldap.Modify(userDN, new[] { ldapReplace }); // Change with Replace
+ }
- if (!m.Success)
- {
- return new ApiErrorItem(ApiErrorCode.LdapProblem, $"Unexpected error: {ex.LdapErrorMessage}");
- }
+ private static void ChangePasswordDelAdd(string currentPassword, string newPassword, ILdapConnection ldap, string userDN)
+ {
+ var oldPassBytes = Encoding.Unicode.GetBytes($@"""{currentPassword}""");
+ var newPassBytes = Encoding.Unicode.GetBytes($@"""{newPassword}""");
- var errCodeString = m.Groups[1].Value;
- var errCode = int.Parse(errCodeString, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
- var err = Win32ErrorCode.ByCode(errCode);
+ var oldAttr = new LdapAttribute("unicodePwd", oldPassBytes);
+ var newAttr = new LdapAttribute("unicodePwd", newPassBytes);
- return err == null
- ? new ApiErrorItem(ApiErrorCode.LdapProblem, $"Unexpected Win32 API error; error code: {errCodeString}")
- : new ApiErrorItem(ApiErrorCode.InvalidCredentials,
- $"Resolved Win32 API Error: code={err.Code} name={err.CodeName} desc={err.Description}");
- }
+ var ldapDel = new LdapModification(LdapModification.Delete, oldAttr);
+ var ldapAdd = new LdapModification(LdapModification.Add, newAttr);
+ ldap.Modify(userDN, new[] { ldapDel, ldapAdd }); // Change with Delete/Add
+ }
- private string CleaningUsername(string username)
+ private static ApiErrorItem ParseLdapException(LdapException ex)
+ {
+ // If the LDAP server returned an error, it will be formatted
+ // similar to this:
+ // "0000052D: AtrErr: DSID-03191083, #1:\n\t0: 0000052D: DSID-03191083, problem 1005 (CONSTRAINT_ATT_TYPE), data 0, Att 9005a (unicodePwd)\n\0"
+ //
+ // The leading number before the ':' is the Win32 API Error Code in HEX
+ if (ex.LdapErrorMessage == null)
{
- var cleanUsername = username;
- var index = cleanUsername.IndexOf("@", StringComparison.Ordinal);
- if (index >= 0)
- cleanUsername = cleanUsername.Substring(0, index);
+ return new ApiErrorItem(ApiErrorCode.LdapProblem, "Unexpected null exception");
+ }
- // Must sanitize the username to eliminate the possibility of injection attacks:
- // * https://docs.microsoft.com/en-us/windows/desktop/adschema/a-samaccountname
- // * https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb726984(v=technet.10)
- var invalidChars = "\"/\\[]:;|=,+*?<>\r\n\t".ToCharArray();
+ var m = Regex.Match(ex.LdapErrorMessage, "([0-9a-fA-F]+):");
- if (cleanUsername.IndexOfAny(invalidChars) >= 0)
- {
- throw new ApiErrorException("Username contains one or more invalid characters", ApiErrorCode.InvalidCredentials);
- }
+ if (!m.Success)
+ {
+ return new ApiErrorItem(ApiErrorCode.LdapProblem, $"Unexpected error: {ex.LdapErrorMessage}");
+ }
- // LDAP filters require escaping of some special chars:
- // * http://www.ldapexplorer.com/en/manual/109010000-ldap-filter-syntax.htm
- var escape = "()&|=>= 0)
+ cleanUsername = cleanUsername[..index];
- while (escapeIndex >= 0)
- {
- buff.Append(cleanUsername.Substring(copyFrom, escapeIndex));
- buff.Append(string.Format("\\{0:X}", (int)cleanUsername[escapeIndex]));
- copyFrom = escapeIndex + 1;
- escapeIndex = cleanUsername.IndexOfAny(escape, copyFrom);
- }
+ // Must sanitize the username to eliminate the possibility of injection attacks:
+ // * https://docs.microsoft.com/en-us/windows/desktop/adschema/a-samaccountname
+ // * https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb726984(v=technet.10)
+ var invalidChars = "\"/\\[]:;|=,+*?<>\r\n\t".ToCharArray();
- if (copyFrom < maxLen)
- buff.Append(cleanUsername.Substring(copyFrom));
- cleanUsername = buff.ToString();
- _logger.LogWarning("Had to clean username: [{0}] => [{1}]", username, cleanUsername);
+ if (cleanUsername.IndexOfAny(invalidChars) >= 0)
+ throw new ApiErrorException("Username contains one or more invalid characters", ApiErrorCode.InvalidCredentials);
- return cleanUsername ?? string.Empty;
- }
+ // LDAP filters require escaping of some special chars:
+ // * http://www.ldapexplorer.com/en/manual/109010000-ldap-filter-syntax.htm
+ var escape = "()&|=>= 0)
+ {
+ buff.Append(cleanUsername.Substring(copyFrom, escapeIndex));
+ buff.Append($"\\{cleanUsername[escapeIndex]:X}");
+ copyFrom = escapeIndex + 1;
+ escapeIndex = cleanUsername.IndexOfAny(escape, copyFrom);
+ }
- if (string.IsNullOrEmpty(_options.LdapPassword))
- {
- throw new ArgumentException("Options missing or invalid LDAP bind password",
- nameof(_options.LdapPassword));
- }
+ if (copyFrom < maxLen)
+ buff.Append(cleanUsername.Substring(copyFrom));
+ cleanUsername = buff.ToString();
+ _logger.LogWarning("Had to clean username: [{0}] => [{1}]", username, cleanUsername);
- if (string.IsNullOrEmpty(_options.LdapSearchBase))
- {
- throw new ArgumentException("Options must specify LDAP search base",
- nameof(_options.LdapSearchBase));
- }
+ return cleanUsername;
+ }
- if (string.IsNullOrWhiteSpace(_options.LdapSearchFilter))
- {
- throw new ArgumentException(
- $"No {nameof(_options.LdapSearchFilter)} is set. Fill attribute {nameof(_options.LdapSearchFilter)} in file appsettings.json",
- nameof(_options.LdapSearchFilter));
- }
+ private void Init()
+ {
+ // Validate required options
+ if (_options.LdapIgnoreTlsErrors || _options.LdapIgnoreTlsValidation)
+ _ldapRemoteCertValidator = CustomServerCertValidation;
- if (!_options.LdapSearchFilter.Contains("{Username}"))
- {
- throw new ArgumentException(
- $"The {nameof(_options.LdapSearchFilter)} should include {{Username}} value in the template string",
- nameof(_options.LdapSearchFilter));
- }
+ if (_options.LdapHostnames?.Length < 1)
+ {
+ throw new ArgumentException("Options must specify at least one LDAP hostname",
+ nameof(_options.LdapHostnames));
+ }
- // All other configuration is optional, but some may warrant attention
- if (!_options.HideUserNotFound)
- _logger.LogWarning($"Option [{nameof(_options.HideUserNotFound)}] is DISABLED; the presence or absence of usernames can be harvested");
+ if (string.IsNullOrEmpty(_options.LdapUsername))
+ {
+ throw new ArgumentException("Options missing or invalid LDAP bind distinguished name (DN)",
+ nameof(_options.LdapUsername));
+ }
- if (_options.LdapIgnoreTlsErrors)
- _logger.LogWarning($"Option [{nameof(_options.LdapIgnoreTlsErrors)}] is ENABLED; invalid certificates will be allowed");
- else if (_options.LdapIgnoreTlsValidation)
- _logger.LogWarning($"Option [{nameof(_options.LdapIgnoreTlsValidation)}] is ENABLED; untrusted certificate roots will be allowed");
+ if (string.IsNullOrEmpty(_options.LdapPassword))
+ {
+ throw new ArgumentException("Options missing or invalid LDAP bind password",
+ nameof(_options.LdapPassword));
+ }
- if (_options.LdapPort == LdapConnection.DefaultSslPort && !_options.LdapSecureSocketLayer)
- _logger.LogWarning($"Option [{nameof(_options.LdapSecureSocketLayer)}] is DISABLED in combination with standard SSL port [{_options.LdapPort}]");
+ if (string.IsNullOrEmpty(_options.LdapSearchBase))
+ {
+ throw new ArgumentException("Options must specify LDAP search base",
+ nameof(_options.LdapSearchBase));
+ }
- if (_options.LdapPort != LdapConnection.DefaultSslPort && !_options.LdapStartTls)
- _logger.LogWarning($"Option [{nameof(_options.LdapStartTls)}] is DISABLED in combination with non-standard TLS port [{_options.LdapPort}]");
+ if (string.IsNullOrWhiteSpace(_options.LdapSearchFilter))
+ {
+ throw new ArgumentException(
+ $"No {nameof(_options.LdapSearchFilter)} is set. Fill attribute {nameof(_options.LdapSearchFilter)} in file appsettings.json",
+ nameof(_options.LdapSearchFilter));
}
- [Obsolete]
- private LdapConnection BindToLdap()
+ if (!_options.LdapSearchFilter.Contains("{Username}"))
{
- var ldap = new LdapConnection();
- if (_ldapRemoteCertValidator != null)
- ldap.UserDefinedServerCertValidationDelegate += _ldapRemoteCertValidator;
+ throw new ArgumentException(
+ $"The {nameof(_options.LdapSearchFilter)} should include {{Username}} value in the template string",
+ nameof(_options.LdapSearchFilter));
+ }
+
+ // All other configuration is optional, but some may warrant attention
+ if (!_options.HideUserNotFound)
+ _logger.LogWarning($"Option [{nameof(_options.HideUserNotFound)}] is DISABLED; the presence or absence of usernames can be harvested");
+
+ if (_options.LdapIgnoreTlsErrors)
+ _logger.LogWarning($"Option [{nameof(_options.LdapIgnoreTlsErrors)}] is ENABLED; invalid certificates will be allowed");
+ else if (_options.LdapIgnoreTlsValidation)
+ _logger.LogWarning($"Option [{nameof(_options.LdapIgnoreTlsValidation)}] is ENABLED; untrusted certificate roots will be allowed");
+
+ if (_options.LdapPort == LdapConnection.DefaultSslPort && !_options.LdapSecureSocketLayer)
+ _logger.LogWarning($"Option [{nameof(_options.LdapSecureSocketLayer)}] is DISABLED in combination with standard SSL port [{_options.LdapPort}]");
- ldap.SecureSocketLayer = _options.LdapSecureSocketLayer;
+ if (_options.LdapPort != LdapConnection.DefaultSslPort && !_options.LdapStartTls)
+ _logger.LogWarning($"Option [{nameof(_options.LdapStartTls)}] is DISABLED in combination with non-standard TLS port [{_options.LdapPort}]");
+ }
+
+ [Obsolete]
+ private LdapConnection BindToLdap()
+ {
+ var ldap = new LdapConnection();
+ if (_ldapRemoteCertValidator != null)
+ ldap.UserDefinedServerCertValidationDelegate += _ldapRemoteCertValidator;
- string? bindHostname = null;
+ ldap.SecureSocketLayer = _options.LdapSecureSocketLayer;
- foreach (var h in _options.LdapHostnames)
+ string? bindHostname = null;
+
+ foreach (var h in _options.LdapHostnames)
+ {
+ try
{
- try
- {
- ldap.Connect(h, _options.LdapPort);
- bindHostname = h;
- break;
- }
- catch (Exception ex)
- {
- _logger.LogWarning($"Failed to connect to host [{h}]", ex);
- }
+ ldap.Connect(h, _options.LdapPort);
+ bindHostname = h;
+ break;
}
-
- if (string.IsNullOrEmpty(bindHostname))
+ catch (Exception ex)
{
- throw new ApiErrorException("Failed to connect to any configured hostname", ApiErrorCode.InvalidCredentials);
+ _logger.LogWarning($"Failed to connect to host [{h}]", ex);
}
+ }
- if (_options.LdapStartTls)
- ldap.StartTls();
+ if (string.IsNullOrEmpty(bindHostname))
+ {
+ throw new ApiErrorException("Failed to connect to any configured hostname", ApiErrorCode.InvalidCredentials);
+ }
- ldap.Bind(_options.LdapUsername, _options.LdapPassword);
+ if (_options.LdapStartTls)
+ ldap.StartTls();
- return ldap;
- }
+ ldap.Bind(_options.LdapUsername, _options.LdapPassword);
- ///
- /// Custom server certificate validation logic that handles our special
- /// cases based on configuration. This implements the logic of either
- /// ignoring just untrusted root errors or ignoring all TLS errors.
- ///
- /// The sender.
- /// The certificate.
- /// The chain.
- /// The SSL policy errors.
- /// true if the certificate validation was successful.
- private bool CustomServerCertValidation(
- object sender,
- X509Certificate certificate,
- X509Chain chain,
- SslPolicyErrors sslPolicyErrors) =>
- _options.LdapIgnoreTlsErrors || sslPolicyErrors == SslPolicyErrors.None || chain.ChainStatus
- .Any(x => x.Status switch
- {
- X509ChainStatusFlags.UntrustedRoot when _options.LdapIgnoreTlsValidation => true,
- _ => x.Status == X509ChainStatusFlags.NoError
- });
+ return ldap;
}
-}
+
+ ///
+ /// Custom server certificate validation logic that handles our special
+ /// cases based on configuration. This implements the logic of either
+ /// ignoring just untrusted root errors or ignoring all TLS errors.
+ ///
+ /// The sender.
+ /// The certificate.
+ /// The chain.
+ /// The SSL policy errors.
+ /// true if the certificate validation was successful.
+ private bool CustomServerCertValidation(
+ object sender,
+ X509Certificate certificate,
+ X509Chain chain,
+ SslPolicyErrors sslPolicyErrors) =>
+ _options.LdapIgnoreTlsErrors || sslPolicyErrors == SslPolicyErrors.None || chain.ChainStatus
+ .Any(x => x.Status switch
+ {
+ X509ChainStatusFlags.UntrustedRoot when _options.LdapIgnoreTlsValidation => true,
+ _ => x.Status == X509ChainStatusFlags.NoError
+ });
+}
\ No newline at end of file
diff --git a/src/Zyborg.PassCore.PasswordProvider.LDAP/Win32ErrorCode.cs b/src/Zyborg.PassCore.PasswordProvider.LDAP/Win32ErrorCode.cs
index b797850f..780725e8 100644
--- a/src/Zyborg.PassCore.PasswordProvider.LDAP/Win32ErrorCode.cs
+++ b/src/Zyborg.PassCore.PasswordProvider.LDAP/Win32ErrorCode.cs
@@ -1,123 +1,121 @@
-namespace Zyborg.PassCore.PasswordProvider.LDAP
-{
- using System.Collections.Generic;
+using System.Collections.Generic;
+
+namespace Zyborg.PassCore.PasswordProvider.LDAP;
+///
+/// Represents a container of Win32 Error Code.
+///
+public class Win32ErrorCode
+{
///
- /// Represents a container of Win32 Error Code.
+ /// Based on
+ /// docs.
+ /// provides a list of commonly anticipated error codes from a password change request.
///
- public class Win32ErrorCode
+ public static readonly IEnumerable Codes = new[]
{
- ///
- /// Based on
- /// docs.
- /// provides a list of commonly anticipated error codes from a password change request.
- ///
- public static readonly IEnumerable 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."),
- };
+ 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 readonly Dictionary ErrorByCode =
- new Dictionary();
+ private static readonly Dictionary ErrorByCode = new();
- static Win32ErrorCode()
+ static Win32ErrorCode()
+ {
+ foreach (var c in Codes)
{
- foreach (var c in Codes)
- {
- ErrorByCode[c.Code] = c;
- }
+ ErrorByCode[c.Code] = c;
}
+ }
- private Win32ErrorCode(int code, string codeName, string desc)
- {
- Code = code;
- CodeName = codeName;
- Description = desc;
- }
+ private Win32ErrorCode(int code, string codeName, string desc)
+ {
+ Code = code;
+ CodeName = codeName;
+ Description = desc;
+ }
- ///
- /// Gets the code.
- ///
- ///
- /// The code.
- ///
- public int Code { get; }
+ ///
+ /// Gets the code.
+ ///
+ ///
+ /// The code.
+ ///
+ public int Code { get; }
- ///
- /// Gets the name of the code.
- ///
- ///
- /// The name of the code.
- ///
- public string CodeName { get; }
+ ///
+ /// Gets the name of the code.
+ ///
+ ///
+ /// The name of the code.
+ ///
+ public string CodeName { get; }
- ///
- /// Gets the description.
- ///
- ///
- /// The description.
- ///
- public string Description { get; }
+ ///
+ /// Gets the description.
+ ///
+ ///
+ /// The description.
+ ///
+ public string Description { get; }
- ///
- /// Get Error Code by the code.
- ///
- /// The code.
- /// A Win32ErrorCode from the code.
- public static Win32ErrorCode? ByCode(int code) =>
- ErrorByCode.TryGetValue(code, out var err) ? err : null;
+ ///
+ /// Get Error Code by the code.
+ ///
+ /// The code.
+ /// A Win32ErrorCode from the code.
+ public static Win32ErrorCode? ByCode(int code) =>
+ ErrorByCode.TryGetValue(code, out var err) ? err : null;
- ///
- /// Returns a hash code for this instance.
- ///
- ///
- /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
- ///
- public override int GetHashCode() => Code;
+ ///
+ /// Returns a hash code for this instance.
+ ///
+ ///
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ ///
+ public override int GetHashCode() => Code;
- ///
- /// Determines whether the specified , is equal to this instance.
- ///
- /// The to compare with this instance.
- ///
- /// true if the specified is equal to this instance; otherwise, false.
- ///
- public override bool Equals(object? obj)
- {
- return obj != null && obj is Win32ErrorCode err && Code == err.Code;
- }
+ ///
+ /// Determines whether the specified , is equal to this instance.
+ ///
+ /// The to compare with this instance.
+ ///
+ /// true if the specified is equal to this instance; otherwise, false.
+ ///
+ public override bool Equals(object? obj)
+ {
+ return obj is Win32ErrorCode err && Code == err.Code;
}
-}
+}
\ No newline at end of file
diff --git a/src/Zyborg.PassCore.PasswordProvider.LDAP/Zyborg.PassCore.PasswordProvider.LDAP.csproj b/src/Zyborg.PassCore.PasswordProvider.LDAP/Zyborg.PassCore.PasswordProvider.LDAP.csproj
index efcc098e..28166d5a 100644
--- a/src/Zyborg.PassCore.PasswordProvider.LDAP/Zyborg.PassCore.PasswordProvider.LDAP.csproj
+++ b/src/Zyborg.PassCore.PasswordProvider.LDAP/Zyborg.PassCore.PasswordProvider.LDAP.csproj
@@ -1,25 +1,24 @@
-
- net5.0
- true
- enable
- true
- ..\..\StyleCop.Analyzers.ruleset
- Based on Novell.Directory.Ldap.NETStandard & other work.
-
+
+ net6.0
+ true
+ enable
+ true
+ true
+ AllEnabledByDefault
+ true
+ Based on Novell.Directory.Ldap.NETStandard & other work.
+
-
-
-
-
-
- All
-
-
+
+
+
+
+
-
-
-
+
+
+