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.
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();
}
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?
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);
}
}
I've already implemented the basic Web API protection via IdentityServer4 based on this.
The demo is based on in-memory data. And most of tutorials are based on EF Core implementation for user data. As I searched there was a IUserService in IdentityServer3 which is now missing in version 4.
builder.AddInMemoryClients(Clients.Get());
builder.AddInMemoryScopes(Scopes.Get());
builder.AddInMemoryUsers(Users.Get());
How can I retrieve my user data from an EF6 store?
In Startup.cs, do this
builder.Services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
builder.Services.AddTransient<IProfileService, ProfileService>();
Here is a sample of ResourceOwnerPasswordValidator and ProfileService
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private MyUserManager _myUserService { get; set; }
public ResourceOwnerPasswordValidator()
{
_myUserService = new MyUserManager();
}
public async Task<CustomGrantValidationResult> ValidateAsync(string userName, string password, ValidatedTokenRequest request)
{
var user = await _myUserService.FindByNameAsync(userName);
if (user != null && await _myUserService.CheckPasswordAsync(user, password))
{
return new CustomGrantValidationResult(user.EmailAddress, "password");
}
return new CustomGrantValidationResult("Invalid username or password");
}
}
public class ProfileService : IProfileService
{
MyUserManager _myUserManager;
public ProfileService()
{
_myUserManager = new MyUserManager();
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var sub = context.Subject.FindFirst("sub")?.Value;
if (sub != null)
{
var user = await _myUserManager.FindByIdAsync(sub);
var cp = await getClaims(user);
var claims = cp.Claims;
if (context.AllClaimsRequested == false ||
(context.RequestedClaimTypes != null && context.RequestedClaimTypes.Any()))
{
claims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToArray().AsEnumerable();
}
context.IssuedClaims = claims;
}
}
public Task IsActiveAsync(IsActiveContext context)
{
return Task.FromResult(0);
}
private async Task<ClaimsPrincipal> getClaims(CustomerSite user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = await _myUserManager.GetUserIdAsync(user);
var userName = await _myUserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity();
id.AddClaim(new Claim(JwtClaimTypes.Id, userId));
id.AddClaim(new Claim(JwtClaimTypes.PreferredUserName, userName));
var roles = await _myUserManager.GetRolesAsync(user);
foreach (var roleName in roles)
{
id.AddClaim(new Claim(JwtClaimTypes.Role, roleName));
}
id.AddClaims(await _myUserManager.GetClaimsAsync(user));
return new ClaimsPrincipal(id);
}
}
I am trying to login using external authentication in mvc application, but I am not able to do that.
I have changed the table name from AspNetUserLogins to LMS_UserLogins and updated also in context class but still it is giving me error as follows
Invalid object name 'AspNetUserLogins'.
Below is my context class
public class LMSContext : IdentityDbContext<ApplicationUser, ApplicationRole, int>
{
private readonly string _strConnectionString;
private readonly AppTenant tenant;
public LMSContext(AppTenant tenant)
{
this.tenant = tenant;
}
public LMSContext(String ConnectionString)
{
_strConnectionString = ConnectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(tenant.ConnectionString);
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ApplicationUser>().ToTable("LMS_User").HasKey(p => new { p.Id });
modelBuilder.Entity<ApplicationRole>().ToTable("LMS_Roles").HasKey(p => new { p.Id });
modelBuilder.Entity<IdentityUserClaim<int>>().ToTable("LMS_UserClaims").HasKey(p => new { p.Id });
modelBuilder.Entity<IdentityUserLogin<string>>().ToTable("LMS_UserLogins").HasKey(p => new { p.ProviderKey, p.LoginProvider});
modelBuilder.Entity<IdentityUserRole<int>>().ToTable("LMS_UserRoles").HasKey(p => new { p.RoleId, p.UserId });
modelBuilder.Entity<IdentityRoleClaim<int>>().ToTable("LMS_RoleClaims").HasKey(p => new { p.Id });
}
}
And After successful authentication on LinkedIn site, I am trying to login into application using provider name and key received as follows
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null)
{
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return RedirectToAction(nameof(Login));
}
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
_logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = info.LoginProvider;
var email = info.ExternalPrincipal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
Here it is giving me error while signing as Invalid object name 'AspNetUserLogins' though I have mapped the table name in context class.
I am using VS2015, Entity Framework - 7 and Identity Framework - 3.
Why this is so ? What changes I need to do ?
Thanks for the help !