Restrict Auth & Session Cookies to a Subdomain in ASP.NET MVC5 - asp.net-mvc

I have a multi-tenanted application with several clients, who are distinguished by subdomain:
client1.mydomain.com
client2.mydomain.com
etc
I'm using forms authentication and the ASP.NET Auth & Session cookies on the client are set for the subdomain, e.g. client1.mydomain.com. This means that if I browse to client2.mydomain.com then I'm not logged in and the browser doesn't post the client1 cookies. Which is as it should be.
However something that has been picked up by our security testing is that you can take the cookie values from client1 and use the values to create cookies for client2 (we've done this in firebug). ASP.NET accepts these cookies and thinks you're authorised on client2.
How can I configure ASP.NET so that this doesn't happen?
The forms element in web.config allows you to set domain but I can't use this as I've a multi-tenanted app. I'm setting the cookie with
FormsAuthentication.SetAuthCookie(userName, false);
but I don't see a why to limit this to subdomain.

You should add the domain name to the user data of the cookie. To do this you have to switch to another cookie api:
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
... other parameters ..., domain );
HttpCookie cookie = new HttpCookie( FormsAuthentication.FormsCookieName );
cookie.Value = FormsAuthentication.Encrypt( ticket );
Response.SetCookie( cookie );
Then, in your global application class have an event handler that fires after the identity is established for the request. In the handler, verify that the domain name in the cookie is equal to a domain of the current request:
public void Application_PostAuthorizeRequest( object sender, EventArgs e )
{
HttpApplication app = sender as HttpApplication;
HttpContext ctx = app.Context;
if ( ctx.User.Identity.IsAuthenticated )
{
// current domain
string currentDomain = ctx.Request.Url.... // get the domain
// domain from cookie
FormsIdentity id = (FormsIdentity)ctx.User.Identity;
FormsAuthenticationTicket ticket = id.Ticket;
string cookieDomain = ticket.UserData;
if ( currentDomain != cookieDomain )
throw new Exception( "break the execution of the current request" );
...
This check will validate if the cookie was issued for the current domain or rather someone tries to reuse cookies between different domains.

Related

User Principal in Entity Framework Core DbContext

I am upgrading a project from .NET 4.6 to .NET Core. It is an ASP.NET MVC website with a WebAPI that uses EntityFramework. When the a (MVC or WebAPI) Controller fires up the DbContext, there is code that needs to identity the user as a ClaimsIdentity to inspect their claims. In previous .NET, this was most reliably available on Thread.CurrentPrincipal like this:
ClaimsIdentity identity = System.Threading.Thread.CurrentPrincipal.Identity as ClaimsIdentity;
IIRC, this was the safest way to do it since you could be coming from different contexts - WebAPI or ASP.NET MVC.
In the .NET core solution, I have tried to Dependency Inject an IHttpContextAccessor into the constructor, but the User on HttpContext is not authorized and has no claims
ClaimsIdentity identity = httpContext.HttpContext.User.Identity;
// identity.IsAuthenticated == false. identity.Claims is empty.
Security is wired up in Startup.cs:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).
AddCookie(options =>
{
options.LoginPath = "/Login";
options.Cookie.HttpOnly = true;
}).
AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
var key = Configuration["Tokens:Key"];
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["Tokens:Issuer"],
ValidAudience = Configuration["Tokens:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
};
});
The user logins on a /Login MVC view page, which logs in via Cookies and also generates a Bearer token in another request that is saved on the client. After all this the user is redirected to the homepage.
Cookie Login:
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), new AuthenticationProperties() { IsPersistent = bIsPersistent });
Token Generation (called from ajax, saved to localstorage before redirection)
var secretKey = Configuration["Tokens:Key"];
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var creds = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256 );
var expires = DateTime.Now.AddHours(8);
var token = new JwtSecurityToken(
_config["Tokens:Issuer"],
_config["Tokens:Issuer"],
oAuthIdentity.Claims,
expires: expires,
signingCredentials: creds
);
ret = Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
After landing on the homepage, an ajax call is made to the WebApi with the bearer token (I pulled the bearer token out of the http request and verified the signature on jwt.io), and the webapi causes the DbContext to be instantiated, and this is where the identity is not valid.
It's as if the Identity is not properly marshalled over to the DbContext -
How to I get the correct User or Identity in the DbContext?
Additionally, at the point I need it is in the DbContext construction, which I don't have alot of control over with the Dependency Injection. But I need to get this info basically from a default constructor or lazy load it somehow.
With your setup, you have two authentications setup. So, in your ConfigureServices function in Startup class, you need to use something like the following:
services.AddAuthentication().AddCookie().AddJwtBearer();
Don't forget to specify a default authentication. For instance, if you want the authentication to be cookies by default, you can use this:
services.AddAuthentication("Cookies").AddCookie().AddJwtBearer();
Or to keep the code safer,
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie().AddJwtBearer();
In your startup class, in the Configure function, don't forget to add
app.UseAuthentication();
When authenticating within a controller, you will need to use the scheme name along with the [Authorize] if you are not using the default scheme.
[Authorize(AuthenticationSchemes = "")]

