Assign User to IIS AppPool via Powershell - iis-7

I tried this code and it appears to work fine. However, i noticed if you assign the username and password to an account that does not exist the code continues without issue. In addition, if you assign an invalid account and call stop() and then start() the IIS pool indeed stops and starts!! Furthermore, when I go to InetMgr and start,stop or recylce the pool it also stops and starts without complaining!
I was hoping that adding an invalid account would throw an error effectively allowing me to test the validity of an account. Why does it behave this way?
$loginfile = "d:\temp\Logins.csv"
$csv = Import-Csv -path $loginfile
ForEach($line in $csv){
$poolid = "MyDomain\" + $line.Login;
Write-Host "Assigning User to Pool:" $poolid;
$testpool = get-item iis:\apppools\test;
$testpool.processModel.userName = $poolid;
$testpool.processModel.password = $line.Pwd;
$testpool.processModel.identityType = 3;
$testpool | Set-Item
$testpool.Stop();
$testpool.Start();
Write-Host "IIS Recycled";
$testpool = get-item iis:\apppools\test;
write-host "New Pool User: " $testpool.processModel.userName;
write-host "New Pool PWd: " $testpool.processModel.password;
}

You should always validate your credentials before setting the pool identity. This can be accomplished via the PrincipalContext .NET class -- specifically look at PrincipalContext.ValidateCredentials(user, password).
Sample:
#-- Make sure the proper Assembly is loaded
[System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement") | out-null
#-- Code to check user credentials -- put in function but here are the guts
#-- Recommend you use SecureStrings and convert where needed
$ct = [System.DirectoryServices.AccountManagement.ContextType]::Domain
$pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $ct,"domainname"
$isValid = $pc.ValidateCredentials("myuser","mypassword")
If local account change the $ct to 'Machine' ContextType.

Start and Stop are something of a misnomer. They should really be named Enable and Disable.
The worker process for the pool won't actually "start" until it needs to service a request.
It's at that point authentication takes place. If the username and password are invalid then you'll get a 503 Service Unavailable response and three events (5021, 5057 and 5059) logged by the WAS in the System event log.
There is no up-front checking of the validity of a pool's identity when using the API's. Only the IIS management console performs these checks.

Related

Server 2016 AD and IIS Express Cannot Set User Password but Can Create User

I had to create a new dev environment with less VMs as my IDE kept crashing, so I created a VM with AD and IIS on the same server.
I was using the following code fine in my old environment:
PrincipalContext ctx = new PrincipalContext(ContextType.Domain,
Environment.GetEnvironmentVariable("DOMAIN"),
Environment.GetEnvironmentVariable("USER_OU"),
Environment.GetEnvironmentVariable("SERVICE_USERNAME"),
Environment.GetEnvironmentVariable("SERVICE_PASSWORD"));
UserPrincipalEx usr = new UserPrincipalEx(ctx);
usr.Name = ticket.FirstName + " " + ticket.LastName;
usr.SamAccountName = ticket.Username;
usr.GivenName = ticket.FirstName;
usr.Surname = ticket.LastName;
usr.DisplayName = ticket.FirstName + " " + ticket.Account.LastName;
usr.UserPrincipalName = ticket.Username + "#" + Environment.GetEnvironmentVariable("DOMAIN");
usr.Enabled = enabled;
try
{
usr.Save();
usr.SetPassword(temppwd);
usr.ExpirePasswordNow();
}
I can still save the user and it appears in AD, however SetPassword no longer works:
[IIS EXPRESS] Request started: "POST" https://localhost:5001/create
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.UnauthorizedAccessException: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
--- End of inner exception stack trace ---
at System.DirectoryServices.DirectoryEntry.Invoke(String methodName, Object[] args)
at System.DirectoryServices.AccountManagement.SDSUtils.SetPassword(DirectoryEntry de, String newPassword)
at System.DirectoryServices.AccountManagement.ADStoreCtx.SetPassword(AuthenticablePrincipal p, String newPassword)
at System.DirectoryServices.AccountManagement.PasswordInfo.SetPassword(String newPassword)
at System.DirectoryServices.AccountManagement.AuthenticablePrincipal.SetPassword(String newPassword)
My service account is a Domain Admin and I get the same error if I try my own AD creds.
I have tired calling SetPassword() before Save() but it fails at the same point.
The only difference is that I have AD and IIS on the same server. I have tired both Jetbrains Rider and VS2019. I am getting very close to my project deadline and I am really stuck.
None of the users have 'User Cannot Change Password Set' and the new users don't have any options under 'Account Options' set.
SetPassword sets the unicodePwd attribute. That has some restrictions on when it can be updated. The documentation for that says:
Windows 2000 operating system servers require that the client have a 128-bit (or better) SSL/TLS-encrypted connection to the DC in order to modify this attribute. On Windows Server 2003 operating system and later, the DC also permits modification of the unicodePwd attribute on a connection protected by 128-bit (or better) Simple Authentication and Security Layer (SASL)-layer encryption instead of SSL/TLS.
It should setup a secure connection by default (it does for me), but it's possible that it can't in your setup for whatever reason.
You can pass a ContextOptions object in the constructor to your PrincipalContext. By default that is automatically set to ContextOptions.Negotiate | ContextOptions.Signing | ContextOptions.Sealing, which should be secure. But ContextOptions.Negotiate uses "either Kerberos or NTLM", and ContextOptions.Signing (the encryption) depends on Kerberos. So maybe it's falling back to NTLM and can't encrypt.
You might be able to confirm this by inspecting these values, after you create the account:
Console.WriteLine(ctx.Options);
Console.WriteLine(((DirectoryEntry) usr.GetUnderlyingObject()).AuthenticationType);
The values you'd be looking for are:
Negotiate, Signing, Sealing
Secure, Signing, Sealing
That's what I have when SetPassword works. But I'm not sure if it actually changes those values if it falls back to NTLM. Sometimes it does that pretty silently.
In any case, if Kerberos isn't happening, you can either troubleshoot that, or attempt to connect via LDAPS (LDAP over SSL). That would look something like this:
PrincipalContext ctx = new PrincipalContext(ContextType.Domain,
Environment.GetEnvironmentVariable("DOMAIN") + ":636",
Environment.GetEnvironmentVariable("USER_OU"),
ContextOptions.Negotiate | ContextOptions.SecureSocketLayer,
Environment.GetEnvironmentVariable("SERVICE_USERNAME"),
Environment.GetEnvironmentVariable("SERVICE_PASSWORD"));
But that can cause other issues since your DC needs to have a certificate that you trust.

Email notification from BizTalk for failed messages & error from event viewer

enter image description herePlease let me know how to get email notification whenever an message fails in BizTalk and also whenever there is error in event viewer.
I think this depends on your solution. Within the solution I have developed, I have identified most points of failure on message routing, due to mistransformations, missing fields etc. These are then routed to an Orchestration which specifically only sends SMTP emails to set addresses. This works fairly well for the requirements of my company. - you can find plenty of SMTP Orchestration examples with a quick google.. I started here
In tandem with this - for the unknowns, I have also set up a powershell script to email out the last message of the Windows Log Event Viewer. I have created a custom viewer using the BizTalk Administrative console..
<QueryList>
<Query Id="0" Path="Application">
<Select Path="Application">*[System[Provider[#Name='BizTalk DW Reporting' or #Name='BizTalk Server' or #Name='BizTalk Server Deployment' or #Name='BizTalk Server EDI' or #Name='ENTSSO' or #Name='XLANG/s'] and (Level=1 or Level=2)]]</Select>
</Query>
</QueryList>
and then exported that out into a Windows Task Schedule, that triggers a powershell script whenever it detects that there is a new entry that lands in the custom viewer.
I have followed the rough principles provided here for the powershell script.
Hope this points you in the right direction for what you need. There are probably better solutions, but this works fairly well.
This is the powershell script I am using
$event = get-eventlog -LogName Application -Source "XLANG/s","BizTalk Server","BizTalk DW Reporting","BizTalk Server Deployment","BizTalk Server EDI","ENTSSO" -EntryType "Error" -newest 1
#get-help get-eventlog will show there are a handful of other options available for selecting the log entry you want.
$eventtime = $event.TimeGenerated
#ignore any messagebox errors
if (($event.EntryType -eq "Error" -and $event.EventID -inotin 6998, 10514))
{
$Source = $event.Source
$PCName = $env:COMPUTERNAME
$EmailBody = "$Source Error captured at " + $event.TimeGenerated + " in windows error log on BizTalk-UAT server: `n`n" + $event.Message
$EmailFrom = "????-BizTalk-UAT#???.com"
$EmailTo = #('????-BizTalk-UAT#???.com')
$EmailSubject = "BizTalk-UAT Server - Windows Log - " + $event.EntryType
$SMTPServer = "mail.????.com"
Write-host "Sending Email" $EmailFrom "To" $EmailTo
Send-MailMessage -From $EmailFrom -To $EmailTo -Subject $EmailSubject -body $EmailBody -SmtpServer $SMTPServer
}
else
{
write-host "No error found"
write-host "Here is the log entry that was inspected:"
$event
}
The 'correct' way to do this is with a monitoring tool for Windows and other platforms.
First, as your network or server team if they have a tool such as SCOM or Splunk which they should be using to monitor the servers anyway. Then, you can configure any rule you/they want, including email.
In your BizTalk Apps, just be sure to create Windows Events (Event Viewer) in you exception handling code.

IIS using Microsoft.Web.Administration and commiting changes

goal
Change IIS bindings and swap virtual directories on a per request basis. A simple application will open, replace and commit, using the Microsoft.Web.Administration. Following is a simplified flow:
code
ServerManager = New ServerManager()
config = ServerManager.GetApplicationHostConfiguration()
SiteList = config.GetSection("system.applicationHost/sites")
SitesCollection = SiteList.GetCollection()
_site = SitesCollection.FirstOrDefault(Function(f) f.GetAttributeValue("name").ToString() = "XXX")
_bindings = _site.GetCollection("bindings")
_bind As ConfigurationElement = _bindings.CreateElement("binding")
_bind("protocol") = "http"
_bind("bindingInformation") = String.Format("*:80:{0}", "www.zzz.yyy")
_bindings.Add(_bind)
ServerManager.CommitChanges()
problem
This code run on an administrator authenticated web page, and before the call, the thread is impersonated to make sure the privileges are in place. I'm allowed to read the .config but not to write! I also confirmed that, before any call, the thread is running as Administrator. I also tried using a LocalService pool but again, no luck.
the error
Filename: \\?\C:\Windows\system32\inetsrv\config\applicationHost.config
Error: Cannot write configuration file due to insufficient permissions
hresult: 0x80070005
at Microsoft.Web.Administration.Interop.AppHostWritableAdminManager.CommitChanges()
After some checking and experiment I find out that the way to make it work was
Do not use authentication on the application.
Do not use impersonation.
LocalService does not work.
Run it on a pool with enough privileges (administrator)
Looks like using impersonation breaks it again.
This is NOT the setting I wanted but I think I can survive and move the app to this unsecure pool, work the changes and the move it back to a generic ApplicationPoolIdentity.

Active directory - exception has been thrown by the target of an invocation

I have a web application in a separate server than Active Directory and I want to change a user password. The code is the next:
string newPassword = Membership.GeneratePassword(int.Parse(WebConfigurationManager.AppSettings["passLenght"]),
int.Parse(WebConfigurationManager.AppSettings["passNonAlpha"]));
DirectoryEntry de = new DirectoryEntry(WebConfigurationManager.ConnectionStrings["ADConnString"].ConnectionString,
WebConfigurationManager.AppSettings["ADAdmin"], WebConfigurationManager.AppSettings["ADAdminPass"]);
DirectorySearcher deSearch = new DirectorySearcher(de);
deSearch.Filter = "(&(objectClass=user) (userPrincipalName=" + name + "))";
SearchResultCollection results = deSearch.FindAll();
if (results.Count == 1)
{
foreach (SearchResult OneSearchResult in results)
{
DirectoryEntry AlterUser = OneSearchResult.GetDirectoryEntry();
AlterUser.AuthenticationType = AuthenticationTypes.Secure;
AlterUser.Invoke("SetPassword", newPassword);
AlterUser.CommitChanges();
AlterUser.Close();
}
}
When I run this in my development environment (where Active Directory and the web application are on the same server) it is working. But when I try to run it in the production environment I am having the next error:
Exception has been thrown by the target of an invocation
What am I missing?
Thanks.
EDIT:
I could go deep in the exception error and I get this:
Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
Permissions are the issue. The account under which your ASP.NET code is running doesn't have the permission to set the account password.
Either:
Run the AppPool under a user that has the required permissions, or
Use impersonation to elevate the permissions for the SetPassword call
The reason it is working in your dev environment/failing in production is likely due to a combination of:
You are running the app under the Visual Studio development web server that runs under your user account, which has the necessary permissions. Running it under "real" IIS will run it under a less privileged account.
In the live environment there's another machine hop from the web server to the AD server, and the credentials don't get passed along. The web server needs to have network credentials (either as part of the AppPool identity, or a call to LogonUser) in order to authenticate to AD.
The code looks correct. This could be happening because the password your sending though Active Directory does not meet the minimum requirements. Trying using a more complex password such as "M2k3ThisWork!"
If you want to change the password of AD then you use this
AlterUser.Invoke("ChangePassword", OldPassword, newPassword);

Certificate stored in cert store disappearing from ASP.NET application over time

I've got a code signing certificate from Thawte that I'm using to dynamically generate and sign ClickOnce deployment manifests. The problem is that the ASP.NET application that generates and signs these manifests works fine when we restart IIS or re-import the signing certificate, but over time the following function fails to find the certificate:
private static X509Certificate2 GetSigningCertificate(string thumbprint)
{
X509Store x509Store = new X509Store(StoreLocation.CurrentUser);
try
{
x509Store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection x509Certificate2Collection = x509Store.Certificates.Find(
X509FindType.FindByThumbprint, thumbprint, false);
if (x509Certificate2Collection.Count == 0)
throw new ApplicationException(
"SigningThumbprint returned 0 results. Does the code signing certificate exist in the personal store?",
null);
if (x509Certificate2Collection.Count > 1)
throw new ApplicationException(
"SigningThumbprint returned more than 1 result. This isn't possible", null);
var retval = x509Certificate2Collection[0];
if(retval.PrivateKey.GetType() != typeof(RSACryptoServiceProvider))
throw new ApplicationException("Only RSA certificates are allowed for code signing");
return retval;
}
finally
{
x509Store.Close();
}
}
Eventually the application starts throwing errors that it can't find the certificate. I'm stumped because I think the cert is installed correctly (or mostly correct) because it does find the cert when we start the ASP.NET application, but at some point we hit the Count==0 branch and it's just not true: the cert is in the application pool user's "Current User\Personal" cert store.
Question: Why might a cert all of a sudden "disappear" or not be able to be found?
Figured it out on our own (painfully).
The certificate needed to be installed in the LocalMachine store and the application pool account read permissions to the cert using WinHttpCertCfg or CACLS.exe if it's going to be used from an ASP.NET application. Using the CurrentUser store of the account running the application pool was causing the problem. I'm guessing there's some sort of race condition or something that's not entirely cool about accessing the CurrentUser store from a user that isn't running in a interactive logon session.
We were unable to do this at first because we were invoking the MAGE tool to do the ClickOnce deployment manifest creation/signing, and that requires the code signing cert to be in the CurrentUser\My store. However, we've eliminated the need for MAGE by a) creating the manifest from a template file and replacing the values we need to substitute out and b) by signing the manifest by calling the code MAGE calls via reflection that exists in the BuildTasks.v3.5 DLL. As a result we have more control over what cert we use to sign the manifest and can put it wherever we want. Otherwise we'd be stuck had we not gone a little "lower level".
We had the same issue, and it turns out the application pool needed to be set to load the the user profile of the (domain) account that we use to run the application.
If you notice that accessing the My certificate store works while you're signed in with the application pool's user, but not if you were not signed in while the apppool spun up, then this might be the cause for you, too.

Resources