ASP.NET User Authentication across multiple projects - c#

I am building an ASP.NET UI on an existing system, which consists of separate SQL server databases for each project. An "enterprise" database lists all current projects which allows anonymous users to select the project to work in. The project name is stored in a session variable. When log in is required the username/password/roles etc are obtained from the database indicated by the project name. I have implemented my own basic membership and role providers to do this, with changes in web.config to specify the roles required for specific pages. (I do not use the standard ASP.NET Configuration tool to manage users, I have existing apps that work with my user tables).
This all seemed to work initially but I discovered that the session variables are not yet loaded at the time when the authorization system checks the roles the current user belongs to in order to determine if the page is accessible. So if we have a < allow roles="xxx" > in web.config then the authorization system fires before session data is loaded and thus before I know which project database should be used.
[Specifically: HttpContext.Current.Session is null when the call to RoleProvider.GetRolesForUser is made]
Anybody who has tackled this problem should know exactly what I'm talking about. My questions therefore are:
A) What is the "Best Practise" solution to this scenario?
B) Could I be storing the project name somewhere else (not in session variable) that is available during the authorization phase?
[Update: Yes - we can use cookies, assuming we do not require cookieless operation]
C) Is there a way to manually get the session variable at this earlier time?
I tried an option to cache roles in cookies, but after a few minutes of testing with that option on I found GetRolesForUsers was still being called.
Thanks
Update:
Here is another description of the root problem which suggests "The application could cache this information in the Cache or Application objects.":
http://connect.microsoft.com/VisualStudio/feedback/details/104452/session-is-null-in-call-to-getrolesforuser
Update:
This looks like the same problem found here:
Extending the RoleProvider GetRolesForUser()
Update:
There was a suggestion about using UserData in FormsAuthenticationTicket, but I require this data even when not logged on.

