Why isn't my authentication cookie being set in MVC 4? - asp.net-mvc

I've got an MVC4 project that I'm working on. When a user's login credentials are valid, I call FormsAuthentication.SetAuthCookie() to indicate that the user is logged in. (I have it wrapped in a class so I can mock the Interface for my unit tests.)
namespace FlashMercy.Shared.Security
{
using System;
using System.Web.Security;
public class Auth : IAuth
{
public void SetAuthCookie(string userId, bool remember)
{
FormsAuthentication.SetAuthCookie(userId, remember);
}
public void Signout()
{
FormsAuthentication.SignOut();
}
}
}
In the debugger, I can confirm that the .SetAuthCookie(userId, remember) line is executing, and userId is populated.
Then, I have a custom authorize attribute to check that the user is logged in:
namespace FlashMercy.Shared.Security
{
using System.Web.Mvc;
public class FlashMercyAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult("/");
}
}
}
}
When I debug the application, the filterContext.HttpContext.User.Identity.IsAuthenticated is false even after I've supposedly set the auth cookie. Also, filterContext.HttpContext.User.Identity.Name is empty. I'm not sure what I'm missing here.
Update
If you care to look at the whole source, it's available on GitHub: https://github.com/quakkels/flashmercy.

Problem with your code is that you are using FormsAuthentication, but you didn't add it to web.config. Your web.config should have such section:
<system.web>
<authentication mode="Forms"></authentication>
...
</system.web>
Based on this Mode Asp.Net understand what authentication mode it should use, e.g. Forms, Windows, etc. And without settings it to Forms value - FormsAuthenticationModule just ignores .ASPXAUTH cookie from the request.
PS. I've downloaded your code, and with correct authentication section in web.config it works fine and updates HttpContext.User.Identity.IsAuthenticated to true.

The problem is that you only set the authentication cookie but do not have anything that load it.
It's forms authentication that uses that cookie. So you either have to activate forms authentication or you'll have to load it yourself.

filterContext.HttpContext.User.Identity.IsAuthenticated is false even after I've supposedly set the auth cookie.
This will always be the case if you do not redirect after SetAuthCookie(). The ASP.Net pipeline is in charge of authorizing the user (most of the time before we write code) in the AuthenticateRequest. Setting a Cookie does not update the current User.Identity, this requires code that has already been executed. Just make sure anytime you SetAuthCookie() you immediately redirect (server side is fine) to another URL (probably should anyway, its a good way to seperate logging in a user, and what they should do next SRP).

Related

Sustainsys SAML2 Sample for ASP.NET Core WebAPI without Identity

