Force logout in ASP.NET MVC Core - c#

I'm using ASP.Net MVC Core where I have the following code where it is a daily task that sets users as "Expired" if their subscription is over.
But how can I also check and force them to logout if they are currently logged in?
public interface IExpirationJob
{
Task SetExpired();
}
public class ExpirationJob : IExpirationJob
{
private readonly ApplicationDbContext _db;
private readonly IEmailSender _emailSender;
public ExpirationJob(ApplicationDbContext db, IEmailSender emailSender)
{
_db = db;
_emailSender = emailSender;
}
public async Task SetExpired()
{
foreach(var item in _db.Institution)
{
if (item.SubscriptionEndDate != null)
{
if (item.SubscriptionEndDate == DateTime.Today)
{
item.Status = SD.StatusExpired;
//Here I want to check if the user is logged in, then force logout should be done.
}
}
}
await _db.SaveChangesAsync();
}
}
Any help is highly appreciated.

You can add a SecurityStamp property of type GUID to users model and set SecurityStamp to cookie or jwt token.
then when a user login, you must change the SecurityStamp value to a new value and save SecurityStamp to cookie and any time user send a request to application you must check SecurityStamp saved in cookie with SecurityStamp of users in database. and if these properties wasn't equal togeter you must reject user and set sign out user.
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
context = context ?? throw new ArgumentNullException(nameof(context));
var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
if(claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
{
await RejectPrincipal();
return;
}
UserManager<IdentityUser> userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var user = await userManager.FindByNameAsync(context.Principal.FindFirstValue(ClaimTypes.NameIdentifier));
if (user == null || user.SecurityStamp != context.Principal.FindFirst(new ClaimsIdentityOptions().SecurityStampClaimType)?.Value)
{
await RejectPrincipal();
return;
}
async Task RejectPrincipal()
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
In startup class pass ValidateAsync method to OnValidatePrincipal and set ValidationInterval to zero.
services.ConfigureApplicationCookie(options =>
{
//
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = ValidateAsync
};
}).Configure<SecurityStampValidatorOptions>(options =>
{
options.ValidationInterval = TimeSpan.Zero;
});
finally in your method just update SecurityStamp value like this:
public async Task SetExpired()
{
foreach(var item in _db.Institution)
{
if (item.SubscriptionEndDate != null)
{
if (item.SubscriptionEndDate == DateTime.Today)
{
item.Status = SD.StatusExpired;
//Here I want to check if the user is logged in, then force logout should be done.
Guid securityStamp = Guid.NewGuid();
item.SecurityStamp = securityStamp;
}
}
}
await _db.SaveChangesAsync();
}

Related

Force Redirect to Another Page when First Time Login in ASP.NET Core

currently I'm building web apps for local. When I create new user, I'm setup the user with Default Password. And I want to, if the User login with the Default Password it will redirect to Change Password Page. And User will not be able to access any page until they change the password.
My current workaround is checking in each controller, is there any Smart way to do this?
Here's some code after doing login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login([FromForm] LoginViewModel vm, string returnUrl = null)
{
if (ModelState.IsValid)
{
var result = await repository.Login(vm);
if (result.IsSuccess)
{
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, result.Data,
new AuthenticationProperties
{
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddDays(1),
IsPersistent = false
});
if (vm.Password != Password.DefaultPassword)
{
return RedirectToLocal(returnUrl);
}
else
{
return RedirectToAction(nameof(UserController.NewPassword));
}
}
else
{
ViewBag.ErrorMessage = result.ErrorMessage;
}
}
return View(vm);
}
For the other Controller, we always check the Password from session. If the Password is same as Default Password we redirect it to NewPassword Page
Thanks in advance!
You can store user password using session as below
HttpContext.Session.SetString("Password", vm.password);
Then create a filter to check if the user login with the Default Password or not
public class PasswordFilter: IAuthorizationFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISession _session;
public PasswordFilter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_session = _httpContextAccessor.HttpContext.Session;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (_session.GetString("Password") == Password.DefaultPassword)
{
context.Result = new RedirectResult(nameof(UserController.NewPassword));
}
}
}
Finally, you can use this filter by adding this attribute to your controllers
[TypeFilter(typeof(PasswordFilter))]
I hope this approach achieves your goal.
As #JHBonarius said, my current workaround now is to use Custom Middleware for Redirecting to New Password Page.
My middleware look like this:
public class CheckPasswordMiddleware
{
private readonly RequestDelegate next;
public CheckPasswordMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Path != "/User/NewPassword" && context.User.Identity.IsAuthenticated)
{
var userId = context.User.Identity.GetUserId();
if (!string.IsNullOrEmpty(userId))
{
var dbContext = context.RequestServices.GetRequiredService<DatabaseContext>();
var passwordHash = await dbContext.User.Where(x => x.Id.ToLower() == userId.ToLower()).Select(x => x.Password).FirstOrDefaultAsync();
if (Hash.Verify(Password.DefaultPassword, passwordHash))
{
context.Response.Redirect("/User/NewPassword");
}
}
}
await next(context);
}
}
and now I can get rid the check in my other controller, and my Login will just look like this:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login([FromForm] LoginViewModel vm, string returnUrl = null)
{
if (ModelState.IsValid)
{
var result = await repository.Login(vm);
if (result.IsSuccess)
{
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, result.Data,
new AuthenticationProperties
{
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddDays(1),
IsPersistent = false
});
return RedirectToLocal(returnUrl);
}
else
{
ViewBag.ErrorMessage = result.ErrorMessage;
}
}
return View(vm);
}
Hope this help anyone who want to achieve the same goals.
Thanks!

