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
};
Related
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();
}
in my app during the first login I'm checking if the user already exists, and if he doesn't I want to redirect him to finish registration to Registration/Register action. I have the following code:
public class OpenIdConnectOptionsPostConfigureOptions
: IPostConfigureOptions<OpenIdConnectOptions>
{
private readonly IHttpClientFactory _httpClientFactory;
public OpenIdConnectOptionsPostConfigureOptions(
IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory ??
throw new ArgumentNullException(nameof(httpClientFactory));
}
public void PostConfigure(string name, OpenIdConnectOptions options)
{
options.Events = new OpenIdConnectEvents()
{
OnTicketReceived = async ticketReceivedContext =>
{
var userId = ticketReceivedContext.Principal.Claims
.FirstOrDefault(c => c.Type == "sub").Value;
var apiClient = _httpClientFactory.CreateClient("BasicAPIClient");
var request = new HttpRequestMessage(
HttpMethod.Head,
$"/api/users/{userId}");
request.SetBearerToken(
ticketReceivedContext.Properties.GetTokenValue("access_token"));
var response = await apiClient.SendAsync(
request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
if(response.StatusCode == HttpStatusCode.NotFound)
{
var claims = new List<Claim>() { new Claim("UserStatus", "NewUser") };
ticketReceivedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
}
}
};
}
}
I couldn't figure out how to do it with redirect so as a workaround I'm adding a claim
var claims = new List<Claim>() { new Claim("UserStatus", "NewUser") };
which I later check
[Authorize]
public IActionResult Login()
{
if (User.HasClaim("UserStatus", "NewUser"))
{
return RedirectToAction("Register", "Home");
}
else
{
return View();
}
}
Is there a better way to do it?
I think you can try like below:
if (response.StatusCode == HttpStatusCode.NotFound)
{
ticketReceivedContext.Response.Redirect("XXX");
ticketReceivedContext.HandleResponse();
}
Redefining it again.
I have a asp.net core (api) solution a.sln which has accountcontroller.cs which allows a user to login to the application. Here is AccountController.cs code having Login method.
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
if (button != "login")
{
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
if (context != null)
{
await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);
return Redirect(model.ReturnUrl);
}
else
{
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
var user = await _userManager.FindByNameOrEmailAsync(model.Username);
if (user != null)
{
if (await _userManager.CheckPasswordAsync(user, model.Password) && !await _userManager.IsEmailConfirmedAsync(user))
{
ModelState.AddModelError("", Messages.UserEmailUnverified(_httpContextAccessor));
}
else if (await _userManager.CheckPasswordAsync(user, model.Password) && !(await _userManager.IsLockedOutAsync(user)))
{
var userRoles = await _userManager.GetRolesAsync(user);
var userClaims = userRoles.Select(x => new Claim(ClaimTypes.Role, x)).ToList();
await _events.RaiseAsync(
new UserLoginSuccessEvent(user.UserName, user.Id, user.UserName));
var rememberMe = _accountOptions.AllowRememberLogin && model.RememberLogin;
var props = new AuthenticationProperties()
{
IsPersistent = rememberMe,
ExpiresUtc = DateTimeOffset.UtcNow.Add(rememberMe ? TimeSpan.FromDays(_accountOptions.RememberMeLoginDurationDays)
: TimeSpan.FromMinutes(_accountOptions.StandardLoginDurationMinutes))
};
userClaims.Add(new Claim("remember_me", model.RememberLogin.ToString()));
var appIdentity = new ClaimsIdentity(userClaims, CookieAuthenticationDefaults.AuthenticationScheme);
HttpContext.User.AddIdentity(appIdentity);
await HttpContext.SignInAsync(user.Id, user.UserName, props, userClaims.ToArray());
//after successful login reset lockout count
await _userManager.ResetAccessFailedCountAsync(user);
bool isAllowedUrl = !_middlewareConf.ClientRedirectUrls.Where(urlToCheck => model.ReturnUrl.Contains(urlToCheck)).IsNullOrEmpty();
if (_interaction.IsValidReturnUrl(model.ReturnUrl) || isAllowedUrl)
{
return Redirect(model.ReturnUrl);
}
return Redirect(_loginConfiguration.DefaultRedirectUrl);
}
else
{
var error = await _accountManager.HandleLockout(user);
ModelState.AddModelError("", error);
}
}
else
{
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, $"Invalid credentials."));
ModelState.AddModelError("", _accountOptions.InvalidCredentialsErrorMessage);
}
}
var vm = await _account.BuildLoginViewModelAsync(model);
return View(vm);
}
In above Login method, we are explicitly adding Claim "remember_me".
After successful login, i am directed to another asp.net core solution where on start.cs i am trying to find that same claim. Here is the code of start.cs.
public void Configuration(IAppBuilder app)
{
var idConfig = IdentityConfiguration.Configuration;
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
app.UseKentorOwinCookieSaver();
//tell app to use Cookies as the default
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
// Use cookie authentication
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies",
ExpireTimeSpan = TimeSpan.FromMinutes(idConfig.CookieExpiresMinutes ?? 60),
SlidingExpiration = idConfig.CookieSlidingExpiration ?? false,
Provider = new CookieAuthenticationProvider
{
OnResponseSignIn = signInContext =>
{
var rememberMeClaim = signInContext.Identity.Claims.FirstOrDefault(c => c.Type == "remember_me");
if (bool.TryParse(rememberMeClaim?.Value, out var rememberMe))
{
if (rememberMe && idConfig.RememberCookieExpiresDays.HasValue)
{
signInContext.CookieOptions.Expires = DateTime.Now.AddDays(idConfig.RememberCookieExpiresDays.Value);
}
}
}
}
});
}
But in above code, i am not able to find the same claim "remember_me".
Am i missing something ?
Instead of adding claims like :-
userClaims.Add(new Claim("remember_me", model.RememberLogin.ToString()));
Add claim like below :-
await _userManager.AddClaimAsync(user, new Claim("remember_me",model.RememberLogin.ToString()));
Now, i am able to get my claim "remember_me".
I have implemented the user store in a ASP.NET Core 2 MVC application. See my implementation code below. I have set up the options in Startup, and have set lockoutOnFailure: true on the PasswordSignInAsync() method in the AccountController.
For some reason the access failed methods are not being called on the user store. And only the "GetLockoutEnabledAsync()" is being called.
The current implementation works great for regular logins. I can sign in without any problem. But when testing failed logins, I am not sure what I am missing to get it to use the lockout and failed counts correctly.
public class MyUser : IdentityUser<int>
{
//...
//AccessFailedCount, LockoutEnd, and LockoutEnabled are apart of IdentityUser
//...
}
public class AccountController : Controller
{
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
//...
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
//...
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddIdentity<MyUser, MyRole>(a =>
{
//...
a.Lockout.AllowedForNewUsers = true;
a.Lockout.DefaultLockoutTimeSpan = new System.TimeSpan(0, 5, 0);
a.Lockout.MaxFailedAccessAttempts = 5;
})
.AddDefaultTokenProviders()
.AddSignInManager<SignInManager<MyUser>>()
.AddUserStore<MyUserStore>()
.AddRoleStore<MyRoleStore>();
//...
}
}
public class MyUserStore : IUserStore<MyUser>, IUserRoleStore<MyUser>,
IUserPasswordStore<MyUser>, IUserEmailStore<MyUser>, IUserLockoutStore<MyUser>
{
//...
#region IUserLockoutStore interface
public async Task<DateTimeOffset?> GetLockoutEndDateAsync(MyUser user, CancellationToken cancellationToken)
{
if (user == null)
{
throw new ArgumentException("Cannot get lockout end date. User is null.");
}
return user.LockoutEnd;
}
public async Task SetLockoutEndDateAsync(MyUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken)
{
if (user == null)
{
throw new ArgumentException("Cannot set lockout end date. User is null.");
}
user.LockoutEnd = lockoutEnd;
}
public async Task<int> IncrementAccessFailedCountAsync(MyUser user, CancellationToken cancellationToken)
{
if (user == null)
{
throw new ArgumentException("Cannot update AccessFailedCount. User is null.");
}
user.AccessFailedCount += 1;
return user.AccessFailedCount;
}
public async Task ResetAccessFailedCountAsync(MyUser user, CancellationToken cancellationToken)
{
if (user == null)
{
throw new ArgumentException("Cannot update AccessFailedCount. User is null.");
}
user.AccessFailedCount = 0;
}
public async Task<int> GetAccessFailedCountAsync(MyUser user, CancellationToken cancellationToken)
{
if (user == null)
{
throw new ArgumentException("Cannot get AccessFailedCount. User is null.");
}
return user.AccessFailedCount;
}
public async Task<bool> GetLockoutEnabledAsync(MyUser user, CancellationToken cancellationToken)
{
if (user == null)
{
throw new ArgumentException("Cannot get LockoutEnabled. User is null.");
}
return user.LockoutEnabled;
}
public async Task SetLockoutEnabledAsync(MyUser user, bool enabled, CancellationToken cancellationToken)
{
if (user == null)
{
throw new ArgumentException("Cannot set LockoutEnabled. User is null.");
}
user.LockoutEnabled = enabled;
}
#endregion
}
This wasn't working because of something I was doing wrong and a bug (so technically 2 things I did wrong).
I was using a user account that didn't exist (misspelled, didn't catch it). Because the account access was handled in the baseline UserManager, I did not get clear messaging.
I also had a mapping problem with AutoMapper, where I was not mapping the access failed count properly between the Identity user and the EF model.
To troubleshoot this, I ended up getting the Identity code from GitHub (https://github.com/aspnet/Identity) and creating "custom" classes for the SignInManager and the UserManager so I could step through the code. Then added them temporarily to the IoC container
services.AddIdentity<MyUser, MyRole>(a =>
{
a.Password.RequiredLength = 10;
a.Password.RequireLowercase = true;
a.Password.RequireUppercase = true;
a.User.AllowedUserNameCharacters = null;
a.User.RequireUniqueEmail = true;
a.Lockout.AllowedForNewUsers = true;
a.Lockout.DefaultLockoutTimeSpan = new System.TimeSpan(0, 5, 0);
a.Lockout.MaxFailedAccessAttempts = 5;
})
.AddDefaultTokenProviders()
.AddSignInManager<MySignInManager>() //<-----
.AddUserManager<MyUserManager>() //<-----
.AddUserStore<MyUserStore>()
.AddRoleStore<MyRoleStore>();
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.