Does anyone have a working sample for Sustainsys Saml2 library for ASP.NET Core WebAPI only project (no Mvc) and what's more important without ASP Identity? The sample provided on github strongly relies on MVC and SignInManager which I do not need nor want to use.
I added Saml2 authentication and at first it worked fine with my IdP (I also checked the StubIdP provided by Sustainsys) for first few steps so:
IdP metadata get properly loaded
My API properly redirects to sign-in page
Sign-in page redirects to /Saml2/Acs page, and I see in the logs that it parses the result successfully
However I don't know how to move forward from there and extract user login and additional claims (my IdP provided also an e-mail, and it is included in SAML response which I confirmed in the logs).
Following some samples found on the web and modyfing a little bit the MVC Sample from GitHub I did the following:
In Startup.cs:
...
.AddSaml2(Saml2Defaults.Scheme,
options =>
{
options.SPOptions.EntityId = new EntityId("...");
options.SPOptions.ServiceCertificates.Add(...));
options.SPOptions.Logger = new SerilogSaml2Adapter();
options.SPOptions.ReturnUrl = new Uri(Culture.Invariant($"https://localhost:44364/Account/Callback?returnUrl=%2F"));
var idp =
new IdentityProvider(new EntityId("..."), options.SPOptions)
{
LoadMetadata = true,
AllowUnsolicitedAuthnResponse = true, // At first /Saml2/Acs page throwed an exception that response was unsolicited so I set it to true
MetadataLocation = "...",
SingleSignOnServiceUrl = new Uri("...") // I need to set it explicitly because my IdP returns different url in the metadata
};
options.IdentityProviders.Add(idp);
});
In AccountContoller.cs (I tried to follow a somewhat similar situation described at how to implement google login in .net core without an entityframework provider):
[Route("[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly ILog _log;
public AccountController(ILog log)
{
_log = log;
}
[HttpGet("Login")]
[AllowAnonymous]
public IActionResult Login(string returnUrl)
{
return new ChallengeResult(
Saml2Defaults.Scheme,
new AuthenticationProperties
{
// It looks like this parameter is ignored, so I set ReturnUrl in Startup.cs
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl })
});
}
[HttpGet("Callback")]
[AllowAnonymous]
public async Task<IActionResult> LoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync(Constants.Auth.Schema.External);
_log.Information("Authenticate result: {#authenticateResult}", authenticateResult);
// I get false here and no information on claims etc.
if (!authenticateResult.Succeeded)
{
return Unauthorized();
}
// HttpContext.User does not contain any data either
// code below is not executed
var claimsIdentity = new ClaimsIdentity(Constants.Auth.Schema.Application);
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
_log.Information("Logged in user with following claims: {#Claims}", authenticateResult.Principal.Claims);
await HttpContext.SignInAsync(Constants.Auth.Schema.Application, new ClaimsPrincipal(claimsIdentity));
return LocalRedirect(returnUrl);
}
TLDR: Configuration for SAML in my ASP.NET Core WebApi project looks fine, and I get success response with proper claims which I checked in the logs. I do not know how to extract this data (either return url is wrong or my callback method should work differently). Also, it is puzzling why successfuly redirect from SSO Sign-In page is treated as "unsolicited", maybe this is the problem?
Thanks for any assistance
For anyone who still needs assistance on this issue, I pushed a full working example to github which uses a .Net Core WebAPI for backend and an Angular client using the WebAPI. you can find the example from here:
https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample
As it turned out, the various errors I've been getting were due to my solution being hosted inside docker container. This caused a little malfunction in internal aspnet keychain. More details can be found here (docker is mentioned almost at the end of the article):
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?tabs=aspnetcore2x&view=aspnetcore-2.2
Long story short, for the code to be working I had to add only these lines:
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/some/volume/outside/docker")); // it needs to be outside container, even better if it's in redis or other common resource
It fixed everything, which includes:
Sign-in action to external cookie
Unsolicited SSO calls
Exceptions with data protection key chain
So it was very difficult to find, since exceptions thrown by the code didn't point out what's going on (and the unsolicited SSO calls made me think that the SSO provider was wrongly configured). It was only when I disassembled the Saml2 package and tried various code pieces one by one I finally encoutered proper exception (about the key chain) which in turned led me to an article about aspnet data protection.
I provide this answer so that maybe it will help someone, and I added docker tag for proper audience.

MVC Windows Authentication causing querystring too long error

I have an MVC web site that I’d like to modify so instead of using forms to log the user in, it picks up their windows id then passes this to a local Active Directory using LDAP.
However, when I change IIS from Anonymous to Windows Authentication and change the code in Start.Auth.cs with the following (where LoginUser is the script that picks up the user and connects to AD)…
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/LoginUser")
});
…it causes a querystring is too long browser error …
http://localhost:80/UserAuthentication/Account/LoginUser?ReturnUrl=%2FUserAuthentication%2FAccount%2FLoginUser%3FReturnUrl%3D%252FUserAuthentication%252FAccount%252FLoginUser%253FReturnUrl%253D%25252FUserAuthentication%25252FAccount%25252FLoginUser%25253FReturnUrl%25253D%2525252FUserAuthentication%2525252FAccount%2525252FLoginUser%2525253FReturnUrl%2525253D%252525252FUserAuthentication%252525252FAccount%252525252FLoginUser%252525253FReturnUrl%252525253D% (etc)
I have placed [AllowAnonymous] above the LoginUser script so I’m really not sure why it won’t go into it (as this what looks like is happening again and again).
Any help would really really be most appreciated.
Updated to include LoginUser script:
I've included a stripped down version of it that just logs a user in.
[AllowAnonymous]
public async Task<ActionResult> LoginUser()
{
var status = await SignInManager.PasswordSignInAsync("ATHORNE", "Something123!", false, false);
return View();
}
I am using PasswordSignInAsync with a constant password because there would not be a password for Windows Authentication. If there is a better way, please let me know!
The View is the blank default view at the moment.
Typically I would override OnActionExcuting(ActionExecutingContext filterContext)
You can do this in a base Controller class or in an Action Filter
If IIS is set to deny Anonymous Access and enable Windows authentication then within the filter context you can access the users' identity via HttpContext.User.Identity.Name;
To be a bit safer you could write some code like
var user = (filterContext.HttpContext.User.Identity != null
&& filterContext.HttpContext.User.Identity.IsAuthenticated) ?
HttpContext.User.Identity.Name : null;
If username is not null then you have the currently logged on authenticated user, in the form domain\username. You can query Active Directory based on this to get any other information you need.

