Skip to content

Commit fb73a8f

Browse files
authored
Merge pull request #112 from jackbuehner/password-change
Add the ability to change account passwords from the web app
2 parents 08445f5 + 217670c commit fb73a8f

File tree

15 files changed

+772
-13
lines changed

15 files changed

+772
-13
lines changed

aspx/wwwroot/App_Code/AuthService.cs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ public string ValidateCredentials(string username, string password)
5050
{
5151
return new JavaScriptSerializer().Serialize(new { success = false, error = result.Item2, domain = domain });
5252
}
53-
54-
55-
5653
}
5754

5855
[WebMethod]
@@ -61,4 +58,46 @@ public string GetDomainName()
6158
{
6259
return AuthUtilities.SignOn.GetDomainName();
6360
}
61+
62+
[WebMethod]
63+
[ScriptMethod]
64+
public string ChangeCredentials(string username, string oldPassword, string newPassword)
65+
{
66+
if (System.Configuration.ConfigurationManager.AppSettings["PasswordChange.Enabled"] == "false")
67+
{
68+
return new JavaScriptSerializer().Serialize(new { success = false, error = "Password change is disabled." });
69+
}
70+
71+
// if the username contains a domain, split it to get the username and domain separately
72+
string domain = null;
73+
if (username.Contains("\\"))
74+
{
75+
string[] parts = username.Split(new[] { '\\' }, 2);
76+
domain = parts[0]; // the part before the backslash is the domain
77+
username = parts[1]; // the part after the backslash is the username
78+
}
79+
else
80+
{
81+
domain = AuthUtilities.SignOn.GetDomainName();
82+
}
83+
84+
if (string.IsNullOrEmpty(username))
85+
{
86+
return new JavaScriptSerializer().Serialize(new { success = false, error = "Username must be provided.", domain = domain });
87+
}
88+
89+
// attempt to change the credentials for the user
90+
var result = AuthUtilities.SignOn.ChangeCredentials(username, oldPassword, newPassword, domain);
91+
var success = result.Item1;
92+
var errorMessage = result.Item2;
93+
94+
if (success)
95+
{
96+
return new JavaScriptSerializer().Serialize(new { success = true, username = username, domain = domain });
97+
}
98+
else
99+
{
100+
return new JavaScriptSerializer().Serialize(new { success = false, error = errorMessage, domain = domain });
101+
}
102+
}
64103
}

aspx/wwwroot/App_Code/AuthUtilities.cs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public UserInformation GetUserInformation(HttpRequest request)
208208

209209
// get the full name of the user
210210
string fullName = user.DisplayName ?? user.Name ?? user.SamAccountName;
211-
211+
212212
// get all groups of which the user is a member (checks all domains and local machine groups)
213213
var groupInformation = UserInformation.GetAllUserGroups(user);
214214

@@ -857,6 +857,7 @@ out IntPtr phToken
857857
public const int ERROR_INVALID_WORKSTATION = 1329; // the user is not allowed to log on to this workstation
858858
public const int ERROR_PASSWORD_EXPIRED = 1330; // the user's password has expired
859859
public const int ERROR_ACCOUNT_DISABLED = 1331; // the user account is disabled
860+
public const int ERROR_PASSWORD_MUST_CHANGE = 1907; // the user account password must change before signing in
860861