Forcing user logout if account is expired

I'm trying to force the user to logout if his account is expired. I'm using Asp.Net MVC core 3, And here one of the solution I got.
I added new class under Services "ValidateAsync".
public class ValidateAsync
{
public static async Task ValidatingAsync(CookieValidatePrincipalContext context)
{
context = context ?? throw new ArgumentNullException(nameof(context));
var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
{
await RejectPrincipal();
return;
}
UserManager<IdentityUser> userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var user = await userManager.FindByNameAsync(context.Principal.FindFirstValue(ClaimTypes.NameIdentifier));
if (user == null || user.SecurityStamp != context.Principal.FindFirst(new ClaimsIdentityOptions().SecurityStampClaimType)?.Value)
{
await RejectPrincipal();
return;
}
async Task RejectPrincipal()
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}
In Startup class I added inside 'ConfigureServices' method:
services.ConfigureApplicationCookie(options =>
{
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = ValidateAsync
};
}).Configure<SecurityStampValidatorOptions>(options =>
{
options.ValidationInterval = TimeSpan.Zero;
});
And in here I have a background process where it set the user expired, and at the same I want to force him to logout if he is logged in after being expired.
public async Task SetExpired()
{
foreach(var item in _db.Institution)
{
if (item.SubscriptionEndDate != null)
{
if (item.SubscriptionEndDate == DateTime.Today)
{
item.Status = SD.StatusExpired;
//Here I want to check if the user is logged in, then force logout should be done.
Guid securityStamp = Guid.NewGuid();
item.SecurityStamp = securityStamp;
}
}
}
await _db.SaveChangesAsync();
}
I don't know if my approach should work as expected or not. But ValidateAsync in startup class show the following error:
CS0119: 'ValidateAsync' is a type, which is not valid in the giving context.
please any help is much appreciated.
A small misuse. Change
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = ValidateAsync
};
to
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = ValidateAsync.ValidatingAsync
};

Asp.Net Web Api Task deadlock