MVC3: Session_Start Fires twice when testing for Roles

I need to do some authentication for a web app with MVC3. The customer would like there to be a generic page to show if they do not have any of the role groups in windows AD that are allowed to use the app. I found a pretty simple way to do it, but just curious if it is a valid way or if there is something better out there.
Basically in the Session_Start in the global I am checking for User.IsInRole() and if that returns false then I do a Response.Redirect(). This question is: after it his the code in the IF statement and hits the Response.Redirect() code then it hits the session one more time before it goes to the AccessDenied page in the root of the app. Is this okay? Will it cause any issues If they are valid and does not enter the If to do the response.redirect?
//if (!User.IsInRole("test_user"))
//{
// Response.Redirect("~/AccessDenied.aspx", true);
//}
I would recommend you to write your Authorization filter for MVC3 and do this type of logic there:
public class RoleFilter: AuthorizeAttribute
{
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext filterContext)
{
if (!User.IsInRole("test_user"))
{
filterContext.HttpContext.Response.StatusCode = 302;
filterContext.Result = new RedirectResult("~/AcessDenied.aspx");
}
}
}
Also I wouldn't recommend you to use Response.Redirect because it aborts current thread.

Hybrid of Windows Authentication and Forms Authentication in ASP.NET MVC 4

We have an ASP.NET MVC 4 intranet application. We’re using Windows Authentication and that aspect works fine. The user’s credentials are used and we can access those credentials from the web app.
What we really want is some sort of hybrid mode, however. We want to get the user’s credentials from the browser, but we also want to verify that the user is in our application’s database. If the user’s in the database, then they can just continue on. If they’re not, we want to redirect them to a page asking for alternate credentials. What I’m doing now is, in Global.asax.cs, I’ve got an Application_AuthenticateRequest method and I’m checking to see if the user is authenticated. If they are and their cookie information doesn’t reflect the fact that they’re logged into the system, then I log them in and set up some cookies with info about the user. If they’re not authenticated, I redirect them to a login page. We can’t use AD roles for reasons involved with company policy, so we need to use the database for additional authentication.
I’m guessing Application_AuthenticateRequest isn’t the place to do this, but maybe it is. But we basically need a place to filter the requests for authentication. But additionally this implementation leads me to another issue:
We have certain URLs in our app that allow anonymous access. I’ve added <location> tags to the web.config for these. The problem is, when anonymous calls are made into these, it gets to Application_AuthenticateRequest and tries to log the user into the DB. Now, I can add code into Application_AuthenticateRequest to handle these URLs and that’s currently my plan, but if I’m write and Application_AuthenticateRequest isn’t the place to be doing this, then I’d rather figure it out now than later.
You need to use Action Filters for this purpose. You can extend the AuthorizeAttribute like this:
public class MyAuthorizeAttribute : AuthorizeAttribute
{
private UnitOfWork _unitOfWork = new UnitOfWork();
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var isAuthorized = false;
var username = httpContext.User.Identity.Name;
// Some code to find the user in the database...
var user = _unitOfWork.UserRepository.Find(username);
if(user != null)
{
isAuthorized = true;
}
return isAuthorized;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (AuthorizeCore(filterContext.HttpContext))
{
SetCachePolicy(filterContext);
}
else
{
// If not authorized, redirect to the Login action
// of the Account controller...
filterContext.Result = new RedirectToRouteResult(
new System.Web.Routing.RouteValueDictionary {
{"controller", "Account"}, {"action", "Login"}
}
);
}
}
protected void SetCachePolicy(AuthorizationContext filterContext)
{
// ** IMPORTANT **
// Since we're performing authorization at the action level,
// the authorization code runs after the output caching module.
// In the worst case this could allow an authorized user
// to cause the page to be cached, then an unauthorized user would later
// be served the cached page. We work around this by telling proxies not to
// cache the sensitive page, then we hook our custom authorization code into
// the caching mechanism so that we have the final say on whether a page
// should be served from the cache.
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidationHandler, null /* data */);
}
public void CacheValidationHandler(HttpContext context,
object data,
ref HttpValidationStatus validationStatus)
{
validationStatus = OnCacheAuthorization(new HttpContextWrapper(context));
}
}
Then, you can use this attribute at the Controller level or Action level like this:
[MyAuthorize]
public ActionResult SomeAction()
{
// Code that is supposed to be accessed by authorized users only
}