UPDATE: I solve this these days in a much simpler way by using a wildcard SSL certificate that allows me to configure subdomains for each project, thus the project selection is specified directly in the URL (and each project gets its own subdomain). I still use a cookie hack purely for testing purposes when running on localhost where we have no subdomains.
Original solution:
I have not found any "best practise" write up on this scenario, but here is what I have settled on:
1) In order to support anonymous users switching between projects (i.e. SQL databases) I simply use a session variable to track the project selection. I have a global property that uses this project selection to serve the corresponding SQL connection string as and when it is required.
2) In order to support the call to GetRolesForUser() on pages that have role restrictions applied to them we cannot use the session variable, because as stated the session variable has not been initialized yet when GetRolesForUser() is actually called (and I have found no way to force it into being at this early point in the request cycle).
3) The only option is to use a cookie, or use the Forms Authentication ticket's UserData field. I trawled through many theories about using session/cookie/IDs linked to an object stored in the application cache (which is available when the session is not) but ultimately the correct choice is to place this data in the authentication ticket.
4) If a user is logged on to a project it is via a ProjectName/UserName pair, hence anywhere we are tracking the user's authentication we require both these data. In trivial testing we can get away with the username in the ticket and the projectname in a separate cookie, however it is possible for these to get out of synch. For example if we use a session cookie for the projectname and tick "remember me" when we logon (creating a permanent cookie for the authentication ticket) then we can end up with a username but no projectname when the session cookie expires (browser is closed). Hence I manually add the project name to the UserData field of the authentication ticket.
5) I have not figured out how to manipulate the UserData field without explicitly setting a cookie, which means that my solution cannot work in "cookieless" session mode.
The final code turned out to be relatively simple.
I override the Authenticate event of the LoginView in the login page:
//
// Add project name as UserData to the authentication ticket.
// This is especially important regarding the "Remembe Me" cookie - when the authentication
// is remembered we need to know the project and user name, otherwise we end up trying to
// use the default project instead of the one the user actually logged on to.
//
// http://msdn.microsoft.com/en-us/library/kybcs83h.aspx
// http://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.login.remembermeset(v=vs.100).aspx
// http://www.hanselman.com/blog/AccessingTheASPNETFormsAuthenticationTimeoutValue.aspx
// http://www.csharpaspnetarticles.com/2009/02/formsauthentication-ticket-roles-aspnet.html
// http://www.hanselman.com/blog/HowToGetCookielessFormsAuthenticationToWorkWithSelfissuedFormsAuthenticationTicketsAndCustomUserData.aspx
// http://stackoverflow.com/questions/262636/cant-set-formsauthenicationticket-userdata-in-cookieless-mode
//
protected void LoginUser_Authenticate(object sender, AuthenticateEventArgs e)
{
string userName = LoginUser.UserName;
string password = LoginUser.Password;
bool rememberMe = LoginUser.RememberMeSet;
if ( [ValidateUser(userName, password)] )
{
// Create the Forms Authentication Ticket
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
userName,
DateTime.Now,
DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes),
rememberMe,
[ ProjectName ],
FormsAuthentication.FormsCookiePath);
// Create the encrypted cookie
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
if (rememberMe)
cookie.Expires = DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes);
// Add the cookie to user browser
Response.Cookies.Set(cookie);
// Redirect back to original URL
// Note: the parameters to GetRedirectUrl are ignored/irrelevant
Response.Redirect(FormsAuthentication.GetRedirectUrl(userName, rememberMe));
}
}
I have this global method to return the project name:
/// <summary>
/// SQL Server database name of the currently selected project.
/// This name is merged into the connection string in EventConnectionString.
/// </summary>
public static string ProjectName
{
get
{
String _ProjectName = null;
// See if we have it already
if (HttpContext.Current.Items["ProjectName"] != null)
{
_ProjectName = (String)HttpContext.Current.Items["ProjectName"];
}
// Only have to do this once in each request
if (String.IsNullOrEmpty(_ProjectName))
{
// Do we have it in the authentication ticket?
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
FormsIdentity identity = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket = identity.Ticket;
_ProjectName = ticket.UserData;
}
}
}
// Do we have it in the session (user not logged in yet)
if (String.IsNullOrEmpty(_ProjectName))
{
if (HttpContext.Current.Session != null)
{
_ProjectName = (string)HttpContext.Current.Session["ProjectName"];
}
}
// Default to the test project
if (String.IsNullOrEmpty(_ProjectName))
{
_ProjectName = "Test_Project";
}
// Place it in current items so we do not have to figure it out again
HttpContext.Current.Items["ProjectName"] = _ProjectName;
}
return _ProjectName;
}
set
{
HttpContext.Current.Items["ProjectName"] = value;
if (HttpContext.Current.Session != null)
{
HttpContext.Current.Session["ProjectName"] = value;
}
}
}

Can't you postback the project selection to some page, add that selection to the session, then redirect to appropriate protected page, where auth will kick in and force login?
ASP.NET session doesn't get created in the form of a cookie until you place at least one item in it.

Related

Using ClaimsTransformer to set user permissions creates lengthy cookies and rejected by browsers

