I am developing an ASP.NET MVC 4 application that utilizes impersonation (running as domain user), set in web.config and setup app pool to run as same user. Within the application we use a mix of windows authentication and a custom role provider to restrict access.
A new requirement has been brought about that requires querying data from a table locked down by AD groups. For security reasons, we cannot at our generic user to this table, or control it via custom roles. The only way our DBA will allow this data to leave it's respective home is when it is to one of the users within the AD groups assigned. So, I need to be able to make a call to an EF stored proc or Repo method as the current request's user.
I have tried something like the following (I hope this might clear up what I am attempting to do):
[HttpGet]
public ViewResult TestCall()
{
var lst = new List<Staged>();
//Get current request user
using (var person = ((WindowsIdentity) HttpContext.User.Identity))
{
//Impersonate said user
person.Impersonate();
//Make magics happen
using (var db = new TestEntities())
{
lst.AddRange(db.up_testseccall());
}
}
//Return magics, hopefully.
return View(lst.ToArray());
}
This code works fine locally, but fails miserably when tested remotely with a test account of mine. Any ideas or pointers would be greatly appreciated.
EDIT:
The issue I'm having now is that the DB is recieving the login as 'NT Authority\Anonymous Login'
EDIT 2:
Managed to receive a 'Safe handle has been closed' exception...so that's something new.
EDIT 3:
Current connection string
EDIT 4:
I gave up on this approach after a week of trying to get kerberos double-hop setup. I opted for a special domain service account for handling the required transactions.
<add name="TestEntities" connectionString="metadata=res://*/Models.TestModel.csdl|res://*/Models.TestModel.ssdl|res://*/Models.TestModel.msl;provider=System.Data.SqlClient;provider connection string="data source=DATABASENAME;initial catalog=CATALOG;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
Related
I have an ASP.NET 3.5 application that I recently extended with multiple membership and role providers to "attach" a second application within this application. I do not have direct access to the IIS configuration, so I can't break this off into a separate application directory.
That said, I have successfully separated the logins; however, after I login, I am able to verify the groups the user belongs to through custom role routines, and I am capable of having identical usernames with different passwords for both "applications."
The problem that I am running into is when I create a user with an identical username to the other membership (which uses web.config roles on directories), I am able to switch URLs manually to the other application, and it picks up the username, and loads the roles for that application. Obviously, this is bad, as it allows a user to create a username of someone who has access to the other application, and cross into the other application with the roles of the other user.
How can I mitigate this? If I am limited to one application to work with, with multiple role and membership providers, and the auth cookie stores the username that is apparently transferable, is there anything I can do?
I realize the situation is not ideal, but these are the imposed limitations at the moment.
Example Authentication (upon validation):
FormsAuthentication.SetAuthCookie(usr.UserName, false);
This cookie needs to be based on the user token I suspect, rather than UserName in order to separate the two providers? Is that possible?
Have you tried specifying the applicationName attribute in your membership connection string?
https://msdn.microsoft.com/en-us/library/6e9y4s5t.aspx?f=255&MSPPError=-2147217396
Perhaps not the answer I'd prefer to go with, but I was able to separate the two by having one application use the username for the auth cookie, and the other use the ProviderUserKey (guid). This way the auth cookie would not be recognized from one "application" to the other.
FormsAuthentication.SetAuthCookie(user.ProviderUserKey.ToString(), false);
This required me to handle things a little oddly, but it simply came down to adding some extension methods, and handling a lot of membership utilities through my own class (which I was doing anyhow).
ex. Extension Method:
public static string GetUserName(this IPrincipal ip)
{
return MNMember.MNMembership.GetUser(new Guid(ip.Identity.Name), false).UserName;
}
Where MNMember is a static class, MNMembership is returning the secondary membership provider, and GetUser is the standard function of membership providers.
var validRoles = new List<string>() { "MNExpired", "MNAdmins", "MNUsers" };
var isValidRole = validRoles.Intersect(uroles).Any();
if (isValidRole)
{
var userIsAdmin = uroles.Contains("MNAdmins");
if (isAdmin && !userIsAdmin)
{
Response.Redirect("/MNLogin.aspx");
}
else if (!userIsAdmin && !uroles.Contains("MNUsers"))
{
Response.Redirect("/MNLogin.aspx");
}...
Where isAdmin is checking to see if a subdirectory shows up in the path.
Seems hacky, but also seems to work.
Edit:Now that I'm not using the username as the token, I should be able to go back to using the web.config for directory security, which means the master page hack should be able to be removed. (theoretically?)
Edit 2:Nope - asp.net uses the username auth cookie to resolve the roles specified in the web.config.
I want to use ASP.NET SimpleMembership to authenticate users that consume my WebAPI. Thinktecture has a wonderful authentication library called Thinktecture.IdentityModel (http://thinktecture.github.com/Thinktecture.IdentityModel.45/) with an example that ties Forms Auth with Basic Auth (source). However, the example uses Membership.ValidateUser() which doesn't work without a ASP.NET Membership provider, which isn't supported by SimpleMembership (source) (edit: this isn't entirely true, see Mark's answer below).
Edit:
Here's what I did:
1) Create a new MVC Internet Application
2) Install Thinktecture.IdentityModel via NuGet
3) Create a model and an api controller via scaffolding:
public class Goober
{
public int GooberId { get; set; }
public string GooberWords { get; set; }
}
4) Ran the project, created a new user and created a new Goober using Fiddler
5) Added [Authorize] to GetGoober(int id)
6) In WebApiConfig.cs added:
var authConfig = new AuthenticationConfiguration();
authConfig.AddBasicAuthentication((userName, password) =>
Membership.ValidateUser(userName, password));
config.MessageHandlers.Add(new AuthenticationHandler(authConfig));
When I run the project and hit api/goober/1 with Fiddler I get a 401 www-Authenticate: unspecified. But if I log in first using the AccountController then use Fiddler I get a 200 and everything is peachy.
Edit
Okay, I think the problem isn't related to my initial question. I suspect it's related to the initialization of SimpleMembership in the template. When I open the project and run debug then hit the api with Fiddler I can't get past Auth. But when I simply click the "register" link on the web frontend I get past Auth. I'm guessing it's because the InitializeSimpleMembershipAttribute is called at the AccountController so doesn't initialize until the controller is called?
I've tried using WebSecurity.Login() in the place of Membership.ValidateUser() but that doesn't work.
I'm at a loss on how to actually implement this. Does anyone have any advice? Or maybe I'm attempting to tackle this problem from the wrong angle?
You are correct that ASP.NET Membership provider is not compatible with the SimpleMembershipProvider however SimpleMembershipProvider does support ValidateUser, see here. Assuming SimpleMembership is correctly configured and initalised you should still be able to call Membership.ValidateUser().
If you have already tried Membership.ValidateUser() and got an error please let me know and we can try resolve it.
Update
So having followed your reproduction steps I have managed to pin-point an error. Having brought the Thinktecture AuthenticationHandler handler inline and run in debug. 30 seconds after a request to the api controller a database connection error is being raised. This is failing asynchronously and silently.
After some fiddling around I believe it is the DefaultConnection connection string which is at fault.
Like me your default connection probably contains a file name like this:
AttachDBFilename=|DataDirectory|\aspnet-MvcApplication3-20121215104323.mdf"
When ValidateUser is called inside the delegate registered at app start up (for validating the credentials) it appears to be failing to resolve |DataDirectory| I found that by updating this path to the full name my connection problems went away.
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-MvcApplication3-20121215104323;Integrated Security=SSPI;AttachDBFilename=C:\mydatabase\file\path\example\aspnet-MvcApplication-20121215104323.mdf" providerName="System.Data.SqlClient" />
</connectionStrings>
I then found this post here, it indicates that the AppDomain has not had it's data directory set correctly at this point.
So once the config set up and the connection string altered with a proper file path and a user name "test" and a password "testing" this request through fiddler got a 200:
GET http://localhost/api/goober/1
User-Agent: Fiddler
Host: localhost
Authorization: Basic dGVzdDp0ZXN0aW5n
As an aside
I found that to get the forms authorisation token to also allow access to the api controllers I had to add this. Otherwise the Thinkteckture code sets the principle back to anonymous:
Add this
authConfig.InheritHostClientIdentity = true;
To counteract this (line 52):
if (_authN.Configuration.InheritHostClientIdentity == false)
{
//Tracing.Information(Area.HttpAuthentication, "Setting anonymous principal");
SetPrincipal(Principal.Anonymous);
}
I'm using ASP.NET Entity Framework's Code First to create my database from the model, and the login seems to fail when the database needs to be recreated after the model changes.
In Global.asax, I've got the following:
protected void Application_Start()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EntriesContext>());
// ...
}
In my controller, I've got the following:
public ActionResult Index()
{
// This is just to force the database to be created
var context = new EntriesContext();
var all = (from e in context.Entries select e).ToList();
}
When the database doesn't exist, it is created with no problems. However, when I make a change to the model, rebuild and refresh, I get the following error:
Login failed for user 'sa'.
My connection string looks like this:
<add name="EntriesContext"
connectionString="Server=(LOCAL);Database=MyDB;User Id=sa;Password=password"
providerName="System.Data.SqlClient" />
The login definitely works as I can connect to the server and the database from Management Studio using these credentials.
If I delete the database manually, everything works correctly and the database is recreated as expected with the schema reflecting the changes made to the model.
It seems like either the password or access to the database is being lost.
Is there something else I need to do to get this working?
It seems like Code First has a problem using the password specified in the connection string when connecting to a database that has been recreated. Changing this to use a trusted connection gets around the problem as the password no longer needs to be stored.
So, instead of this:
Server=(LOCAL);Database=MyDB;User Id=sa;Password=password
Use the following instead
Server=(LOCAL);Database=MyDB;Trusted_Connection=true
You may need to add your account or the one being used by ASP.NET to SQL Server and grant it the 'dbcreator' permission so that it can drop and recreate the database.
I believe using sa may be an issue. Either way I believe it is good practice to create a separate user profile to use in your connection string.
We have a lot of domains running on one IIS WebSite/AppPool.
Right now we are in the process of implementing SSO with Windows Identity Foundation.
in web.config the realm has to be set with
<wsFederation passiveRedirectEnabled="true" issuer="http://issuer.com" realm="http://realm.com" requireHttps="false" />
My problem is that the realm is dependent on which domain the user accessed the website on
so what I did is that I set it in an global action filter like this
var module = context.HttpContext.ApplicationInstance.Modules["WSFederationAuthenticationModule"] as WSFederationAuthenticationModule;
module.Realm = "http://" + siteInfo.DomainName;
My question is. When I set the realm like this, is it set per user instance
or application instance.
Scenario.
User A loads the page and the realm get set to domain.a.com.
User B is already logged in on domain.b.com and presses login.
Since user A loaded the page before User B pressed login, user A will hit the STS
with the wrong realm set.
What will happen here?
If this is not the way to set the realm per user instance, is there another way to do it?
I have already solved the problem.
I set PassiveRedirectEnabled to false in web.config
I set up the mvc project to use forms authentication, eventhough I dont.
I do that so that I will get redirected to my login controller with a return url everytime a controller with [Authorize] is run.
In my login controller I do
var module = HttpContext.ApplicationInstance.Modules["WSFederationAuthenticationModule"] as WSFederationAuthenticationModule;
module.PassiveRedirectEnabled = true;
SignInRequestMessage mess = module.CreateSignInRequest("passive", returnUrl, false);
mess.Realm = "http://" + Request.Url.Host.ToLower();
HttpContext.Response.Redirect(mess.WriteQueryString());
This is definitely not really how it should be, for me it feels like Windows Identity Foundation is lagging behind, both in documentation and microsoft technology wise, no examples for MVC.
For other MVC people i recommend them to not use the fedutil wizard, and instead write the code and configuration themself
I have a classic ASP page - written in JScript - that's using Scripting.FileSystemObject to save files to a network share - and it's not working. ("Permission denied")
The ASP page is running under IIS using Windows authentication, with impersonation enabled.
If I run the following block of code locally via CScript.exe:
var objNet = new ActiveXObject("WScript.Network");
WScript.Echo(objNet.ComputerName);
WScript.Echo(objNet.UserName);
WScript.Echo(objNet.UserDomain);
var fso = new ActiveXObject("Scripting.FileSystemObject");
var path = "\\\\myserver\\my_share\\some_path";
if (fso.FolderExists(path)) {
WScript.Echo("Yes");
} else {
WScript.Echo("No");
}
I get the (expected) output:
MY_COMPUTER
dylan.beattie
MYDOMAIN
Yes
If I run the same code as part of a .ASP page, substituting Response.Write for WScript.Echo I get this output:
MY_COMPUTER
dylan.beattie
MYDOMAIN
No
Now - my understanding is that the WScript.Network object will retrieve the current security credentials of the thread that's actually running the code. If this is correct - then why is the same user, on the same domain, getting different results from CScript.exe vs ASP? If my ASP code is running as dylan.beattie, then why can't I see the network share? And if it's not running as dylan.beattie, why does WScript.Network think it is?
Your problem is clear. In the current implementation you have only impersonation of users and no delegation. I don't want to repeat information already written by Stephen Martin. I only want to add at least three solutions. The classical way of delegation which Stephen Martin suggests is only one way. You can read some more ways here: http://msdn.microsoft.com/en-us/library/ff647404.aspx#paght000023_delegation. I see three practical ways of you solving your problem:
Convert the impersonation token of the user to a token with delegation level of impersonation or to a new primary token. You can do this with respect of DuplicateToken or DuplicateTokenEx.
Use S4U2Self (see http://msdn.microsoft.com/en-us/magazine/cc188757.aspx and http://msdn.microsoft.com/en-us/library/ms998355.aspx) to receive a new token from the old one with respect of one simple .NET statement WindowsIdentity wi = new WindowsIdentity(identity);
You can access another server with respect of one fixed account. It can be a computer account on an account of the application pool of the IIS. It can be another fixed defined account which one will only use for access to the file system.
It is important to know which version of Windows Server you have on the server where IIS is running and which Domain Function Level you have in Active Directory for your Domain (you see this in "Active Directory Domain and Trusts" tool if you select your domain and choose "Raise Domain Functional Level"). It is also interesting to know under which account the application pool of the IIS runs.
The first and the third way will always work. The third way can be bad for your environment and for the current permission in the file system. The second one is very elegant. It allows control of which servers (file server) are accessed from IIS. This way has some restrictions and it needs some work to be done in Active Directory.
Because you use classic ASP, a small scriptable software component must be created to support your implementation.
Which way do you prefer?
UPDATED based on the question from comment: Because you use classic ASP you can not use a Win32 API directly, but you can write a small COM component in VB6 or in .NET which use APIs which you need. As an example you can use code from http://support.microsoft.com/kb/248187/en. But you should do some other things inside. So I explain now which Win32 API can help you to do everything what you need with tokens and impersonation.
First of all a small explanation about impersonation. Everything works very easy. There are always one primary token under which the process runs. To any thread another token (thread token) can be assigned. To do this one needs to have a token of a user hUserToken and call API ImpersonateLoggedOnUser(hUserToken);.
To go back to the original process token (for the current thread only) you can call RevertToSelf() function. The token of user will be received and already impersonated for you by IIS, because you so configured your Web Site. To go back to the original process token you should implement calling of the function RevertToSelf() in your custom COM component. Probably, if you need to do nothing more in the ASP page, it will be enough, but I recommend you be more careful and save current users token in a variable before operation with files. Then you make all operations with file system and at the end reassign users token back to the current thread. You can assign an impersonation token to a thread with respect of SetThreadToken(NULL,hUserToken);. To give (save) current thread token (user token in your case) you can use OpenThreadToken API. It must work.
UPDATED 2: Probably the usage of RevertToSelf() function at the end of one ASP page would be already OK for you. The corresponding C# code can be so:
Create a new Project in C# of the type "Class Library" with the name LoginAdmin. Paste the following code inside
using System;
using System.Runtime.InteropServices;
namespace LoginAdmin {
[InterfaceTypeAttribute (ComInterfaceType.InterfaceIsDual)]
public interface IUserImpersonate {
[DispId(1)]
bool RevertToSelf ();
}
internal static class NativeMethods {
[DllImport ("advapi32.dll", SetLastError = true)]
internal static extern bool RevertToSelf ();
}
[ClassInterface (ClassInterfaceType.AutoDual)]
public class UserImpersonate : IUserImpersonate {
public UserImpersonate () { }
public bool RevertToSelf () {
return NativeMethods.RevertToSelf();
}
}
}
Check in project properties in "Build" part "Register for COM interop". In "Signing" part of the project check Sign the assembly and in "Choose a strong name key file" choose <New...>, then type any filename and password (or check off "protect my key..."). At the end you should modify a line from AssemblyInfo.cs in Properties part of the project:
[assembly: ComVisible (true)]
After compiling this project you get two files, LoginAdmin.dll and LoginAdmin.tlb. The DLL is already registered on the current computer. To register if on the other computer use RegAsm.exe.
To test this COM DLL on a ASP page you can do following
<%# Language="javascript" %>
<html><body>
<% var objNet = Server.CreateObject("WScript.Network");
Response.Write("Current user: ");Response.Write(objNet.UserName);Response.Write("<br/>");
Response.Write("Current user's domain: ");Response.Write(objNet.UserDomain);Response.Write("<br/>");
var objLoginAdmin = Server.CreateObject("LoginAdmin.UserImpersonate");
var isOK = objLoginAdmin.RevertToSelf();
if (isOK)
Response.Write("RevertToSelf return true<br/>");
else
Response.Write("RevertToSelf return false<br/>");
Response.Write("One more time after RevertToSelf()<br/>");
Response.Write("Current user: ");Response.Write(objNet.UserName);Response.Write("<br/>");
Response.Write("Current user's domain: ");Response.Write(objNet.UserDomain);Response.Write("<br/>");
var fso = Server.CreateObject("Scripting.FileSystemObject");
var path = "\\\\mk01\\C\\Oleg";
if (fso.FolderExists(path)) {
Response.Write("Yes");
} else {
Response.Write("No");
}%>
</body></html>
If the account used to run the IIS application pool has access to the corresponding network share, the output will be look like following
Current user: Oleg
Current user's domain: WORKGROUP
RevertToSelf return true
One more time after RevertToSelf()
Current user: DefaultAppPool
Current user's domain: WORKGROUP
Yes
Under impersonation you can only access securable resources on the local computer you cannot access anything over the network.
On Windows when you are running as an impersonated user you are running under what is called a Network token. This token has the user's credentials for local computer access but has no credentials for remote access. So when you access the network share you are actually accessing it as the Anonymous user.
When you are running a process on your desktop (like CScript.exe) then you are running under an Interactive User token. This token has full credentials for both local and remote access, so you are able to access the network share.
In order to access remote resources while impersonating a Windows user you must use Delegation rather then Impersonation. This will involve some changes to your Active directory to allow delegation for the computer and/or the users in your domain. This can be a security risk so it should be reviewed carefully.