Commit 791e09d2 authored by Ana Atayde's avatar Ana Atayde Committed by Geovanni Perez

Password provider (#149)

* Password provider

* Working on refactor

* Alerts

* Messages

* Update DebugPasswordChangeProvider.cs

* Update DebugPasswordChangeProvider.cs

* Update DebugPasswordChangeProvider.cs
parent 17ba3a65
......@@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unosquare.PassCore.Web", "src\Unosquare.PassCore.Web\Unosquare.PassCore.Web.csproj", "{22E2F79B-7816-4FAB-894D-112759551796}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unosquare.PassCore.PasswordProvider", "src\Unosquare.PassCore.PasswordProvider\Unosquare.PassCore.PasswordProvider.csproj", "{4329B3BC-3CD4-4A19-AAAC-6EB7200DA29A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unosquare.PassCore.Common", "src\Unosquare.PassCore.Common\Unosquare.PassCore.Common.csproj", "{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
......@@ -25,12 +29,22 @@ Global
{22E2F79B-7816-4FAB-894D-112759551796}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22E2F79B-7816-4FAB-894D-112759551796}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22E2F79B-7816-4FAB-894D-112759551796}.Release|Any CPU.Build.0 = Release|Any CPU
{4329B3BC-3CD4-4A19-AAAC-6EB7200DA29A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4329B3BC-3CD4-4A19-AAAC-6EB7200DA29A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4329B3BC-3CD4-4A19-AAAC-6EB7200DA29A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4329B3BC-3CD4-4A19-AAAC-6EB7200DA29A}.Release|Any CPU.Build.0 = Release|Any CPU
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{22E2F79B-7816-4FAB-894D-112759551796} = {0A003964-77CA-4779-BD97-BADDD710A745}
{4329B3BC-3CD4-4A19-AAAC-6EB7200DA29A} = {0A003964-77CA-4779-BD97-BADDD710A745}
{E80F6938-B399-44DD-8BF0-A27DFD7A6B7A} = {0A003964-77CA-4779-BD97-BADDD710A745}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {30FE6198-367B-44D3-97CC-50927E101F3E}
......
namespace Unosquare.PassCore.Web.Models
namespace Unosquare.PassCore.Common
{
/// <summary>
/// Represents error codes
......@@ -44,4 +44,4 @@ namespace Unosquare.PassCore.Web.Models
/// </value>
public string Message { get; set; }
}
}
\ No newline at end of file
}
namespace Unosquare.PassCore.Common
{
/// <summary>
/// Represents a interface for a password change provider
/// </summary>
public interface IPasswordChangeProvider
{
/// <summary>
/// Performs the password change.
/// </summary>
/// <param name="username">The username.</param>
/// <param name="currentPassword">The current password.</param>
/// <param name="newPassword">The new password.</param>
/// <returns></returns>
ApiErrorItem PerformPasswordChange(string username, string currentPassword, string newPassword);
}
}
\ No newline at end of file
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
namespace Unosquare.PassCore.PasswordProvider
{
using System.Collections.Generic;
public class PasswordChangeOptions
{
public bool UseAutomaticContext { get; set; } = true;
public int LdapPort { get; set; } = 389;
public string LdapHostname { get; set; }
public string LdapPassword { get; set; }
public string LdapUsername { get; set; }
public List<string> RestrictedADGroups { get; set; }
public bool CheckRestrictedAdGroups { get; set; }
}
}
#pragma warning disable SA1310 // Field names must not contain underscore
namespace Unosquare.PassCore.Web.Helpers
#pragma warning disable SA1310 // Field names must not contain underscore
namespace Unosquare.PassCore.PasswordProvider
{
using System;
/// <summary>
/// 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
/// </summary>
internal partial class PasswordChangeProvider
partial class PasswordChangeProvider
{
// See http://support.microsoft.com/kb/155012
internal const int ERROR_PASSWORD_MUST_CHANGE = 1907;
......
namespace Unosquare.PassCore.Web.Helpers
namespace Unosquare.PassCore.PasswordProvider
{
using System.DirectoryServices.AccountManagement;
using System;
using Microsoft.Extensions.Options;
using Models;
using System.Linq;
using Common;
internal partial class PasswordChangeProvider : IPasswordChangeProvider
public partial class PasswordChangeProvider : IPasswordChangeProvider
{
private readonly AppSettings _options;
private readonly PasswordChangeOptions _options;
public PasswordChangeProvider(IOptions<AppSettings> options)
public PasswordChangeProvider(IOptions<PasswordChangeOptions> options)
{
_options = options.Value;
}
public ApiErrorItem PerformPasswordChange(ChangePasswordModel model)
public ApiErrorItem PerformPasswordChange(string username, string currentPassword, string newPassword)
{
// perform the password change
try
{
// Check for default domain: if none given, ensure EFLD can be used as an override.
var parts = model.Username.Split(new[] { '@' }, StringSplitOptions.RemoveEmptyEntries);
var domain = parts.Length > 1 ? parts[1] : _options.ClientSettings.DefaultDomain;
// Domain-determinance
if (string.IsNullOrEmpty(domain))
{
return new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidDomain, Message = _options.ClientSettings.Alerts.ErrorInvalidDomain };
}
var username = parts.Length > 1 ? model.Username : $"{model.Username}@{domain}";
using (var principalContext = AcquirePrincipalContext())
{
var userPrincipal = UserPrincipal.FindByIdentity(principalContext, username);
......@@ -38,31 +27,31 @@ namespace Unosquare.PassCore.Web.Helpers
// Check if the user principal exists
if (userPrincipal == null)
{
return new ApiErrorItem { ErrorCode = ApiErrorCode.UserNotFound, Message = _options.ClientSettings.Alerts.ErrorInvalidUser };
return new ApiErrorItem { ErrorCode = ApiErrorCode.UserNotFound };
}
// Check if password change is allowed
if (userPrincipal.UserCannotChangePassword)
{
return new ApiErrorItem { ErrorCode = ApiErrorCode.ChangeNotPermitted, Message = _options.ClientSettings.Alerts.ErrorPasswordChangeNotAllowed };
return new ApiErrorItem { ErrorCode = ApiErrorCode.ChangeNotPermitted };
}
// Verify user is not a member of an excluded group
if (_options.ClientSettings.CheckRestrictedAdGroups)
if (_options.CheckRestrictedAdGroups)
{
foreach (var userPrincipalAuthGroup in userPrincipal.GetAuthorizationGroups())
{
if (_options.ClientSettings.RestrictedADGroups.Contains(userPrincipalAuthGroup.Name))
if (_options.RestrictedADGroups.Contains(userPrincipalAuthGroup.Name))
{
return new ApiErrorItem { ErrorCode = ApiErrorCode.ChangeNotPermitted, Message = _options.ClientSettings.Alerts.ErrorPasswordChangeNotAllowed };
return new ApiErrorItem { ErrorCode = ApiErrorCode.ChangeNotPermitted };
}
}
}
// Validate user credentials
if (principalContext.ValidateCredentials(model.Username, model.CurrentPassword) == false)
if (principalContext.ValidateCredentials(username, currentPassword) == false)
{
if (!LogonUser(username, domain, model.CurrentPassword, LogonTypes.Network, LogonProviders.Default, out _))
if (!LogonUser(username, username.Split('@').Last(), currentPassword, LogonTypes.Network, LogonProviders.Default, out _))
{
var errorCode = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
switch (errorCode)
......@@ -72,7 +61,7 @@ namespace Unosquare.PassCore.Web.Helpers
// Both of these means that the password CAN change and that we got the correct password
break;
default:
return new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidCredentials, Message = _options.ClientSettings.Alerts.ErrorInvalidCredentials };
return new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidCredentials };
}
}
}
......@@ -81,14 +70,14 @@ namespace Unosquare.PassCore.Web.Helpers
try
{
// Try by regular ChangePassword method
userPrincipal.ChangePassword(model.CurrentPassword, model.NewPassword);
userPrincipal.ChangePassword(currentPassword,newPassword);
}
catch
{
if (_options.PasswordChangeOptions.UseAutomaticContext) { throw; }
if (_options.UseAutomaticContext) { throw; }
// If the previous attempt failed, use the SetPassword method.
userPrincipal.SetPassword(model.NewPassword);
userPrincipal.SetPassword(newPassword);
}
userPrincipal.Save();
......@@ -106,7 +95,7 @@ namespace Unosquare.PassCore.Web.Helpers
{
PrincipalContext principalContext;
if (_options.PasswordChangeOptions.UseAutomaticContext)
if (_options.UseAutomaticContext)
{
principalContext = new PrincipalContext(ContextType.Domain);
}
......@@ -114,9 +103,9 @@ namespace Unosquare.PassCore.Web.Helpers
{
principalContext = new PrincipalContext(
ContextType.Domain,
$"{_options.PasswordChangeOptions.LdapHostname}:{_options.PasswordChangeOptions.LdapPort}",
_options.PasswordChangeOptions.LdapUsername,
_options.PasswordChangeOptions.LdapPassword);
$"{_options.LdapHostname}:{_options.LdapPort}",
_options.LdapUsername,
_options.LdapPassword);
}
return principalContext;
......
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.0-rc1-final" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.5.0-rc1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Unosquare.PassCore.Common\Unosquare.PassCore.Common.csproj" />
</ItemGroup>
</Project>
......@@ -15,7 +15,7 @@ import { ViewOptions } from '../models/view-options.model';
import { ErrorsPasswordForm } from '../models/errors-password-form.model';
const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
const usernameRegex = /^[a-z0-9._-]{3,15}$/; // Maybe find a better regex
const usernameRegex = /^[a-zA-Z0-9._-]{3,20}$/; // Maybe find a better regex
@Component({
selector: 'app-root',
......@@ -155,8 +155,27 @@ export class ChangePasswordComponent implements OnInit {
this.clean('success');
},
errorResponse => {
errorResponse.error.errors.forEach((error: any) => {
this.ErrorAlertMessage += error.message;
errorResponse.error.errors.forEach((error: any) => {
switch (error.errorCode) {
case 0:
return this.ErrorAlertMessage += error.message;
case 1:
return this.ErrorAlertMessage += this.ViewOptions.alerts.errorFieldRequired;
case 2:
return this.ErrorAlertMessage += this.ViewOptions.alerts.errorFieldMismatch;
case 3:
return this.ErrorAlertMessage += this.ViewOptions.alerts.errorInvalidUser;
case 4:
return this.ErrorAlertMessage += this.ViewOptions.alerts.errorInvalidCredentials;
case 5:
return this.ErrorAlertMessage += this.ViewOptions.alerts.errorCaptcha;
case 6:
return this.ErrorAlertMessage += this.ViewOptions.alerts.errorPasswordChangeNotAllowed;
case 7:
return this.ErrorAlertMessage += this.ViewOptions.alerts.errorInvalidDomain;
default:
return null;
}
});
this.openSnackBar(this.ErrorAlertMessage, 'OK');
this.clean('error');
......
......@@ -4,4 +4,8 @@ export class Alerts {
errorPasswordChangeNotAllowed: string;
successAlertBody: string;
successAlertTitle: string;
errorFieldRequired: string;
errorFieldMismatch: string;
errorInvalidUser: string;
errorCaptcha: string;
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ namespace Unosquare.PassCore.Web.Controllers
using System.Threading.Tasks;
using System;
using Helpers;
using Unosquare.PassCore.Common;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Models;
......@@ -69,11 +70,7 @@ namespace Unosquare.PassCore.Web.Controllers
// Sonar-Codacy suggested ConfigureAwait
if (await ValidateRecaptcha(model.Recaptcha).ConfigureAwait(false) == false)
{
result.Errors.Add(new ApiErrorItem
{
ErrorCode = ApiErrorCode.InvalidCaptcha,
Message = _options.ClientSettings.Alerts.ErrorCaptcha
});
result.Errors.Add(new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidCaptcha });
}
}
catch (Exception ex)
......@@ -90,7 +87,20 @@ namespace Unosquare.PassCore.Web.Controllers
return BadRequest(result);
}
var resultPasswordChange = _passwordChangeProvider.PerformPasswordChange(model);
// Check for default domain: if none given, ensure EFLD can be used as an override.
var parts = model.Username.Split(new[] { '@' }, StringSplitOptions.RemoveEmptyEntries);
var domain = parts.Length > 1 ? parts[1] : _options.ClientSettings.DefaultDomain;
// Domain-determinance
if (string.IsNullOrEmpty(domain))
{
result.Errors.Add(new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidDomain });
return BadRequest(result);
}
var currentUsername = parts.Length > 1 ? model.Username : $"{model.Username}@{domain}";
var resultPasswordChange = _passwordChangeProvider.PerformPasswordChange(currentUsername, model.CurrentPassword, model.NewPassword);
if (resultPasswordChange != null)
{
......
......@@ -2,31 +2,36 @@
{
using System;
using Models;
using Microsoft.Extensions.Options;
using Common;
// Sonar-Codacy thought we needed a static method here; and suggested dual default nulls was pointless.
internal class DebugPasswordChangeProvider : IPasswordChangeProvider
{
private readonly AppSettings _options;
public DebugPasswordChangeProvider(IOptions<AppSettings> options)
{
_options = options.Value;
}
public ApiErrorItem PerformPasswordChange(ChangePasswordModel model)
public ApiErrorItem PerformPasswordChange(string username, string currentPassword, string newPassword)
{
var username = model.Username.Substring(0, model.Username.IndexOf("@", StringComparison.Ordinal));
var currentUsername = username.Substring(0, username.IndexOf("@", StringComparison.Ordinal));
switch (username)
switch (currentUsername)
{
case "error":
return new ApiErrorItem { ErrorCode = ApiErrorCode.Generic, Message = _options.ClientSettings.Alerts.ErrorCaptcha };
case "notfound":
return new ApiErrorItem { ErrorCode = ApiErrorCode.UserNotFound, Message = _options.ClientSettings.Alerts.ErrorInvalidUser };
return new ApiErrorItem { ErrorCode = ApiErrorCode.Generic, Message ="Error" };
case "changeNotPermitted":
return new ApiErrorItem { ErrorCode = ApiErrorCode.ChangeNotPermitted };
case "fieldMismatch":
return new ApiErrorItem { ErrorCode = ApiErrorCode.FieldMismatch };
case "fieldRequired":
return new ApiErrorItem { ErrorCode = ApiErrorCode.FieldRequired };
case "invalidCaptcha":
return new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidCaptcha };
case "invalidCredentials":
return new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidCredentials };
case "invalidDomain":
return new ApiErrorItem { ErrorCode = ApiErrorCode.InvalidDomain };
case "userNotFound":
return new ApiErrorItem { ErrorCode = ApiErrorCode.UserNotFound };
default:
return null;
}
}
}
}
\ No newline at end of file
}
namespace Unosquare.PassCore.Web.Helpers
{
using Models;
public interface IPasswordChangeProvider
{
ApiErrorItem PerformPasswordChange(ChangePasswordModel model);
}
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ namespace Unosquare.PassCore.Web.Models
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Common;
/// <summary>
/// Represent a generic response from a REST API call
......
......@@ -9,27 +9,15 @@ namespace Unosquare.PassCore.Web.Models
{
public bool EnableHttpsRedirect { get; set; } = true;
public ClientSettings ClientSettings { get; set; }
public PasswordChangeOptions PasswordChangeOptions { get; set; }
public string RecaptchaPrivateKey { get; set; }
}
public class PasswordChangeOptions
{
public bool UseAutomaticContext { get; set; } = true;
public int LdapPort { get; set; } = 389;
public string LdapHostname { get; set; }
public string LdapPassword { get; set; }
public string LdapUsername { get; set; }
}
public class ClientSettings
{
public Alerts Alerts { get; set; }
public bool CheckRestrictedAdGroups { get; set; }
public bool ShowPasswordMeter { get; set; }
public ChangePasswordForm ChangePasswordForm { get; set; }
public ErrorsPasswordForm ErrorsPasswordForm { get; set; }
public List<string> RestrictedADGroups { get; set; }
public Recaptcha Recaptcha { get; set; }
public string ApplicationTitle { get; set; }
public string ChangePasswordTitle { get; set; }
......@@ -68,6 +56,8 @@ namespace Unosquare.PassCore.Web.Models
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 class ErrorsPasswordForm
......
namespace Unosquare.PassCore.Web.Models
{
using Common;
using System.ComponentModel.DataAnnotations;
public class ChangePasswordModel
......
namespace Unosquare.PassCore.Web
{
using Helpers;
using Common;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore;
......@@ -8,6 +9,7 @@ namespace Unosquare.PassCore.Web
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PasswordProvider;
using Models;
#if !DEBUG
using Microsoft.AspNetCore.Rewrite;
......@@ -70,6 +72,7 @@ namespace Unosquare.PassCore.Web
#if DEBUG
services.AddSingleton<IPasswordChangeProvider, DebugPasswordChangeProvider>();
#else
services.Configure<PasswordChangeOptions>(Configuration.GetSection(nameof(PasswordChangeOptions)));
services.AddSingleton<IPasswordChangeProvider, PasswordChangeProvider>();
#endif
}
......
......@@ -18,7 +18,6 @@
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.0.*" />
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.*" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.5.0-rc1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.*">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
......@@ -36,6 +35,10 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Unosquare.PassCore.Common\Unosquare.PassCore.Common.csproj" />
<ProjectReference Include="..\Unosquare.PassCore.PasswordProvider\Unosquare.PassCore.PasswordProvider.csproj" />
</ItemGroup>
<Target Name="NpmInstall" BeforeTargets="Build" Condition="!Exists('.\package-lock.json')">
<Exec Command="node --version" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
......
......@@ -7,16 +7,22 @@
"Microsoft": "Information"
}
},
"PasswordChangeOptions": {
"UseAutomaticContext": true,
"LdapHostname": "",
"LdapPort": 389,
"LdapUsername": "",
"LdapPassword": "",
"CheckRestrictedAdGroups": true,
"RestrictedADGroups": [
"Administrators",
"Domain Admins",
"Enterprise Admins"
]
},
"AppSettings": {
"EnableHttpsRedirect": false,
"RecaptchaPrivateKey": "", // ReCAPTCHA private key: replace this!
"PasswordChangeOptions": {
"UseAutomaticContext": true,
"LdapHostname": "",
"LdapPort": 389,
"LdapUsername": "",
"LdapPassword": ""
},
"ClientSettings": {
"ShowPasswordMeter": true,
"Recaptcha": {
......@@ -46,13 +52,7 @@
"UsernameEmailPattern": "Please enter a valid email address",
"PasswordMatch": "Passwords do not match"
},
"CheckRestrictedAdGroups": true,
"DefaultDomain": "domain.com", // Set your default AD domain here, or non "@" logins will not work!
"RestrictedADGroups": [
"Administrators",
"Domain Admins",
"Enterprise Admins"
],
"Alerts": {
"SuccessAlertTitle": "You have changed your password successfully.",
"SuccessAlertBody": "Please note it may take a few hours for your new password to reach all domain controllers.",
......@@ -60,7 +60,9 @@
"ErrorInvalidCredentials": "You need to provide the correct current password.",
"ErrorInvalidDomain": "You have supplied an invalid domain to logon to.",
"ErrorInvalidUser": "We could not find your user account.",
"ErrorCaptcha": "Could not verify you are not a robot"
"ErrorCaptcha": "Could not verify you are not a robot",
"ErrorFieldRequired": "Fulfil all the fields",
"ErrorFieldMismatch": "The passwords do not match"
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment