I'm having a lot of problems that whenever I call an ASP.Net Identity Async method I get access denied exceptions from SQL server.
The following returns a user:
var user = (from u in ctx.Users
where u.Id == 1
select u).FirstOrDefault();
Whereas
var user = await userManager.FindByIdAsync(1);
Triggers an exception
System.Data.SqlClient.SqlException: Login failed for user 'DOMAIN\MACHINE$'.
What seems to be happening is we have the following line in our web.config configuration/system.web section
<identity impersonate="true" userName="DOMAIN\Domain User" password="xxx" />
This impersonated user has permission to access the SQL Server database but the application pool user does not. It seems that whenever I call an async method in ASP.Net Identity, it falls back to the app pool user and loses the impersonation.
I did find a similar sounding question with an explanation here https://stackoverflow.com/a/30876110/1093406
Could this also be causing my problem?
Is there any way around this apart from changing the app pool user to one that has database access?
Is using web.config to set an impersonation user an old way of doing things and now bad practice?
Edit: After further investigation I've found these articles
http://www.hanselman.com/blog/AvoidUsingImpersonationInASPNET.aspx
http://blog.codeishard.net/2012/09/17/await-async-mvc-and-impersonation/
Looks as though using the impersonation is a bad idea unless someone can tell me otherwise.
Impersonation is no longer supported in Integrated Pipeline modes. Especially when using async methods.
The reason is that Impersonation happens on the thread, and when you call an async function, it may actually execute or return on a different thread.
You should use a WindowsImpersonationContext to wrap your database calls.
https://msdn.microsoft.com/en-us/library/system.security.principal.windowsimpersonationcontext(v=vs.110).aspx
using System.Security.Principal;
...
//Get the identity of the current user
IIdentity contextId = HttpContext.Current.User.Identity;
WindowsIdentity userId = (WindowsIdentity)contextId;
//Temporarily impersonate
using (WindowsImpersonationContext imp = userId.Impersonate())
{
//Perform tasks using the caller's security context
DoSecuritySensitiveTasks();
}
Make sure that you do this in a using block, since if an uncaught exception occurs in your code, you would end up not restoring the original context, and creating a security issue.
My solution in the end was to set the user on the App Pool rather than using Impersonation.
This seems to have exactly the same effect as setting impersonation in the Web.config with equivalent security, just without the problems when Async is being used.
As #Erik said in his answer, it seems that impersonation is no longer supported and according to http://www.hanselman.com/blog/AvoidUsingImpersonationInASPNET.aspx
it was never encouraged in the first place.
Related
I am using Identity Server 4 with the quickstart UI and a client using Cookie Authentication.
Lets say I have user A on machine A who is currently logged in via the browser. Then user A decides to go on machine B and logs into that one. As it stands, a new session cookie will be issued for user A on machine B as well as machine A.
This is fine, but I want the option to mark particular users with a flag e.g. IsConcurrent and if it is set to true, they will be given the option to either keep their existing session on machine A, or terminate it and start a new session on machine B.
I have done some reading and found references here to updating the security stamp for a user and setting the interval to zero, so it checks the security stamp in the cookie against the stored version. However, this code didn't seem to be inline with Identity Server's code. Also, it it a valid option in this case?
I also found here which mentions storing and checking the value of session IDs, but I'm not sure if this is valid either?
An initial idea was to implement some middleware that obtained the Machine ID and stored it in a table along with the user, but then I was unsure how to take something like this any further.
Any help or advice would be much appreciated.
Assuming cookie based authentication, you can extend the client to validate the user session provided that the client keeps track of user sessions.
For that you can create a session manager where you add a session for the user (sub) after login, this also includes automatic login sessions (SSO). Remove one or all sessions on logout, which should also be updated on back channel logout (LogoutCallback).
Assuming you use middleware, you can consult the session manager there and decide what to do. Make sure that the current session isn't already activated after login. It has to step through the middleware at least once. Some pseudo code to illustrate the idea:
public Task Invoke(HttpContext context, SessionManager sessionManager)
{
if (context.Principal.Identity.IsAuthenticated)
{
var sub = context.Principal.FindFirst("sub")?.Value;
var sid = context.Principal.FindFirst("sid")?.Value;
// User is allowed when the current session is active.
if (!sessionManager.CurrentSessionIsActive(sub, sid))
{
// Rewrite path if user needs and is allowed to choose: redirect to session selector or
// Activate the current session and deactivate other sessions, if any.
if (sessionManager.HasMultipleSessions(sub) && sessionManager.CanSelectSession(sub))
context.Request.Path = new PathString("/SelectSession");
else
sessionManager.ActivateCurrentSession(sub, sid);
}
}
return _next(context);
}
On post of the SelectSession form you can mark in the session manager which sessions are active. If the old session should be preserved, then ignore the old session (remains active) and mark the current session as active.
Make sure to add the middleware after authenticating the user.
Please note that for access tokens you'll need a different strategy.
I am running the following code in an ASP.Net MVC website:
using (var ctx = new PrincipalContext(ContextType.Domain, DOMAIN))
using (var userPrincipal = UserPrincipal.FindByIdentity(ctx, principal.Identity.Name))
using (var groups = userPrincipal.GetAuthorizationGroups())
The code is used a custom RoleProvider and the variable "principal" is HttpContext.Current.User. DOMAIN is my local domain.
This code works fine when I log in to the site, but throws an exception on the last line if I log in as either of a couple of test users.
While trying to retrieve the authorization groups, an error (5) occurred.
The test users have fewer permissions on the domain but are users in active directory. I tried creating the PrincipalContext with an alternative constructor supplying my username and password but it didn't make any difference to the behaviour.
This happens both on my dev machine and also on a separate staging server so I don't think it has anything to do with local permissions. I also don't see how it can be related to the users' permissions as I would expect the active directory request to be made as the IIS App Pool user (impersonation is turned off) or perhaps the user specified in the PrincipalContext constructor.
Any suggestions as to what is going on gratefully received.
It's not enough that they are users in Active Directory. The IIS App Pool user needs to be a member of the Windows Authorization Access Group to execute GetAuthorizationGroups.
Answered here: While trying to retrieve the authorization groups, an error (5) occurred
I'm having trouble impersonating logged on user and then access unc files. I have tried using this sample code:
using System.Security.Principal;
...
// Obtain the authenticated user's Identity
WindowsIdentity winId = (WindowsIdentity)HttpContext.Current.User.Identity;
WindowsImpersonationContext ctx = null;
try
{
// Start impersonating
ctx = winId.Impersonate();
// Now impersonating
// Access resources using the identity of the authenticated user
}
// Prevent exceptions from propagating
catch{}
finally
{
// Revert impersonation
if (ctx != null)
ctx.Undo();
}
// Back to running under the default ASP.NET process identity
If I try to access a file locally where the comment says Access resources using the identity of the authenticated user it works exactly as it should. If I however try to do the same thing with a file on a file server somewhere using UNC like \\ServerName\Share\FileName.txt it doesn't matter that the impersonated account has enough rights. The application throws an exception saying that the ASP.NET account does not have enough rights.
I have also tried to use unmanaged code to perform the impersonation and then it works! Local file or UNC, doesn't matter, works like a charm!
The problem is that you have to provide password and since it is the logged on users rights I want to check I can't provide that.
Does anyone know why the application behaves like this? Is it some configuration setting I need to set?
Web application runs with specific
identity, this identity is based on a
user account on the local machine or
domain.
The application uses this identity
when it accesses resources on disk or
services.
If the account does not have rights to
the resource, the web application will
not be able to use the resource.
Impersonation is where the web
application assumes a different
identity from the default
Impersonation can be configured to be
used for the web application on
startup, by adding an tag
to the web.config file. Impersonation
can also be implemented dynamically in
code, so that it can be turned on and
off as needed.
From ASP.NET Identity and Impersonation
So check you web.config and IIS Configuration and ensure that you're impersonating the correct user.
Thanks for reading and for your thoughts; this is a hairy problem, so I thought I'd share to see if it is actually a fair challenge for more seasoned developers than ourselves.
We're developing a web application for a corporate Microsoft Active Directory environment, and we use Windows Authentication provided by IIS to authenticate users for single-sign-on, alongside Forms Authentication. I know IIS complains when both are enabled, but it works very well, and every site we've deployed at has had no weird quirks to work around - until now.
The new site has "shared" machines, logged in permanently with a generic account that has read-only access to the applications they need to use. This means that we can't differentiate between users who should have different permissions to the application; we need some way of prompting the user for authentication details.
First try was some serious googling; nobody else in the world seemed to have our problem except for a few misguided souls who had asked questions into the ether and received no response.
After a bit of brainstorming and nutting out the way IIS's authentication works, it seemed that the most straightforward way to approach the problem was to issue a 401 Unauthorized in response to a user known to be a shared account. Initial tests here seemed fruitful, yielding successful changes of username inside the browser, however a prototype at the site did not prompt for credentials, and the browser kept the same account details. We also hit on the IE-specific javascript
document.execCommand("ClearAuthenticationCache")
which, again, worked in the lab but not onsite. Further experiments with IE security settings onsite revealed that the browser would automatically reauthenticate if the webapp site was excluded from the Intranet Zone, regardless of the method used to trick the browser into prompting the user for new account details.
Now we're stuck. We've got workaround options for getting it going on time, but they're definitely not the "right" answers:
require users to log out of the shared account before logging into our app (...yuck)
exclude our webapp from Intranet Zone on all machines
provide a non-SSO login service for users
I'm convinced that there's a canonical way to do this - a known pattern, a common base problem that's already been solved, something like that - and I'm very interested to hear what sort of inventive methods there are to solve this sort of problem, and if anyone else has actually ever experienced anything remotely like it.
We ended up settling on a solution that submits a query to the LDAP directory the server knows about. It means having to accept the user's password, but no other solution was solid enough to run in a production environment.
Hopefully this helps someone. .NET Framework 3.5+ required.
using System.DirectoryServices.AccountManagement;
private static bool IsLdapAuthenticated(string username, string password)
{
PrincipalContext context;
UserPrincipal principal;
try
{
context = new PrincipalContext(ContextType.Domain);
principal = Principal.FindByIdentity(context, IdentityType.SamAccountName, username) as UserPrincipal;
}
catch (Exception ex)
{
// handle server failure / user not found / etc
}
return context.ValidateCredentials(principal.UserPrincipalName, password);
}
Could you not create a page to which the shared accounts are denied access. Then do a redirect to that page, with a return URL encoded in the query string, at any point where you need the user to reauthenticate with a non-shared account? This should trigger the browser to put up the usual login dialog.
After the user reauthenticates, the new page should just redirect back to the return URL in the query string.
In .NET, there appears to be several ways to get the current Windows user name. Three of which are:
string name = WindowsIdentity.GetCurrent().Name;
or
string name = Thread.CurrentPrincipal.Identity.Name;
or
string name = Environment.UserName;
What's the difference, and why choose one method over the other? Are there any other ways?
Environment.UserName calls GetUserName within advapi32.dll. This means that if you're impersonating another user, this property will reflect that.
Thread.CurrentPrincipal has a setter and can be changed programmatically. (This is not impersonation btw.)
WindowsIdentity is your current windows identity, if any. It will not necessarily reflect the user, think ASP.NET with FormsAuthentication. Then the WindowsIdentity will be the NT-service, but the FormsIdentity will be the logged in user. There's also a PassportIdentity, and you can build your own stuff to complicate things further.
You asked for alternative ways.
Of course, you can always use the native Windows API: GetUserName.
I believe the property was put in several places so that it would be easier for the programmer to find. There's only one logged in user, and only one respective name.
The three methods are described as follow:
HttpContext = HttpContext.Current.User, which returns an IPrincipal object that contains security information for the current Web request. This is the authenticated Web client.
WindowsIdentity = WindowsIdentity.GetCurrent(), which returns the identity of the security context of the currently executing Win32 thread.
Thread = Thread.CurrentPrincipal which returns the principal of the currently executing .NET thread which rides on top of the Win32 thread.
And they change in result depending on your IIS configuration as explained in this article:
http://msdn.microsoft.com/en-us/library/aa302377.aspx