In our Aspnet core 1.1 app, we are using ClaimsTransformer to set permissions for the current user at the start of each request. It appears the cookie got bigger due to this and sometimes I see a warning in browser's console that the cookie was rejected as it was too long. Is it possible to add claims to identity without being passed down to the client through cookies? If not, is it possible to replace the cookie value with a Guid or some identifier and retain the lengthy cookie in the server itself or possibly DB?
This is how our code looks like
public class ClaimsTransformer : IClaimsTransformer
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var principal = context.Principal;
if (!principal.Identity.IsAuthenticated)
return principal;
var identity = (ClaimsIdentity)principal.Identity;
var db = context.Context.RequestServices.GetService<IDb>();
var entity = await db.Entity.FirstAsync(/*Add conditions here*/);
//This list is expected to grow with time.
identity.AddClaim(new Claim("CanManageAPIs", entity.CanManageAPIs.ToString()));
identity.AddClaim(new Claim("CanManageUsers", entity.CanManageUsers.ToString()));
identity.AddClaim(new Claim("CanManageSystems", entity.CanManageSystems.ToString()));
return principal;
}
}
//Configure method in Startup.cs
app.UseClaimsTransformation(new ClaimsTransformationOptions
{
Transformer = new ClaimsTransformer()
});
public static class ClaimsPrincipalExtensions
{
public static bool CanManageSystems(this ClaimsPrincipal principal)
{
var value = principal.GetClaimValue("CanManageSystems");
bool.TryParse(value, out bool result);
return result;
}
}
And use the claims in our views.
#if (User.CanManageSystems())
{
<!-- Render some HTML content here. -->
}
Update Sep 03
I am not able to reproduce the issue anymore with the same source code without any local changes. The length of the cookie with name .AspNetCore.Cookies remains constant at 3331 irrespective of the number of claims being added to the identity and they are not persisting either. I did see that earlier when I was able to reproduce the issue, there two more cookies with names .AspNetCore.Cookies1 and .AspNetCore.Cookies2. The only change that happened between then and now is a claim was removed by one of my colleagues in Azure AD B2C and I can't say for sure if that is the reason. Since I don't have access to that part of Azure, I will need to wait for him to get back to work on Tuesday ET to do some experimenting.
Update Sep 11
I was able to get back to this issue today and figured out what caused the problem. As I keep browsing through the pages, when I cross half way mark of cookie expiration (approx. 3mins out of 5mins I configured to test), a new cookie is issued. And this cookie includes all the claims that were added by ClaimsTransformer code above and persists across page requests. The claims added during the subsequent requests seem to not overwrite but instead add additional claims to the identity. So I updated my code to remove existing claim if detected and add new one instead. This solves my problem.
However I would like to know if this is the expected behavior and I am using the claims incorrectly. Or is it a bug that I stumbled upon?

On handling any event related to login / logout activity

Currently, my application can track whenever user has logged in or out. The logging out activity is saved to database because User simply click a button. However, I would be interested in how to implement something similar but when user's ticket is no longer valid, either because it's been removed or expired.
I would like to handle any event (if possible) that is fired whenever user is no longer valid.
Is it possible to implement anything like this?
Cheers!
EDIT
I found two solutions. One uses BaseController--the code below from Handle asp.net MVC session expiration. This is simply what the author claims the easiest and working solution. First, is it a good idea to inherit all conrollers from a custom controllers? Second, I have already customized my own [Authorize] attribute. I don't know how all this together is going to work, and three, can i still have access to the cookie of a logged user where I hold UserID and some other data such as UserRole? And also it is not triggered automatically but whenver user trying to access and cookie is expired. It could be 6 hours later.
public class BaseController : Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Check if session exists
if (filterContext.HttpContext.Session != null)
{
//if new session
if (filterContext.HttpContext.Session.IsNewSession)
{
string cookie = filterContext.HttpContext.Request.Headers["Cookie"];
//if cookie exists and session id index is greater than zero
if ((cookie != null) && (cookie.IndexOf("ASP.NET_SessionId") >= 0))
{
//redirect to desired session expiration action and controller
filterContext.Result = RedirectToAction("SessionExpired", "Home");
return;
}
}
}
//otherwise continue with action
base.OnActionExecuting(filterContext);
}
}
The second solution I found is based on Global.aspx's function
void Session_End(object sender, EventArgs e)
{
Response.Redirect("~/Account/Timeout");
}
but I read it only works when I use InProc. I don't know what that is, though.
If you go with the filter [Authorize] you can add it as a global filter.
filters.Add(new AuthorizeAttribute());
Which is what your controller does too.
To answer your questions:
Yes, if you are building a framework that impacts all controllers. There is no problem with inheriting from a base controller.
Looks like the global filter is right up your alley.
You can't access the cookie once the session expires since there is no client!
When a session expires, the screen gets frozen to information they've already been authorized for until any subsequent request is made.
Hope that helps.

asp_sessionid cookie default path to be made application specific

