I have been having this problem for a couple months. The issue is sporadic and difficult to reproduce, but I have managed to find a somewhat consistent way to do it. I am debugging and see that as soon as it should redirect to /home/index, it goes to /account/logoff instead. If I test locally then it will work on the first login fine then it will fail if I use a different account. I set the CookieName and CookieSecure after seeing a couple of posts related to them. It didn't seem to help though.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieManager = new SystemWebChunkingCookieManager(),
CookieName = "NewImprovedCookieName",
CookieSecure = CookieSecureOption.SameAsRequest,
LoginPath = new PathString("/Account/Login"),
LogoutPath = new PathString("/Account/LogOff"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity =
SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
The LogOff happens right after this last line of code here.
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
I tried updating everything in the project to see if it was a bug that was fixed at some point (this was an old project), but nothing seemed to help it.
Is there a way to fix this, or should I consider migrating to .Net Core?
I don't know if this is the best solution, but I did find one that works. I noticed that the SessionID was persisting after logging out. I added code to remove abandon the session and change the session ID, and it appears to be working now.
public ActionResult LogOff()
{
AuthenticationManager.SignOut();
Session.Clear();
Session.Abandon();
Response.Cookies.Add(new HttpCookie("ASP.NET_SessionId", ""));
return RedirectToAction("Index", "Home");
}
Related
I found the problem in my project. The problem is that the cookie is not saved when the user is logged in via the iframe. Cookies are saved when you log in to the site normally, that is, directly through the domain. There are 2 different problems for me right now:
When logging into the site directly (www.example.com), the relevant cookie is saved. But this cookie is saved as Lax. I want it to be saved as none.
If there is no direct login to the site before, if you want to log in from within the iframe, the cookie is not saved. I want to create cookies on logins made via iframe.
I did not perform anything on the cookie. I guess SignAsync() creates the relevant cookies automatically. I am also attaching my codes inside the Account Controller.
public IActionResult Login(string returnurl)
{
return View(new LoginViewModel {ReturnUrl=returnurl, IsPersistent=true });
}
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
var result = await signInManager.PasswordSignInAsync(model.Username, model.Password, model.IsPersistent, true);
if (result.Succeeded)
{
return Redirect(model.ReturnUrl ?? "/");
}
else
{
ModelState.AddModelError("","Invalid User Login");
return View(model);
}
}
I also tried the solutions in the links below but it didn't work for me. Maybe I have applied these solutions incorrectly.
https://learn.microsoft.com/tr-tr/aspnet/core/security/samesite/rp31?view=aspnetcore-3.1&viewFallbackFrom=aspnetcore-6.0
https://learn.microsoft.com/tr-tr/aspnet/core/security/samesite?view=aspnetcore-6.0
And here is a photo:
Cookie View with Cookie Editor
I will be accessing several tables to determine if a user is "Validated" or not as well as adding custom roles to a Windows authenticated user for authorization. For now I'm running a test in a basic .net Core web application just to see how I should be doing this. I have setup a RequiredClaim in my Fallback Policy and a ClaimsLoader and it works great:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddTransient<IClaimsTransformation, ClaimsLoader>();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireClaim("ValidatedUser")
.Build();
});
}
public class ClaimsLoader : IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var claimsIdentity = (ClaimsIdentity)principal.Identity;
claimsIdentity.AddClaim(new Claim("ValidatedUser", ""));
return await Task.FromResult(principal);
}
}
As long as that AddClaim line is in there, they can access the app, without it they get a not-authorized response which is what I want.
Based on what I've read I thought any claims/roles I add in the transformation should come back each time but they do not. In the code above I have the AddClaim running every time so it's working, but in reality I will be going to a database to determine if I should add that claim which is an expensive process. I want to persist the results across multiple requests. So I want to check if the claim is already there and not bother getting it again if it is. For whatever reason it is NEVER there when it comes back for a second request.
From what I've read here back in 2.x the claims should persist:
https://philipm.at/2018/aspnetcore_claims_with_windowsauthentication.html
But here in my 3.1 application they do not.
What I need to do
I'm developing an application using ASP.NET CORE and I actually encountered a problem using the Identity implementation.
In the official doc infact there is no reference about the multiple session, and this is bad because I developed a SaaS application; in particular a user subscribe a paid plan to access to a specific set of features and him can give his credentials to other users so they can access for free, this is a really bad scenario and I'll lose a lot of money and time.
What I though
After searching a lot on the web I found many solutions for the older version of ASP.NET CORE, so I'm not able to test, but I understood that the usually the solution for this problem is related to store the user time stamp (which is a GUID generated on the login) inside the database, so each time the user access to a restricted page and there are more session (with different user timestamp) the old session will closed.
I don't like this solution because an user can easily copy the cookie of the browser and share it will other users.
I though to store the information of the logged in user session inside the database, but this will require a lot of connection too.. So my inexperience with ASP.NET CORE and the lack of resource on the web have sent me in confusion.
Someone could share a generic idea to implement a secure solution for prevent multiple user login?
I've created a github repo with the changes to the default .net core 2.1 template needed to only allow single sessions. https://github.com/xKloc/IdentityWithSession
Here is the gist.
First, override the default UserClaimsPrincipalFactory<IdentityUser> class with a custom one that will add your session to the user claims. Claims are just a key/value pair that will be stored in the user's cookie and also on the server under the AspNetUserClaims table.
Add this class anywhere in your project.
public class ApplicationClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser>
{
private readonly UserManager<IdentityUser> _userManager;
public ApplicationClaimsPrincipalFactory(UserManager<IdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor)
{
_userManager = userManager;
}
public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
// find old sessions and remove
var claims = await _userManager.GetClaimsAsync(user);
var session = claims.Where(e => e.Type == "session");
await _userManager.RemoveClaimsAsync(user, session);
// add new session claim
await _userManager.AddClaimAsync(user, new Claim("session", Guid.NewGuid().ToString()));
// create principal
var principal = await base.CreateAsync(user);
return principal;
}
}
Next we will create an authorization handler that will check that the session is valid on every request.
The handler will match the session claim from the user's cookie to the session claim stored in the database. If they match, the user is authorized to continue. If they don't match, the user will get a Access Denied message.
Add these two classes anywhere in your project.
public class ValidSessionRequirement : IAuthorizationRequirement
{
}
public class ValidSessionHandler : AuthorizationHandler<ValidSessionRequirement>
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public ValidSessionHandler(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidSessionRequirement requirement)
{
// if the user isn't authenticated then no need to check session
if (!context.User.Identity.IsAuthenticated)
return;
// get the user and session claim
var user = await _userManager.GetUserAsync(context.User);
var claims = await _userManager.GetClaimsAsync(user);
var serverSession = claims.First(e => e.Type == "session");
var clientSession = context.User.FindFirst("session");
// if the client session matches the server session then the user is authorized
if (serverSession?.Value == clientSession?.Value)
{
context.Succeed(requirement);
}
return;
}
}
Finally, just register these new classes in start up so they get called.
Add this code to your Startup class under the ConfigureServices method, right below services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// build default authorization policy
var defaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new ValidSessionRequirement())
.Build();
// add authorization to the pipe
services.AddAuthorization(options =>
{
options.DefaultPolicy = defaultPolicy;
});
// register new claims factory
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, ApplicationClaimsPrincipalFactory>();
// register valid session handler
services.AddTransient<IAuthorizationHandler, ValidSessionHandler>();
You can use UpdateSecurityStamp to invalidate any existing authentication cookies. For example:
public async Task<IActionResult> Login(LoginViewModel model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "Invalid username/password.");
return View();
}
if (await _userManager.ValidatePasswordAsync(user, model.Password))
{
await _userManager.UpdateSecurityStampAsync(user);
var result = await _signInManager.SignInAsync(user, isPersistent: false);
// handle `SignInResult` cases
}
}
By updating the security stamp will cause all existing auth cookies to be invalid, basically logging out all other devices where the user is logged in. Then, you sign in the user on this current device.
Best way is to do something similar to what Google, Facebook and others do -- detect if user is logging in from a different device. For your case, I believe you would want to have a slight different behavior -- instead of asking access, you'll probably deny it. It's almost like you're creating a license "per device", or a "single tenant" license.
This Stack Overflow thread talks about this solution.
The most reliable way to detect a device change is to create a
fingerprint of the browser/device the browser is running on. This is a
complex topic to get 100% right, and there are commercial offerings
that are pretty darn good but not flawless.
Note: if you want to start simple, you could start with a Secure cookie, which is less likely to be exposed to cookie theft via eavesdropping. You could store a hashed fingerprint, for instance.
There are some access management solutions (ForgeRock, Oracle Access Management) that implement this Session Quota functionality. ForgeRock has a community version and its source code is available on Github, maybe you can take a look at how it is implemented there. There is also a blog post from them giving a broad view of the functionality (https://blogs.forgerock.org/petermajor/2013/01/session-quota-basics/)
If this is too complex for your use case, what I would do is combine the "shared memory" approach that you described with an identity function, similar to what Fabio pointed out in another answer.
I totally understand if someone finds that my question is very basic or might not make a lot of sense all the way.
I am new to this and I am trying to use the latest .NET Framework 5 with MVC 6 in order to build a Web Api that could be used from an Angular JS client-side. This will allow me to create a website for it, as well as a mobile application by wrapping it with Phonegap. So please bear with me a bit.
What I am trying to achieve for the moment is to have a Web API controller that receives a login request and returns a result to the client based on Cookie Authentication (later the client should store this cookie and use it for communications with the server)
I added the following in the project.json
In the Startup.cs, I added under ConfigureServices:
// Add entity framework support
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
});
// add ASP.NET Identity
services.AddIdentity<ApplicationUser, IdentityRole>(options => {
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonLetterOrDigit = false;
options.Password.RequiredLength = 6;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
In the Startup.cs, under Configure:
// Using the identity that technically should be calling the UseCookieAuthentication
app.UseIdentity();
Now, in the Controller method to login, I am able to find the user using its email address and the UserManager:
// Verify that the model is valid according to the validation rules in the model itself.
// If it isn't valid, return a 400 Bad Request with some JSON reviewing the errors
if (!ModelState.IsValid)
{
return HttpBadRequest(ModelState);
}
// Find the user in our database. If the user does not exist, then return a 400 Bad Request with a general error.
var user = await userManager.FindByEmailAsync(model.Email);
if (user == null)
{
ModelState.AddModelError("", INVALID_LOGIN_MESSAGE);
return HttpBadRequest(ModelState);
}
// If the user has not confirmed his/her email address, then return a 400 Bad Request with a request to activate the account.
if (!user.EmailConfirmed)
{
ModelState.AddModelError("Email", "Account not activated");
return HttpBadRequest(ModelState);
}
// Authenticate the user with the Sign-In Manager
var result = await signInManager.PasswordSignInAsync(user.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
// If the authentication failed, add the same error that we add when we can't find the user
// (so you can't tell the difference between a bad username and a bad password) and return a 400 Bad Request
if (!result.Succeeded)
{
ModelState.AddModelError("", INVALID_LOGIN_MESSAGE);
return new BadRequestObjectResult(ModelState);
}
return Ok();
The problem is happening at the line:
// Authenticate the user with the Sign-In Manager
var result = await signInManager.PasswordSignInAsync(user.UserName, model.Password, model.RememberMe, lockoutOnFailure: false);
it is throwing the following error:
Error: No authentication handler is configured to handle the scheme:
Microsoft.AspNet.Identity.Application
I am currently blocked and I searched googled for almost every possible token I could think of and tried multiple solution still in no vain. Any help is highly appreciated.
Regards,
Ok I finally figured it out after writing this whole question and I wanted to share the answer to avoid the hussle for someone else if they commit the same mistake I did!
The problem was that in the Configure in Startup.cs, I called "app.UseIdentity()" after calling "app.UseMVC()". The order should have been inversed. I donno if this is common knowledge or I should have read about it somewhere.
I've made my own authorize attribute, and this is what it looks like
public class RedirectAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectToRouteResult(new
RouteValueDictionary(new { controller = "NotExist" }));
}
}
}
So if the user isn't authenticated, I want them to get redirected to the NotExist controller. I've debugged and it seems that unauthorized users get in the if clause, which is correct. But I've also tried doing this with logged in users, and they get in the if clause as well which is wrong.
I dont understand why this is happening. It makes me hesitate about whether my log-in didnt work. Is this the right way of logging a user in?
FormsAuthentication.SetAuthCookie(acc.username, false);
I've never made a log-in system in asp.net mvc before, so please tell me what I'm doing wrong.
Edit:
It seems that the default [Authorized] attribute isn't working either... I really think the problem lays in the log in:
[HttpPost]
public ActionResult Login(User acc)
{
if(ModelState.IsValid)
{
if (Validate(acc.username, acc.password))
{
FormsAuthentication.SetAuthCookie(acc.username, false);
return RedirectToAction("Index", "System");
}
}
ModelState.AddModelError("IncorrectDetails", "Wrong details. Please try again.");
return View(acc);
}
The custom authorize attribute looks correct.
Since you are setting the cookie yourself I would guess you are not using the built-in membership provider.
If you set the cookie yourself, you also need to read the auth cookie and set the Identity and Principal objects on each request. Otherwise, HttpContext.User.Identity.IsAuthenticated will always be false, which seems to be what you are experiencing.