I'm devoloping a test project based on the ASP.NET Identity open source project
In the specific case, I have an ASP.Net Web Api project where I have extended the CookieAuthenticationProvider and override the ValidateIdentity method.
This is the class I have extended
public sealed class MeteoraInternalCookieAuthenticationProvider : CookieAuthenticationProvider
{
private readonly TimeSpan _validateInterval;
public MeteoraInternalCookieAuthenticationProvider(TimeSpan validateInterval)
: base()
{
_validateInterval = validateInterval;
}
public override async Task ValidateIdentity(CookieValidateIdentityContext context)
{
var currentUtc = DateTimeOffset.UtcNow;
if (context.Options != null && context.Options.SystemClock != null)
{
currentUtc = context.Options.SystemClock.UtcNow;
}
var issuedUtc = context.Properties.IssuedUtc;
// Only validate if enough time has elapsed
var validate = (issuedUtc == null);
if (issuedUtc != null)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
validate = timeElapsed > _validateInterval;
}
if (validate)
{
var manager = context.OwinContext.Get<UserManager>();
var userId = context.Identity.GetUserId<Guid>();
var app = await this.GetApplicationContextForValidateIdentity(context.OwinContext.Get<Guid>(MeteoraOwinSetsKeys.ApplicationId).ToString());
if (manager != null && userId != null)
{
var user = manager.FindById(userId);
var reject = true;
//Refresh the identity if the stamp matches, otherwise reject
if (user != null && manager.SupportsUserSecurityStamp)
{
var securityStamp = context.Identity.FindFirstValue(MeteoraClaimTypes.SecurityStampClaimType);
if (securityStamp == manager.GetSecurityStamp(userId))
{
reject = false;
// Regenerate fresh claims if possible and resign in
var identity = await this.RegenerateIdentity(manager, user, app);
/*
Other Code
*/
}
}
if (reject)
{
context.RejectIdentity();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
}
}
}
}
private async Task<ClaimsIdentity> RegenerateIdentity(UserManager usrMgr, IdentityUser usr, IdentityApplication app)
{
ClaimsIdentity identity = await usrMgr.CreateIdentityAsync(usr, app, OAuthDefaults.AuthenticationType /* default is "Bearer" */).WithCurrentCulture();
identity.AddClaim(new IdentityClaim(MeteoraClaimTypes.ApplicationIdType, app.Id.ToString()));
return identity;
}
/*
Other Methods
*/
}
This application is hosted in IIS and when i do a request the method ValidateIdentity is called.
Then inside the method RegenerateIdentity is called.
RegenerateIdentity uses a UserManager class to generate a new ClaimsIdentity.
Here there is all the code
public class UserManager<TUser, TApplication, ... >
{
public virtual async Task<ClaimsIdentity> CreateIdentityAsync(TUser user, TApplication app, string authenticationType)
{
/* Other Code */
return await ClaimsIdentityFactory.CreateAsync(this, user, app, authenticationType);
}
public virtual async Task<IList<string>> GetRoleNamesAsync(TApplication app, TUser user)
{
/* Other Code */
var userRoleStore = GetUserRoleStore();
return await userRoleStore.GetRoleNamesAsync(app, user).WithCurrentCulture();
}
}
public class ClaimsIdentityFactory<TUser, TApplication, TRole, ... >
{
public virtual async Task<ClaimsIdentity> CreateAsync(UserManager<TUser, TApplication, ...> manager, TUser user, TApplication app, string authenticationType)
{
/* Other Code */
ClaimsIdentity cid = new ClaimsIdentity(authenticationType, UserNameClaimType, RoleClaimType);
cid.AddClaim(new Claim(UserIdClaimType, ConvertIdToString(user.Id), ClaimValueTypes.String));
cid.AddClaim(new Claim(UserNameClaimType, user.UserName, ClaimValueTypes.String));
cid.AddClaim(new Claim(IdentityProviderClaimType, DefaultIdentityProviderClaimValue, ClaimValueTypes.String));
/* Other code */
if (manager.SupportsUserRole)
{
IList<string> roles = await manager.GetRoleNamesAsync(app, user); // *** CALLING THIS METHOD ***
foreach (string roleName in roles)
cid.AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String));
}
/* Other Code */
return cid;
}
}
public abstract class UserStore <TKey, TUser, TApplication ...>
{
public virtual async Task<IList<string>> GetRoleNamesAsync(TApplication app, TUser user)
{
/* Other Code */
return await this.GetRoleNamesAsync(app.Id, user.Id);
}
public virtual async Task<IList<string>> GetRoleNamesAsync(TKey appId, TKey userId)
{
/* Other Code */
var query = from userAppRole in _userApplicationRoles
where userAppRole.User.Id.Equals(userId) && userAppRole.Application.Id.Equals(appId)
join role in _roleStore.DbEntitySet on userAppRole.Role.Id equals role.Id
select role.InvariantName;
return await query.ToListAsync(); // *** DEADLOCK ????? ***
}
}
First is called the method ClaimsIdentityFactory.CreateAsync.
Inside the ClaimsIdentityFactory class CreateAsync method is called manager.GetRoleNamesAsync, which using a EntityFramework-based store calls the userRoleStore.GetRoleNamesAsync method.
Into the UserStore class, I seem to be experimenting with some kind of deadlock situation after calling return await query.ToListAsync(); because the method never returns.
This problem is not present into my UnitTest project, but it occurs on IIS environment.
What could I do to understand what is happening in reality?

how to write XUNIT test with to the controller

When i am running this code entering valid username and password, still it giving me 404.When i debug the code Username is showing null.
How to write unit test to test the variables inside the controller's Login method, to test the Identity is Null or not.(claims Identity added)
I have written the test code for model state.
I am not getting any clue to initiate the test code. It would be great if someone pushes up.
public class AccountsControllers : Controller
{
private readonly ApplicationContext appDbContext;
private readonly IApplicationContext iappDbContext;
private readonly UserManager<ApplicationUser> userManager;
public AccountsControllers(ApplicationContext appDb
,UserManager<ApplicationUser> um,
IApplicationContext iappdb)
{
userManager = um;
appDbContext = appDb;
iappDbContext = iappdb;
}
[Route("api/login")]
[HttpPost
public async Task<IActionResult> Login([FromBody]LoginViewModel credentials)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
if (identity == null)
{
return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
}
var response = new
{
id = identity.Claims.Single(c => c.Type == "id").Value,
auth_token = await jwtFactory.GenerateEncodedToken(credentials.UserName, identity),
expires_in = (int)jwtOptions.ValidFor.TotalSeconds
};
var json = JsonConvert.SerializeObject(response,serializerSettings);
return new OkObjectResult(json);
}
public async Task<ClaimsIdentity> GetClaimsIdentity(string userName,
string password)
{
if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
{
// get the user to verifty
var userToVerify = await userManager.FindByNameAsync(userName);
if (userToVerify != null)
{
// check the credentials
if (await userManager.CheckPasswordAsync(userToVerify, password))
{
return await Task.FromResult(jwtFactory.GenerateClaimsIdentity(userName, userToVerify.Id));
}
}
}
return await Task.FromResult<ClaimsIdentity>(null);
}
}