I have a website in which I had to make the default path which is usually visible as "/" for asp.netSessionId cookie and .ASPXAUTH cookie to application specific for security reasons and after lot of RND i was able to do that but I have a confusion and a problem now described as follows:
The confusion is: In order to make the cookie path application specific I wrote a method say "setcookiepath" to do so and called that method from global file's session_start but that did not work that as the path either used to get reset sometime after certain clicks or more than 1 cookie got generated one with default path and other with app specific path. So I went ahead and called my method 1 by 1 from other events in global.asax file and finally ended up calling from each one, still it worked only sometimes. Then I abandoned session in my Session_Start only the first time when Application is called by using a flag that i set in application_start and reset after session abandons in session_start. This time it worked perfectly fine. Now, i tried removing unneccesary calls from setcookiepath and tested this time it is working with single method call in any of the events and even after removing the logic of abandoning session it is working now.I am confused what is the right approach?
The problem is: Although now my code is working for the website but there is an Admin module which is called from a sub folder within that website. For this Admin sub module my code is not working i.e. the problem I am facing is. I am able to login to Admin module the first time I hit the url and 2 cookies get generated for ASP.NetSessionId one with default path and the other has application specific path. This works as long as my browser is open, when I close the browser and try to relogin, now my previous session cookies are removed and I keep getting redirected to login page because the user session gets invalidated in between somewhere when I am redirecting from login page to home page for Admin module, this issue is not occuring in main website module. Also, after removing the path reset logic from global.asax.cs file admin module session works fine between login and home page.
Please help.
Code for path set is :
public void setCookieApp()
{
if (HttpContext.Current != null)
{
/// only apply session cookie persistence to requests requiring session information
if (HttpContext.Current.Handler is IRequiresSessionState || HttpContext.Current.Handler is IReadOnlySessionState)
{
SessionStateSection sessionState = (SessionStateSection)ConfigurationManager.GetSection("system.web/sessionState");
string cookieName = sessionState != null && !string.IsNullOrEmpty(sessionState.CookieName) ? sessionState.CookieName : "ASP.NET_SessionId";
string cookieName1 = sessionState != null && !string.IsNullOrEmpty(sessionState.CookieName) ? sessionState.CookieName : ".ASPXAUTH";
//System.TimeSpan timeout = sessionState != null ? sessionState.Timeout : TimeSpan.FromMinutes(20);
/// Ensure ASP.NET Session Cookies are accessible throughout the subdomains.
if (HttpContext.Current.Request.Cookies[cookieName] != null)
{
//Response.Cookies[cookieName].Value = Session.SessionID;
HttpContext.Current.Response.Cookies[cookieName].Path = HttpContext.Current.Request.ApplicationPath;
//Response.Cookies[cookieName].Expires = DateTime.Now.Add(timeout);
}
if (HttpContext.Current.Request.Cookies[cookieName1] != null)
{
//Response.Cookies[cookieName].Value = Session.SessionID;
HttpContext.Current.Response.Cookies[cookieName1].Path = HttpContext.Current.Request.ApplicationPath;
//Response.Cookies[cookieName].Expires = DateTime.Now.Add(timeout);
}
}
foreach (string k in HttpContext.Current.Request.Cookies.AllKeys)
{
if (k.ToString().Contains("ASP.NET_SessionId"))
{
HttpContext.Current.Response.Cookies["ASP.NET_SessionId"].Path = HttpContext.Current.Request.ApplicationPath;
}
else if (k.ToString().Contains(".ASPXAUTH"))
{
HttpContext.Current.Response.Cookies[".ASPXAUTH"].Path = HttpContext.Current.Request.ApplicationPath;
}
}
}
}

How do I secure a web app using active directory groups?