861862
[DllImport("kernel32.dll", SetLastError = true)]
862863
[return: MarshalAs(UnmanagedType.Bool)]
@@ -944,10 +945,135 @@ public static Tuple<bool, string> ValidateCredentials(string username, string pa
944945
return Tuple.Create(false, Resources.WebResources.Login_PasswordExpiredError);
945946
case ERROR_ACCOUNT_DISABLED:
946947
return Tuple.Create(false, Resources.WebResources.Login_AccountDisabledError);
948+
case ERROR_PASSWORD_MUST_CHANGE:
949+
return Tuple.Create(false, Resources.WebResources.Login_PasswordMustChange);
947950
default:
948951
return Tuple.Create(false, "An unknown error occurred: " + errorCode);
949952
}
950953
}
951954
}
955+
956+
[DllImport("Netapi32.dll", SetLastError = true)]
957+
public static extern int NetUserChangePassword(
958+
[In] string domainname,
959+
[In] string username,
960+
[In] string oldpassword,
961+
[In] string newpassword
962+
);
963+
public static Tuple<bool, string> ChangeCredentials(string username, string oldPassword, string newPassword, string domain)
964+
{
965+
if (domain.Trim() == Environment.MachineName)
966+
{
967+
domain = null; // for local machine
968+
}
969+
970+
string entryUrl = null;
971+
972+
// if the user is on the local machine, we can use the WinNT provider to change the password
973+
if (string.IsNullOrEmpty(domain))
974+
{
975+
entryUrl = "WinNT://" + Environment.MachineName + "/" + username + ",user";
976+
}
977+
// othwerwise, we need to find the user's distinguished name in the domain
978+
// so we can use the LDAP provider to change the password
979+
else
980+
{
981+
string userDistinguishedName = null;
982+
string ldapPath = "LDAP://" + domain;
983+
try
984+
{
985+
986+
using (DirectoryEntry searchRoot = new DirectoryEntry(ldapPath))
987+
{
988+
using (DirectorySearcher searcher = new DirectorySearcher(searchRoot))
989+
{
990+
searcher.Filter = "(&(objectClass=user)(sAMAccountName=" + username + "))";
991+
searcher.PropertiesToLoad.Add("distinguishedName");
992+
993+
SearchResult result = searcher.FindOne();
994+
if (result != null && result.Properties.Contains("distinguishedName"))
995+
{
996+
userDistinguishedName = result.Properties["distinguishedName"][0].ToString();
997+
}
998+
}
999+
}
1000+
}
1001+
catch (Exception ex)
1002+
{
1003+
return Tuple.Create(false, "The domain cannot be accessed.");
1004+
}
1005+
1006+
if (string.IsNullOrEmpty(userDistinguishedName))
1007+
{
1008+
return Tuple.Create(false, "User could not be found in the domain: " + domain);
1009+
}
1010+
1011+
entryUrl = "LDAP://" + domain + "/" + userDistinguishedName;
1012+
}
1013+
1014+
// get the user's directory entry and then attempt to change the password
1015+
using (DirectoryEntry user = new DirectoryEntry(entryUrl))
1016+
{
1017+
// if the user is not found, throw an exception
1018+
if (user == null)
1019+
{
1020+
return Tuple.Create(false, "The user could not be found.");
1021+
}
1022+
1023+
// change the password
1024+
{
1025+
try
1026+
{
1027+
user.Invoke("ChangePassword", new object[] { oldPassword, newPassword });
1028+
user.CommitChanges();
1029+
return Tuple.Create(true, (string)null);
1030+
}
1031+
catch (System.Reflection.TargetInvocationException ex)
1032+
{
1033+
// if the password change fails, return false with an error message
1034+
if (ex.InnerException != null)
1035+
{
1036+
// if there is a constraint violation, try the PrincipalContext method
1037+
if (ex.InnerException is System.DirectoryServices.DirectoryServicesCOMException)
1038+
{
1039+
try
1040+
{
1041+
if (string.IsNullOrEmpty(domain))
1042+
{
1043+
using (var pc = new PrincipalContext(ContextType.Machine))
1044+
using (var userPrincipal = UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, username))
1045+
{
1046+
userPrincipal.ChangePassword(oldPassword, newPassword);
1047+
userPrincipal.Save();
1048+
return Tuple.Create(true, (string)null);
1049+
}
1050+
}
1051+
else
1052+
{
1053+
using (var pc = new PrincipalContext(ContextType.Domain, domain ?? Environment.MachineName))
1054+
using (var userPrincipal = UserPrincipal.FindByIdentity(pc, IdentityType.SamAccountName, username))
1055+
{
1056+
userPrincipal.ChangePassword(oldPassword, newPassword);
1057+
userPrincipal.Save();
1058+
return Tuple.Create(true, (string)null);
1059+
}
1060+
}
1061+
}
1062+
catch (Exception pEx)
1063+
{
1064+
return Tuple.Create(false, pEx.Message);
1065+
}
1066+
}
1067+
return Tuple.Create(false, ex.InnerException.Message);
1068+
}
1069+
throw ex; // rethrow if there is no inner exception - we don't know what went wrong
1070+
}
1071+
catch (Exception ex)
1072+
{
1073+
return Tuple.Create(false, ex.Message);
1074+
}
1075+
}
1076+
}
1077+
}
9521078
}
9531079
}