OpenID Connect ASP NET MVC AAD

I implemented a sample app using OpenID Connect standard with ASP NET MVC website. My goal was to outsource storing sensitive data to Azure so i used Azure Active Directory. Since it's impossible to add custom properties to users in Azure i store non sensitive user Claims in our private db. I managed to get this claims and "add" them to the cookie like this:
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = context =>
{
var objectId = context.AuthenticationTicket.Identity.Claims.First(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier");
var claims = GetUserClaims(objectId.Value);
foreach (var item in claims)
{
context.AuthenticationTicket.Identity.AddClaim(new System.Security.Claims.Claim(item.Key, item.Value));
}
return Task.FromResult(0);
}
}
This way I added required claims to the cookie so those claims persist in my User object until user sign-out which is fine but there is one Claim which can change during the session ( basically user can change it on one page ). The problem is I can't find how to "change" this Claim in the cookie so it will persist. Ideally I would like to somehow force
AuthorizationCodeReceived
function to be called again. Is it possible ? Or is there another way where I can swap the value stored in the cookie ?
So far my only solution is to log-out user when he change this value so it will force him to sign-out again and my callback for AuthorizationCodeReceived will be called again, but it's not a very user-friendly way.
You can call HttpContext.GetOwinContext().Authentication.SignIn() after you add a claim in identity object to persist the new claim in cookie.

FormsAuthentication on AppHarbor

I upload my first ASP.NET MVC application to App Harbor, that uses FormsAuthentication to simple authentication of the user.
As usual on local machine operation that required authentications performes well, at the moment I tun them on AppHarbor, they failed.
I checked that authentication cookie released on access is send with the request, as it visible from image:
and my server side authentication code looks like
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
1,
user.UserName,
DateTime.Now,
DateTime.Now.AddDays(1),
true,
user.UserName
);
string encTicket = FormsAuthentication.Encrypt(authTicket);
this.Response.Cookies.Add(
new HttpCookie(
FormsAuthentication.FormsCookieName,
encTicket)
{ Expires = authTicket.Expiration });
I read different posts from AppHarbor blogs like ASP.NET Forms Authentication Considered Broken, it says "harmflul" and not that it's not working.
So, what I do wrong here ?

Setup a route {tenant}/{controller}/{action}/{id} with ASP.NET MVC?