Why does AuthorizeAttribute redirect to the login page for authentication and authorization failures?

In ASP.NET MVC, you can mark up a controller method with AuthorizeAttribute, like this:
[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
// ...
}
This means that, if the currently logged-in user is not in the "CanDeleteTags" role, the controller method will never be called.
Unfortunately, for failures, AuthorizeAttribute returns HttpUnauthorizedResult, which always returns HTTP status code 401. This causes a redirection to the login page.
If the user isn't logged in, this makes perfect sense. However, if the user is already logged in, but isn't in the required role, it's confusing to send them back to the login page.
It seems that AuthorizeAttribute conflates authentication and authorization.
This seems like a bit of an oversight in ASP.NET MVC, or am I missing something?
I've had to cook up a DemandRoleAttribute that separates the two. When the user isn't authenticated, it returns HTTP 401, sending them to the login page. When the user is logged in, but isn't in the required role, it creates a NotAuthorizedResult instead. Currently this redirects to an error page.
Surely I didn't have to do this?
When it was first developed, System.Web.Mvc.AuthorizeAttribute was doing the right thing -
older revisions of the HTTP specification used status code 401 for both "unauthorized" and "unauthenticated".
From the original specification:
If the request already included Authorization credentials, then the 401 response indicates that authorization has been refused for those credentials.
In fact, you can see the confusion right there - it uses the word "authorization" when it means "authentication". In everyday practice, however, it makes more sense to return a 403 Forbidden when the user is authenticated but not authorized. It's unlikely the user would have a second set of credentials that would give them access - bad user experience all around.
Consider most operating systems - when you attempt to read a file you don't have permission to access, you aren't shown a login screen!
Thankfully, the HTTP specifications were updated (June 2014) to remove the ambiguity.
From "Hyper Text Transport Protocol (HTTP/1.1): Authentication" (RFC 7235):
The 401 (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.
From "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content" (RFC 7231):
The 403 (Forbidden) status code indicates that the server understood the request but refuses to authorize it.
Interestingly enough, at the time ASP.NET MVC 1 was released the behavior of AuthorizeAttribute was correct. Now, the behavior is incorrect - the HTTP/1.1 specification was fixed.
Rather than attempt to change ASP.NET's login page redirects, it's easier just to fix the problem at the source. You can create a new attribute with the same name (AuthorizeAttribute) in your website's default namespace (this is very important) then the compiler will automatically pick it up instead of MVC's standard one. Of course, you could always give the attribute a new name if you'd rather take that approach.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
Add this to your Login Page_Load function:
// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
Response.Redirect("Unauthorized.aspx");
When the user is redirected there but is already logged in, it shows the unauthorized page. If they are not logged in, it falls through and shows the login page.
I always thought this did make sense. If you're logged in and you try to hit a page that requires a role you don't have, you get forwarded to the login screen asking you to log in with a user who does have the role.
You might add logic to the login page that checks to see if the user is already authenticated. You could add a friendly message that explains why they've been bumbed back there again.
Unfortunately, you're dealing with the default behavior of ASP.NET forms authentication. There is a workaround (I haven't tried it) discussed here:
http://www.codeproject.com/KB/aspnet/Custon401Page.aspx
(It's not specific to MVC)
I think in most cases the best solution is to restrict access to unauthorized resources prior to the user trying to get there. By removing/graying out the link or button that might take them to this unauthorized page.
It probably would be nice to have an additional parameter on the attribute to specify where to redirect an unauthorized user. But in the meantime, I look at the AuthorizeAttribute as a safety net.
Try this in your in the Application_EndRequest handler of your Global.ascx file
if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
HttpContext.Current.Response.ClearContent();
Response.Redirect("~/AccessDenied.aspx");
}
If your using aspnetcore 2.0, use this:
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Core
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;
if (!user.Identity.IsAuthenticated)
{
context.Result = new UnauthorizedResult();
return;
}
}
}
}
In my case the problem was "HTTP specification used status code 401 for both "unauthorized" and "unauthenticated"". As ShadowChaser said.
This solution works for me:
if (User != null && User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
//Do whatever
//In my case redirect to error page
Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}

Resources