aspx/wwwroot/App_Data/appSettings.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
<appSettings>
33
<add key="RegistryApps.Enabled" value="true" />
44
<add key="RegistryApps.AdditionalProperties" value="drivestoredirect:s:*;redirectclipboard:i:1" />
5+
<add key="PasswordChange.Enabled" value="false" />
56
</appSettings>

aspx/wwwroot/App_GlobalResources/WebResources.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@
3737
<value>You are not allowed to sign in to this server.</value>
3838
</data>
3939
<data name="Login_PasswordExpiredError" xml:space="preserve">
40-
<value>The password for this account has expired.</value>
40+
<value>The password for this account has expired. {password_change_button}</value>
4141
</data>
4242
<data name="Login_AccountDisabledError" xml:space="preserve">
4343
<value>Your account is currently disabled.</value>
4444
</data>
45+
<data name="Login_PasswordMustChange" xml:space="preserve">
46+
<value>You must change your password before you can sign in. {password_change_button}</value>
47+
</data>
4548
</root>

aspx/wwwroot/lib/controls/AppRoot.ascx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@
267267
hidePortsEnabled: '<%= System.Configuration.ConfigurationManager.AppSettings["App.HidePortsEnabled"] %>',
268268
iconBackgroundsEnabled: '<%= System.Configuration.ConfigurationManager.AppSettings["App.IconBackgroundsEnabled"] %>',
269269
simpleModeEnabled: '<%= System.Configuration.ConfigurationManager.AppSettings["App.SimpleModeEnabled"] %>',
270+
passwordChangeEnabled: '<%= System.Configuration.ConfigurationManager.AppSettings["PasswordChange.Enabled"] %>',
270271
}
271272
window.__machineName = '<%= resolver.Resolve(Environment.MachineName) %>';
272273
window.__envMachineName = '<%= Environment.MachineName %>';

aspx/wwwroot/password.aspx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<%-- This page should only be used in the version of the app built with vite. --%>
2+
<%@ Page Language="C#" AutoEventWireup="true"%>
3+
<%@ Register Src="~/lib/controls/AppRoot.ascx" TagName="AppRoot" TagPrefix="raweb" %>
4+
5+
<script runat="server">
6+
protected void Page_Load(object sender, EventArgs e)
7+
{
8+
// prevent client-side caching
9+
Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);
10+
Response.Cache.SetNoStore();
11+
Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(-1));
12+
13+
if (System.Configuration.ConfigurationManager.AppSettings["PasswordChange.Enabled"] == "false")
14+
{
15+
Response.StatusCode = 403;
16+
Response.Write("<h1>403 Forbidden</h3><p>Password change is disabled.</p>");
17+
Response.End();
18+
return;
19+
}
20+
}
21+
</script>
22+
23+
<raweb:AppRoot runat="server" />
24+
25+
<script type="module">
26+
const mainScript = document.createElement('script');
27+
mainScript.type = 'module';
28+
mainScript.src = '<%= ResolveUrl("~/lib/assets/password.js") %>';
29+
mainScript.crossOrigin = 'use-credentials';
30+
document.body.appendChild(mainScript);
31+
32+
const mainStylesheet = document.createElement('link');
33+
mainStylesheet.rel = 'stylesheet';
34+
mainStylesheet.href = '<%= ResolveUrl("~/lib/assets/password.css") %>';
35+
mainStylesheet.crossOrigin = 'use-credentials';
36+
document.head.appendChild(mainStylesheet);
37+
38+
const sharedScript = document.createElement('script');
39+
sharedScript.type = 'module';
40+
sharedScript.src = '<%= ResolveUrl("~/lib/assets/shared.js") %>';
41+
sharedScript.crossOrigin = 'use-credentials';
42+
document.body.appendChild(sharedScript);
43+
44+
const sharedStylesheet = document.createElement('link');
45+
sharedStylesheet.rel = 'stylesheet';
46+
sharedStylesheet.href = '<%= ResolveUrl("~/lib/assets/shared.css") %>';
47+
sharedStylesheet.crossOrigin = 'use-credentials';
48+
document.head.appendChild(sharedStylesheet);
49+
</script>

0 commit comments

Comments
 (0)