I would like to setup a multi-tenant ASP.NET MVC app. Ideally, this app would have a route with {tenant}/{controller}/{action}/{id}, each tenant representing an logical instance of the app (simply independent multi-user accounts)
The fine grained details how do that are still quite unclear to me. Any guide available to setup such multi-tenant scheme with ASP.NET MVC?
I am currently working on a similar project using ASP.Net MVC, Forms Authentication and the SQL providers for Membership/Roles/Profile. Here is the approach I am taking:
Register the default route as `{tenant}/{controller}/{action}/{id}
Change the default behavior of the FormsAuthenticationService that comes with the standard MVC template. It should set the UserData of the authentication ticket to include the tenant name (from your route).
public void SignIn(string userName, bool createPersistentCookie, string tenantName)
{
var ticket = new FormsAuthenticationTicket(1, userName, DateTime.Now, DateTime.Now.AddMinutes(30),
createPersistentCookie, tenantName);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
HttpContext.Current.Response.AppendCookie(cookie);
}
In your global.asax file to do some tenant security checking and allow partioning of users between tenants in one membership database
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
//Since this method is called on every request
//we want to fail as early as possible
if (!Request.IsAuthenticated) return;
var route = RouteTable.Routes.GetRouteData(new HttpContextWrapper(Context));
if (route == null || route.Route.GetType().Name == "IgnoreRouteInternal") return;
if (!(Context.User.Identity is FormsIdentity)) return;
//Get the current tenant specified in URL
var currentTenant = route.GetRequiredString("tenant");
//Get the tenant that that the user is logged into
//from the Forms Authentication Ticket
var id = (FormsIdentity)Context.User.Identity;
var userTenant = id.Ticket.UserData;
if (userTenant.Trim().ToLower() != currentTenant.Trim().ToLower())
{
//The user is attempting to access a different tenant
//than the one they logged into so sign them out
//an and redirect to the home page of the new tenant
//where they can sign back in (if they are authorized!)
FormsAuthentication.SignOut();
Response.Redirect("/" + currentTenant);
return;
}
//Set the application of the Sql Providers
//to the current tenant to support partitioning
//of users between tenants.
Membership.ApplicationName = currentTenant;
Roles.ApplicationName = currentTenant;
ProfileManager.ApplicationName = currentTenant;
}
Partition each tenants data. Here are two options:
4a. Use a separate database for each tenant. This provides the best data security for your tenants. In the shared membership database, add a table that is keyed on unique appid for each tenant and use this table to store and retrieve the connection string based on the current tenant.
4b. Store all data in one database and key each table on the unique tenant id. This provides slightly less data security for your tenants but uses only one SQL Server license.
You will prob find these links useful.

Windows authentication & SQL Membership services

I have an ASP.Net MVC intranet site which uses Windows Authentication to know who is logged in (no anon browsing allowed). The first time the users visit, I collect some very basic information from them for their Contact object (such as name, email, country) which is then stored in the apps database.
I want to make the site role based, so I need to be able to assign each user a role (user, admin etc). I could do this using ADS groups, but this seems rather heavyweight. Can I use the SQL Membership services provided by ASP.Net to store their usernames and then the roles they belong to, or will I be forced to collect passwords etc (defeating the point of using Windows Authentication)? Also does this integrate with the ASP.Net MVC [Authorize] attribute?
It is certainly the case in "normal" ASP.NET that you can use this combination (Windows authentication and SQL for Roles), so it should be possible for MVC too.
Here's a link that might help.
Yes, you can do this.
Authorize uses the IsInRole method of IPrincipal to determine if the user is within a given role.
You can switch out the default implementation of IPrincipal during the AuthenticateRequest event within Global.asax with your implementation that handles this your way.
Here's some sample code that might actually work and compile and not expose your website to attacks by hackers:
private void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (!Request.IsAuthenticated)
{
Context.User = new MyPrincipal { Identity = new MyIdentity
{ Type = UserType.Inactive, Id = int.MinValue }};
Thread.CurrentPrincipal = Context.User;
}
else
{
HttpCookie authCookie = Request.Cookies[
FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
FormsAuthenticationTicket authTicket =
FormsAuthentication.Decrypt(authCookie.Value);
var identity = Db.GetIdentity(
authTicket.Name, new HttpRequestWrapper(Request));
Context.User = new MyPrincipal { Identity = new MyIdentity
{ Type = UserType.Inactive, Id = int.MinValue }};
Thread.CurrentPrincipal = Context.User;
}
}
}

Resources