asp net identity EF

I am having a problem with "cache" in asp .net identity, when I change password, name, any claim, I must restart the application for validate the changes.
I have this in SecurityContext
public class SecurityContext : IdentityDbContext<IdentityUser>
{
public SecurityContext()
: base("Db")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("security");
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<IdentityUser>()
.ToTable("_Users");
modelBuilder.Entity<IdentityRole>()
.ToTable("_Roles");
modelBuilder.Entity<IdentityUserRole>()
.ToTable("_UsersRoles");
modelBuilder.Entity<IdentityUserClaim>()
.ToTable("_UsersClaims");
modelBuilder.Entity<IdentityUserLogin>()
.ToTable("_UsersLogins");
}
}
Login:
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
private readonly string _PublicClientId;
private readonly Func<UserManager<IdentityUser>> _UserManagerFactory;
private readonly Func<RoleManager<IdentityRole>> _RoleManagerFactory;
#region Constructors
public ApplicationOAuthProvider(string publicClientId,
Func<UserManager<IdentityUser>> userManagerFactory,
Func<RoleManager<IdentityRole>> roleManagerFactory
)
{
if (publicClientId == null)
throw new ArgumentNullException("publicClientId");
_PublicClientId = publicClientId;
if (userManagerFactory == null)
throw new ArgumentNullException("userManagerFactory");
_UserManagerFactory = userManagerFactory;
if (roleManagerFactory == null)
throw new ArgumentNullException("roleManagerFactory");
_RoleManagerFactory = roleManagerFactory;
}
#endregion Constructors
#region GrantResourceOwnerCredentials
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
using (var userManager = _UserManagerFactory())
{
using (var roleManager = _RoleManagerFactory())
{
var user = await userManager.FindAsync(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
// Start Login success
var oAuthIdentity = await userManager.CreateIdentityAsync(user, context.Options.AuthenticationType);
var cookiesIdentity = await userManager.CreateIdentityAsync(user, CookieAuthenticationDefaults.AuthenticationType);
// Claims
cookiesIdentity.AddClaim(new Claim(XpClaimTypes.Application, _SessionData.ApplicationName));
// Properties
var properties = CreateProperties(user, roleManager);
var ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
// End Login success
}
}
}
#endregion GrantResourceOwnerCredentials
}
obviating others methods
For example the method for changePassword:
#region Password
[HttpPut]
[Authorize(Roles = AccountRoles.Superadministrador + "," + AccountRoles.Administrador)]
public async Task<IHttpActionResult> Password(SetPasswordBindingModel model)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var identity = await UserManager.FindByNameAsync((Thread.CurrentPrincipal.Identity as ClaimsIdentity).Name);
var user = await UserManager.FindByIdAsync(model.Id);
if (!(
(identity.Roles.Any(x => x.Role.Name == AccountRoles.Superadministrador) && user.Roles.Any(x => x.Role.Name == AccountRoles.Administrador)) ||
(identity.Roles.Any(x => x.Role.Name == AccountRoles.Administrador) && user.Roles.Any(x => x.Role.Name == AccountRoles.Usuario))
))
throw new AuthenticationException();
// Delete password
{
var result = await UserManager.RemovePasswordAsync(model.Id);
var errorResult = GetErrorResult(result);
if (errorResult != null)
return errorResult;
}
// Add password
{
var result = await UserManager.AddPasswordAsync(model.Id, model.Password);
var errorResult = GetErrorResult(result);
if (errorResult != null)
return errorResult;
}
return Ok();
}
#endregion Password
There are the steps I followed:
Login application
Change the password
Logout application
Login with the new password (in table is changed, is correctly the change)
Error with password
Login with older password (the old password in table is not exists)
Login successful
Restart application
The new password now is valid
The same problem is occurred when I change any value in BBDD of asp .net identity
Any Idea please?
Thanks!!
If I recall correctly I add the same issue because one of the contexts was being persisted and the other recreated on every call.
If you check one will not have the correct value from the DB, probably ApplicationOAuthProvider.
Try recreating the context for every call on the ApplicationOAuthProvider.

Categories

Resources