Sorry for the basic question, first time with Web MVC4 in C#...
I'm creating a web interface for an application I've written in C#/SQL. I've been able to attach the MVC4 framework to the SQL DB. Now I want to secure what people can do based on group membership in AD. I have the authentication in my web.config set to "Windows" and it properly displays the User.Identity.Name that i'm logged in with. So I know it's pulling up the current logged in user. More over, I need to be able to authenticate a user outside of the active directory domain in the case of an android or ipad device. I haven't gotten that far yet though... for the most part, I'd like to auto authenticate the logged in user if possible then prompt for a username/password if none exists.
Ok, also I already know how to pull group membership for a user in AD. But I need to run that AD query and store that information somewhere that can be accessed on each page. Then on each page how do I access that variable?
For example, I don't want to display a menu option if they don't have access to it so that variable needs to be used to either display or not display the menu option that's being secured. Also, I assume I need to add that security on the webpage as well so that if someone tries to go there manually they cannot.
I assume I don't want to use session variables for security reasons..
In the past with Adobe Flex I used a singleton to manage the session state. I did a search out there and people are saying that it's probably not a good idea in C#. Not many examples of this anyway...
What are you doing to do this?
Here is what I would recommend. Start looking for examples of the ActiveDirectoryMembershipProvider Class. This MembershipProvider combined with Forms Authentication will provide you with a secure system to authenticate users.
Once authenticated, you need to authorize your users to access resources by combining the Active Directory Role Provider(ADRP) (to determine User Groups) with the standard way of Securing your MVC Application.
To get you started I created these simple extension methods when you can extend to use the ADRP (as I haven't used the ADRP).
public static class IPrincipalExtensions
{
private static _adminName = "Administrator";
public static bool IsAnonymous(this IPrincipal instance)
{
return (instance == null);
}
public static bool IsAdminOrInRole(this IPrincipal instance, string role)
{
if (instance == null
|| instance.Identity == null
|| !instance.Identity.IsAuthenticated)
{
return false;
}
bool result = instance.IsInRole(role)
|| instance.IsInRole(IPrincipalExtensions._adminName));
return result;
}
}
Then I also extended the default AuthorizeAttibute to give me an attribute I can use solely for Administrators:
public class AuthorizeAdministratorAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
bool result = false;
IPrincipal user = httpContext.User;
if (user.Identity.IsAuthenticated)
{
result = user.IsAdmin();
}
return result;
}
}
This uses the same extension methods provided in my IPrincipalExtensions so I don't repeat myself. Some might find this overkill as the following two lines are equal:
[Authorize("Administrator")]
[AuthorizeAdministrator]
However, since the first example is using a string, a simple mistype denies access, and if I decided to change the role/group name to "Admins" it becomes more difficult. So using the second one (which I could argue is strongly typed) if the group changes, I only have to change the name in one location.

How to force logout user when his/her username is changed by another user?

In my application I am using Forms-Authentication to sign in and sign out users.
One functionality is admin can change the username of other users. In that case, I need to sign out the user whose username is changed.
If I do not, due to their cookies set before, they gain access to application and receive error messages (since their username does not exist and there are parts where I use their username for some functionality).
How can I force these users to log out using Forms-Authentication ?
UPDATE :
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string controller = filterContext.RouteData.Values["controller"].ToString();
string action = filterContext.RouteData.Values["action"].ToString(); ;
// Below returns the previous username, which does not exist anymore in db.
string userName = HttpContext.Current.User.Identity.Name;
UnitOfWork unitOfWork = new UnitOfWork();
if (!unitOfWork.UserRepository.UserExists(userName))
{
FormsAuthentication.SignOut();
filterContext.HttpContext.Session.Clear();
filterContext.HttpContext.Session.Abandon();
// I am not using Roles.
}
unitOfWork.Dispose();
base.OnActionExecuting(filterContext);
}
In my customer global filter, I check whether user exist or not, if not I sign them out. However, it is not working. By working I mean they pass the authentication and gain access to application.
Thanks in advance.
Here's what you do to force user to sign out:
public void UserPasswordChangedHandler()
{
FormsAuthentication.SignOut();
Roles.DeleteCookie();
Session.Clear();
}
I don't think line by line explanation required, its self explanatory enough.
Please let me know if I am mistaken.
Update
Straightforward answer to your additional question is to keep per user boolean tracking if his data was updated by admin and if yes - just redirect him to login page.
Please see following articles for forced logout using forms authentication information:
How can I force a user to log out
How can I force a log out of all users for a website,
ASP.NET forms authentication forced logout
How to log off multiple MembershipUsers that are not the current user
Update 2
Clearing cookies
HowTo: create and remove Cookies with ASP.NET MVC
How do you clear cookies in ASP NET MVC 3 and C#
How do I invalidate a bad authentication cookie
Hope this help you.
When a user needs to become invalidated you must add their details to some kind of internal static list.
Then on every page request (possibly using Application_BeginRequest) see if that current user is in that list, and if so to call FormsAuthentication.SignOut there-and-then.
It seems like a bit of a hack, but it's the best I can think of right now.
Note that removing a user-in-absentia's session state is another issue entirely.

Categories

Resources