I have a couple of legacy ASP.NET web apps that share a database for ASP.NET Membership. I want to move to a microservices architecture utilizing .NET Core and IdentityServer4 and have the identity server in the new microservices ecosystem to use the existing ASP.NET Membership user store, but .NET Core doesn't appear to support ASP.NET Membership at all.
I currently have a proof of concept stood up involving a web API, identity server and an MVC web app as my client. The identity server implements a subclass of IdentityUser and implements IUserStore/IUserPasswordStore/IUserEmailStore to adapt it to the ASP.NET Membership tables in my existing database. I can register new users and login via my POC MVC client app but these users cannot log into my legacy apps. Conversely, users registered in legacy apps can't log into my POC MVC client. I assume its because my implementation of IPasswordHasher isn't hashing the passwords the same as ASP.NET Membership in my legacy apps.
Below is my code. Any insight into what I might be doing wrong would be greatly appreciated. Security and cryptography are not my strong suit.
Startup.cs
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets<Startup>();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
/* Add CORS policy */
services.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:5003")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
/* Add MVC componenets. */
services.AddMvc();
/* Configure IdentityServer. */
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
// Cookie settings
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
options.Cookies.ApplicationCookie.LoginPath = "/Account/Login";
options.Cookies.ApplicationCookie.LogoutPath = "/Account/Logout";
// User settings
options.User.RequireUniqueEmail = true;
});
/* Add the DbContext */
services.AddDbContext<StoreContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyConnectionString")));
/* Add ASP.NET Identity to use for registration and authentication. */
services.AddIdentity<AspNetMembershipUser, IdentityRole>()
.AddEntityFrameworkStores<StoreContext>()
.AddUserStore<AspNetMembershipUserStore>()
.AddDefaultTokenProviders();
services.AddTransient<IPasswordHasher<AspNetMembershipUser>, AspNetMembershipPasswordHasher>();
/* Add IdentityServer and its components. */
services.AddIdentityServer()
.AddInMemoryCaching()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
/* Configure logging. */
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
if (env.IsDevelopment())
{
loggerFactory.AddDebug();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
/* Configure wwwroot */
app.UseStaticFiles();
/* Configure CORS */
app.UseCors("default");
/* Configure AspNet Identity */
app.UseIdentity();
/* Configure IdentityServer */
app.UseIdentityServer();
/* Configure MVC */
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
AspNetMembershipUser.cs
public class AspNetMembershipUser : IdentityUser
{
public string PasswordSalt { get; set; }
public int PasswordFormat { get; set; }
}
AspNetMembershipUserStore.cs
public class AspNetMembershipUserStore : IUserStore<AspNetMembershipUser>, IUserPasswordStore<AspNetMembershipUser>, IUserEmailStore<AspNetMembershipUser>
{
private readonly StoreContext _dbcontext;
public AspNetMembershipUserStore(StoreContext dbContext)
{
_dbcontext = dbContext;
}
public Task<IdentityResult> CreateAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = new User();
this.Convert(user, dbUser);
_dbcontext.Users.Add(dbUser);
_dbcontext.SaveChanges();
return IdentityResult.Success;
}
catch (Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
public Task<IdentityResult> DeleteAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderUserName == user.NormalizedUserName);
if (dbUser != null)
{
_dbcontext.AspNetUsers.Remove(dbUser.AspNetUser);
_dbcontext.Users.Remove(dbUser);
_dbcontext.SaveChanges();
}
return IdentityResult.Success;
}
catch (Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
public void Dispose()
{
_dbcontext.Dispose();
}
public Task<AspNetMembershipUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderEmailAddress == normalizedEmail);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<AspNetMembershipUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
long lUserId = long.Parse(userId);
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u=> u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.UserId == lUserId);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<AspNetMembershipUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderUserName == normalizedUserName);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<string> GetEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Email);
}
public Task<bool> GetEmailConfirmedAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.EmailConfirmed);
}
public Task<string> GetNormalizedEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedEmail);
}
public Task<string> GetNormalizedUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedUserName);
}
public Task<string> GetPasswordHashAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.PasswordHash);
}
public Task<string> GetUserIdAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Id.ToString());
}
public Task<string> GetUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.UserName);
}
public Task<bool> HasPasswordAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => !string.IsNullOrEmpty(user.PasswordHash));
}
public Task SetEmailAsync(AspNetMembershipUser user, string email, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Email = email);
}
public Task SetEmailConfirmedAsync(AspNetMembershipUser user, bool confirmed, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.EmailConfirmed = confirmed);
}
public Task SetNormalizedEmailAsync(AspNetMembershipUser user, string normalizedEmail, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedEmail = normalizedEmail);
}
public Task SetNormalizedUserNameAsync(AspNetMembershipUser user, string normalizedName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedUserName = normalizedName);
}
public Task SetPasswordHashAsync(AspNetMembershipUser user, string passwordHash, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.PasswordHash = passwordHash);
}
public Task SetUserNameAsync(AspNetMembershipUser user, string userName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.UserName = userName);
}
public Task<IdentityResult> UpdateAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.UserId.ToString() == user.Id);
if (dbUser != null)
{
this.Convert(user, dbUser);
_dbcontext.Users.Update(dbUser);
_dbcontext.SaveChanges();
}
return IdentityResult.Success;
}
catch(Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
private void Convert(User from, AspNetMembershipUser to)
{
to.Id = from.ProviderUserKey.ToString();
to.UserName = from.ProviderUserName;
to.NormalizedUserName = from.ProviderUserName.ToLower();
to.Email = from.ProviderEmailAddress;
to.NormalizedEmail = from.ProviderEmailAddress.ToLower();
to.EmailConfirmed = true;
to.PasswordHash = from.AspNetUser.AspNetMembership.Password;
to.PasswordSalt = from.AspNetUser.AspNetMembership.PasswordSalt;
to.PasswordFormat = from.AspNetUser.AspNetMembership.PasswordFormat;
to.AccessFailedCount = from.AspNetUser.AspNetMembership.FailedPasswordAttemptCount;
to.EmailConfirmed = true;
to.Roles.Clear();
from.UserGroups.ToList().ForEach(ug =>
{
to.Roles.Add(new IdentityUserRole<string>
{
RoleId = ug.GroupId.ToString(),
UserId = ug.UserId.ToString()
});
});
to.PhoneNumber = from.Phone ?? from.ShippingPhone;
to.PhoneNumberConfirmed = !string.IsNullOrEmpty(to.PhoneNumber);
to.SecurityStamp = from.AspNetUser.AspNetMembership.PasswordSalt;
}
private void Convert(AspNetMembershipUser from , User to)
{
AspNetApplication application = _dbcontext.AspNetApplications.First();
to.ProviderUserKey = Guid.Parse(from.Id);
to.ProviderUserName = from.UserName;
to.ProviderEmailAddress = from.Email;
to.InternalEmail = $"c_{Guid.NewGuid().ToString()}#mycompany.com";
to.AccountOwner = "MYCOMPANY";
to.UserStatusId = (int)UserStatus.Normal;
AspNetUser aspNetUser = to.AspNetUser;
if (to.AspNetUser == null)
{
to.AspNetUser = new AspNetUser
{
ApplicationId = application.ApplicationId,
AspNetApplication= application,
AspNetMembership = new AspNetMembership
{
ApplicationId = application.ApplicationId,
AspNetApplication = application
}
};
}
to.AspNetUser.UserId = Guid.Parse(from.Id);
to.AspNetUser.UserName = from.UserName;
to.AspNetUser.LoweredUserName = from.UserName.ToLower();
to.AspNetUser.LastActivityDate = DateTime.UtcNow;
to.AspNetUser.IsAnonymous = false;
to.AspNetUser.ApplicationId = application.ApplicationId;
to.AspNetUser.AspNetMembership.CreateDate = DateTime.UtcNow;
to.AspNetUser.AspNetMembership.Email = from.Email;
to.AspNetUser.AspNetMembership.IsApproved = true;
to.AspNetUser.AspNetMembership.LastLoginDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LastLockoutDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LastPasswordChangedDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LoweredEmail = from.NormalizedEmail.ToLower();
to.AspNetUser.AspNetMembership.Password = from.PasswordHash;
to.AspNetUser.AspNetMembership.PasswordSalt = from.PasswordSalt;
to.AspNetUser.AspNetMembership.PasswordFormat = from.PasswordFormat;
to.AspNetUser.AspNetMembership.IsLockedOut = false;
to.AspNetUser.AspNetMembership.FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.FailedPasswordAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000");
// Merge Groups/Roles
to.UserGroups
.Where(ug => !from.Roles.Any(r => ug.GroupId.ToString() == r.RoleId))
.ToList()
.ForEach(ug => to.UserGroups.Remove(ug));
to.UserGroups
.Join(from.Roles, ug => ug.GroupId.ToString(), r => r.RoleId, (ug, r) => new { To = ug, From = r })
.ToList()
.ForEach(j =>
{
j.To.UserId = long.Parse(j.From.UserId);
j.To.GroupId = int.Parse(j.From.RoleId);
});
from.Roles
.Where(r => !to.UserGroups.Any(ug => ug.GroupId.ToString() == r.RoleId))
.ToList()
.ForEach(r =>
{
to.UserGroups.Add(new UserGroup
{
UserId = long.Parse(from.Id),
GroupId = int.Parse(r.RoleId)
});
});
}
}
AspNetMembershipPasswordHasher.cs
public class AspNetMembershipPasswordHasher : IPasswordHasher<AspNetMembershipUser>
{
private readonly int _saltSize;
private readonly int _bytesRequired;
private readonly int _iterations;
public AspNetMembershipPasswordHasher()
{
this._saltSize = 128 / 8;
this._bytesRequired = 32;
this._iterations = 1000;
}
public string HashPassword(AspNetMembershipUser user, string password)
{
string passwordHash = null;
string passwordSalt = null;
this.HashPassword(password, out passwordHash, ref passwordSalt);
user.PasswordSalt = passwordSalt;
return passwordHash;
}
public PasswordVerificationResult VerifyHashedPassword(AspNetMembershipUser user, string hashedPassword, string providedPassword)
{
// Throw an error if any of our passwords are null
if (hashedPassword == null)
{
throw new ArgumentNullException("hashedPassword");
}
if (providedPassword == null)
{
throw new ArgumentNullException("providedPassword");
}
string providedPasswordHash = null;
if (user.PasswordFormat == 0)
{
providedPasswordHash = providedPassword;
}
else if (user.PasswordFormat == 1)
{
string providedPasswordSalt = user.PasswordSalt;
this.HashPassword(providedPassword, out providedPasswordHash, ref providedPasswordSalt);
}
else
{
throw new NotSupportedException("Encrypted passwords are not supported.");
}
if (providedPasswordHash == hashedPassword)
{
return PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
}
private void HashPassword(string password, out string passwordHash, ref string passwordSalt)
{
byte[] hashBytes = null;
byte[] saltBytes = null;
byte[] totalBytes = new byte[this._saltSize + this._bytesRequired];
if (!string.IsNullOrEmpty(passwordSalt))
{
// Using existing salt.
using (var pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(passwordSalt), this._iterations))
{
saltBytes = pbkdf2.Salt;
hashBytes = pbkdf2.GetBytes(this._bytesRequired);
}
}
else
{
// Generate a new salt.
using (var pbkdf2 = new Rfc2898DeriveBytes(password, this._saltSize, this._iterations))
{
saltBytes = pbkdf2.Salt;
hashBytes = pbkdf2.GetBytes(this._bytesRequired);
}
}
Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, this._saltSize);
Buffer.BlockCopy(hashBytes, 0, totalBytes, this._saltSize, this._bytesRequired);
using (SHA256 hashAlgorithm = SHA256.Create())
{
passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes));
passwordSalt = Convert.ToBase64String(saltBytes);
}
}
}
One of my coworkers was able to help me out. Below is what the hash function should look like. With this change, ASP.NET Identity is able to piggy back on an existing ASP.NET Membership database.
private void HashPassword(string password, out string passwordHash, ref string passwordSalt)
{
byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
byte[] saltBytes = null;
if (!string.IsNullOrEmpty(passwordSalt))
{
saltBytes = Convert.FromBase64String(passwordSalt);
}
else
{
saltBytes = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(saltBytes);
}
}
byte[] totalBytes = new byte[saltBytes.Length + passwordBytes.Length];
Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, saltBytes.Length);
Buffer.BlockCopy(passwordBytes, 0, totalBytes, saltBytes.Length, passwordBytes.Length);
using (SHA1 hashAlgorithm = SHA1.Create())
{
passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes));
}
passwordSalt = Convert.ToBase64String(saltBytes);
}
You can find all the source code on GitHib.
Related
Till .net5 I've been Seeding data using the following in startup.cs file:
SeedData.Seed(_userManager, _roleManager);
And then in a seperate file SeedData.cs, the following code:
public static class SeedData
{
public static void Seed(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
{
SeedRoles(roleManager);
SeedUsers(userManager);
}
private static void SeedUsers(UserManager<IdentityUser> userManager)
{
if(userManager.FindByNameAsync("admin#localhost.com").Result == null)
{
var user = new IdentityUser
{
UserName = "admin#localhost.com",
Email = "admin#localhost.com"
};
var result = userManager.CreateAsync(user, "P#ssword1").Result;
if(result.Succeeded)
{
userManager.AddToRoleAsync(user, "Administrator").Wait();
}
}
}
private static void SeedRoles(RoleManager<IdentityRole> roleManager)
{
if(!roleManager.RoleExistsAsync("Administrator").Result)
{
var role = new IdentityRole
{
Name = "Administrator",
};
var result = roleManager.CreateAsync(role).Result;
}
if(!roleManager.RoleExistsAsync("Employee").Result)
{
var role = new IdentityRole
{
Name = "Employee",
};
var result = roleManager.CreateAsync(role).Result;
}
}
}
Now, how do i do the same with .net6, since it has only program.cs file?
This is what I personally do:
I make an extension to IApplicationBuilder:
public static class ApplicationBuilderExtensions
{
public static async Task<IApplicationBuilder> PrepareDatabase(this IApplicationBuilder app)
{
using var scopedServices = app.ApplicationServices.CreateScope();
var serviceProvider = scopedServices.ServiceProvider;
var data = serviceProvider.GetRequiredService<NeonatologyDbContext>();
data.Database.Migrate();
await SeedAdministratorAsync(serviceProvider);
await SeedDoctorAsync(data, serviceProvider);
return app;
}
Here are the seedings:
private static async Task SeedDoctorAsync(NeonatologyDbContext data, IServiceProvider serviceProvider)
{
if (data.Doctors.Any())
{
return;
}
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = serviceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
var identityRole = new ApplicationRole()
{
Name = DoctorRoleName
};
await roleManager.CreateAsync(identityRole);
var city = await data.Cities.Where(x => x.Name == "Плевен").FirstOrDefaultAsync();
var doctor = new ApplicationUser()
{
Email = DoctorEmail,
UserName = DoctorEmail,
EmailConfirmed = true,
Doctor = new Doctor
{
FirstName = DoctorFirstName,
LastName = DoctorLastName,
PhoneNumber = DoctorPhone,
Address = Address,
Age = DoctorAge,
Biography = Biography,
CityId = city.Id,
City = city,
YearsOfExperience = YearsOfExperience,
Email = DoctorEmail
}
};
await userManager.CreateAsync(doctor, DoctorPassword);
await userManager.AddToRoleAsync(doctor, identityRole.Name);
doctor.Doctor.UserId = doctor.Id;
doctor.Doctor.Image = new Image()
{
RemoteImageUrl = "SomeURL"
};
await data.SaveChangesAsync();
}
private static async Task SeedAdministratorAsync(IServiceProvider serviceProvider)
{
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = serviceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
if (await roleManager.RoleExistsAsync(AdministratorRoleName))
{
return;
}
var identityRole = new ApplicationRole()
{
Name = AdministratorRoleName
};
await roleManager.CreateAsync(identityRole);
const string adminEmail = AdministratorEmail;
const string adminPassword = AdministratorPassword;
var adminUser = new ApplicationUser()
{
Email = adminEmail,
UserName = adminEmail,
EmailConfirmed = true
};
if (await userManager.IsInRoleAsync(adminUser, identityRole.Name))
{
return;
}
await userManager.CreateAsync(adminUser, adminPassword);
await userManager.AddToRoleAsync(adminUser, identityRole.Name);
}
And in the Program.cs I have:
app.PrepareDatabase()
.GetAwaiter()
.GetResult();
The following snippet works for me and seeds the data upon initialization of the application.
This question already has answers here:
What is a NullReferenceException, and how do I fix it?
(27 answers)
Closed last year.
I'm trying to get all users with roles I have done it before and it's working in my other web apps and it's the same code.
I seeded Default Roles and default user with a role and registered other some users.
I get this error :NullReferenceException: Object reference not set to an instance of an object. at line var users = await _userManager.Users.ToListAsync();
I debugged it and the _userManager is null, I tried some solutions and gave the same Exception.
so what did I miss?
Program.CS
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.EntityFrameworkCore;
using EmoloyeeSystemApp.Areas.Identity.Data;
using EmployeeSystemApp.Data;
using EmoloyeeSystemApp.Areas;
//using EmoloyeeSystemApp.Migrations
using Microsoft.AspNetCore.Identity.UI.Services;
//using Employee_System.Models;
using Microsoft.Extensions.DependencyInjection;
using System;
using EmoloyeeSystemApp.Models;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<EmployeeSystemAppContext>(options =>
options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<EmployeeSystemAppUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
})
.AddRoles<IdentityRole>()
.AddDefaultUI()
.AddEntityFrameworkStores<EmployeeSystemAppContext>()
.AddDefaultTokenProviders();
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<EmployeeSystemAppContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
//builder.Services.AddAuthorization(options =>
//{
// //options.AddPolicy("rolecreation", policy => policy.RequireRole("Admin"));
//});
var app = builder.Build();
using (IServiceScope? scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var context = services.GetRequiredService<EmployeeSystemAppContext>();
var userManager = services.GetRequiredService<UserManager<EmployeeSystemAppUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await Seeds.SeedRoles(userManager, roleManager);
await Seeds.SeedUser(userManager, roleManager);
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.Run();
controller:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using EmoloyeeSystemApp.Areas.Identity.Data;
using EmployeeSystemApp.Models;
using EmoloyeeSystemApp.Areas;
namespace EmployeeSystemApp.Controllers
{
public class UsersWithRolesController : Controller
{
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<EmployeeSystemAppUser> _userManager;
public UsersWithRolesController(RoleManager<IdentityRole> roleManager, UserManager<EmployeeSystemAppUser> userManager)
{
roleManager = _roleManager;
userManager = _userManager;
}
private async Task<List<string>> GetUserRoles(EmployeeSystemAppUser user)
{
return new List<string>(await _userManager.GetRolesAsync(user));
}
//get all users with thier were assigned roles
public async Task <IActionResult> Index()
{
var users = await _userManager.Users.ToListAsync();
var usersRoles = new List<UsersWRoles>();
foreach(EmployeeSystemAppUser user in users)
{
var details = new UsersWRoles();
details.UserId = user.Id;
details.FirstName = user.FirstName;
details.LastName = user.LastName;
details.UserName = user.UserName;
details.Roles = await GetUserRoles(user);
}
return View(usersRoles);
}
public async Task<IActionResult> Manage(string userId)
{
//Get the user by Id
ViewBag.userId = userId;
var user = await _userManager.FindByIdAsync(userId);
//define of UserName
ViewBag.UserNanme = user.UserName;
// catch the possibility that there is no userId
if (user == null)
{
ViewBag.Erorr = $"User with Id = {userId} cannot be found";
return View("cannot be found");
}
var model = new List<ManageUsersAndRoles>();
foreach (var role in _roleManager.Roles)
{
//define constructor based on "ManageUsersAndRoles" Model
var usersRolesManage = new ManageUsersAndRoles()
{
RoleId = role.Id,
RoleName = role.Name
};
if (await _userManager.IsInRoleAsync(user, role.Name))
{
usersRolesManage.Selected = true;
}
else
{
usersRolesManage.Selected = false;
}
model.Add(usersRolesManage);
}
return View(model);
}
[HttpPost]
public async Task<IActionResult> Manage(List<ManageUsersAndRoles> model, string userId)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return View();
}
var roles = await _userManager.GetRolesAsync(user);
var result = await _userManager.RemoveFromRolesAsync(user, roles);
if (!result.Succeeded)
{
ModelState.AddModelError("", "Cannot remove user's existing roles");
return View(model);
}
result = await _userManager.AddToRolesAsync(user, model.Where(x => x.Selected).Select(y => y.RoleName));
if (!result.Succeeded)
{
ModelState.AddModelError("", "Cannot add selected roles to user");
return View(model);
}
return RedirectToAction("Index");
}
}
}
Model:
namespace EmployeeSystemApp.Models
{
public class UsersWRoles
{
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public IEnumerable<string> Roles { get; set; }
}
}
it was a mistake in the controller in the constructor
_userManager = userManager;
it was backwards so I preferred to change the constructor to be :
private readonly UserManager<EmployeeSystemAppUser> userManager;
public UsersWithRolesController(RoleManager<IdentityRole> roleManager, UserManager<EmployeeSystemAppUser> userManager)
{
this.userManager = userManager;
}
and Add a missing line here to be :
foreach (EmployeeSystemAppUser user in users)
{
var details = new UsersWRoles();
details.UserId = user.Id;
details.FirstName = user.FirstName;
details.LastName = user.LastName;
details.UserName = user.UserName;
details.Roles = await GetUserRoles(user);
usersRoles.Add(details);
}
Am trying to create a user database that I can modify to suit what my users will need to submit when registering for my service, I've created the database and am able to modify it and include whatever columns I want but I can't seem to access them in my c# code, the only fields that appear are those native to AspNetUsers, I've tried looking at similar questions but I can't seem to grasp the concepts specific to what I need, anyone that can help me get some clarity on this cause am a bit new to working with IdentityUser.
//Registration/Login
public class Identify : IIdentify
{
private readonly UserManager<IdentityUser> _manager;
private readonly Mystery _jwtset;
private readonly DataContext _personality;
public Identify(UserManager<IdentityUser> userManager, Mystery jW, DataContext users)
{
_manager = userManager;
_jwtset = jW;
_personality = users;
}
public async Task<Authentication_result> RegisterAsync(string email, string password, string Username)
{
var exists = await _manager.FindByEmailAsync(email);
if (exists != null)
{
return new Authentication_result
{
Errors = new[] { "User with this email already exists" }
};
}
var newPerson = new IdentityUser()
{
Email = email,
UserName = Username
};
var Creation = await _manager.CreateAsync(newPerson, password);
if (!Creation.Succeeded)
{
return new Authentication_result
{
Errors = new[] { "Invalid user!" }
};
}
return Generate_Authentication_Result(newPerson);
}
public async Task<Authentication_result> LoginAsync(string email, string Password)
{
var exists = await _manager.FindByEmailAsync(email);
if (exists == null)
{
return new Authentication_result
{
Errors = new[] { "User does not exists" }
};
}
var pass_validation = await _manager.CheckPasswordAsync(exists, Password);
if (!pass_validation)
{
return new Authentication_result
{
Errors = new[] { "f78wrvep034rf wrong" }
};
}
return Generate_Authentication_Result(exists);
}
private Authentication_result Generate_Authentication_Result(IdentityUser newPerson)
{
var Tokenhandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_jwtset.Secret);
var TokenDescripter = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, newPerson.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, newPerson.Email),
new Claim("id",newPerson.Id)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = Tokenhandler.CreateToken(TokenDescripter);
return new Authentication_result
{
Success = true,
Token = Tokenhandler.WriteToken(token)
};
}
}
//Controller for the above
public class IdentifyMe : Controller
{
private readonly IIdentify _identify;
public IdentifyMe(IIdentify identifying)
{
_identify = identifying;
}
[HttpPost(Api_Routes.Identity.Register)]
public async Task<IActionResult> Register(UserRegistration register)
{
if (!ModelState.IsValid)
{
return BadRequest(new Unauthenticated
{
Errors = ModelState.Values.SelectMany(x => x.Errors.Select(xx => xx.ErrorMessage))
});
}
var authresponce = await _identify.RegisterAsync(register.Email, register.Password, register.User_Name);
if (!authresponce.Success)
{
return BadRequest(new Unauthenticated
{
Errors = authresponce.Errors
});
}
return Ok(new Authenticated
{
Token = authresponce.Token
});
}
[HttpPost(Api_Routes.Identity.Login)]
public async Task<IActionResult> LoginAsync(User_login login)
{
var authresponce = await _identify.LoginAsync(login.email, login.Password);
if (!authresponce.Success)
{
return BadRequest(new Unauthenticated
{
Errors = authresponce.Errors
});
}
return Ok(new Authenticated
{
Token = authresponce.Token
});
}
}
//Domain object, these are the values I would like to be able to access
public class Users : IdentityUser
{
public string PreferredNet { get; set; }
public int Inactive { get; set; }
public int Active { get; set; }
public int Max_Return { get; set; }
public DateTime Time { get; set; }
}
//Other controller
public ActionResult <IEnumerable<Time_dto>> Getitem(string usernum, int amt, string user, string server)
{
var Total = caller.Getusers();
//This is my attempt to acces the domain object, pitcture below[![Intelisense does not display fields in domain object][1]][1]
var container=Total.Select(x=>x.)
var totalin = _digital.Map<IEnumerable<User_dto>>(Total).Count(x => x.PreferredNet == user);
var totalout= _digital.Map<IEnumerable<User_dto>>(Total).Count(x=>x.PreferredNet== server);
int factor = 1;
var HCD = caller.rates(factor, user, server);
var result = shift;
int retrive = caller.Total(amt, user, server, HCD);
var serials = caller.cards(retrive);
int differential = retrive > serials.Sum() ? retrive serials.Sum() : serials.Sum() - retrive;
int number = serials.Count();
IEnumerable<int> Real_cards=new List<int>();
}
```
[1]: https://i.stack.imgur.com/HEenG.png
When I login using google authentication in the startup I can get the access token.
Startup:
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "",
ClientSecret = "",
Scope = { "" },
Provider = new GoogleOAuth2AuthenticationProvider
{
OnAuthenticated = async context =>
{
context.Identity.AddClaim(new Claim("googletoken", context.AccessToken));
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, "http://www.w3.org/2001/XMLSchema#string"));
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, "http://www.w3.org/2001/XMLSchema#string"));
}
}
});
My custom claim manager:
public class ClaimManager
{
private readonly ClaimsIdentity _user;
public ClaimManager(ClaimsIdentity user)
{
this._user = user;
}
public static string GetAccessToken(ClaimsIdentity user)
{
var claim = user.Claims.Select(c => new { Type = c.Type, Value = c.Value }).FirstOrDefault(c => c.Type == "googletoken");
return claim == null ? null : claim.Value;
}
public static string GetName(ClaimsIdentity user)
{
var claim = user.Claims.Select(c => new { Type = c.Type, Value = c.Value }).FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name");
return claim == null ? null : claim.Value;
}
public static string GetEmail(ClaimsIdentity user)
{
var claim = user.Claims.Select(c => new { Type = c.Type, Value = c.Value }).FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
return claim == null ? null : claim.Value;
}
}
The access token is not persisted in the user claims. How can I persist claims so that they stay in the user session?
Here is the solution I came up with. I am storing the access token just like a regular claim in the database.
In account controller:
public async Task StoreAccessToken(ExternalLoginInfo loginInfo)
{
var user = await UserManager.FindAsync(loginInfo.Login);
if (user != null)
{
var newClaim = loginInfo.ExternalIdentity.Claims.Select(c => new Claim(c.Type, c.Value)).FirstOrDefault(c => c.Type == "googletoken");
if (newClaim != null)
{
var userClaims = await UserManager.GetClaimsAsync(user.Id);
foreach (var userClaim in userClaims.Where(c => c.Type == newClaim.Type).ToList())
await UserManager.RemoveClaimAsync(user.Id, userClaim);
await UserManager.AddClaimAsync(user.Id, newClaim);
}
}
}
ExternalLoginCallback():
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
await StoreAccessToken(loginInfo);
ExternalLoginConfirmation():
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user.Id, info.Login);
if (result.Succeeded)
{
await StoreAccessToken(info);
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
return RedirectToLocal(returnUrl);
}
}
have this (model builder) code on our DbContext.cs
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<IdentityUserLogin>().HasKey<string>(l => l.UserId);
modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id);
modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId });
modelBuilder.Entity<ApplicationUser>().ToTable("ApplicationUser");
Everything works fine except for Authorization/User Roles.
After checking all tables I noticed that IdentityUserRoles table creates 4 columns: RoleId, UserId, IdentityRole_Id and ApplicationUser_Id.
I found out that, IdentityRole_Id and ApplicationUser_Id [Foreign Keys] are mapped or used instead of the RoleId and UserId [Primary Keys]. Unfortunately, identity (Id's) data were inserted in RoleId/UserId column and IdenityRole_Id/ApplicationUser_Id are NULL by default.
Please help.
My Code:
public class RqDbContext : DbContext
{
private const string ConnectionString = "RqDbContext";
public RqDbContext() : base(ConnectionString)
{
}
public static RqDbContext Create()
{
return new RqDbContext();
}
// ----------------------------------------------------------------------
// Data Tables
// ----------------------------------------------------------------------
public DbSet<Quote> Quotes { get; set; }
public DbSet<Booking> Bookings { get; set; }
public DbSet<CompanyAccount> CompanyAccounts { get; set; }
// ----------------------------------------------------------------------
// Security
// ----------------------------------------------------------------------
public DbSet<ApplicationUserExtend> ApplicationUserExtends { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<IdentityUserLogin>().HasKey<string>(l => l.UserId);
modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id);
modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId });
modelBuilder.Entity<ApplicationUser>().ToTable("ApplicationUser");
}
}
public partial class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
//custom+
public virtual ApplicationUserExtend Extend { get; set; }
}
public class ApplicationUserExtend
{
public ApplicationUserExtend()
{
}
[Key]
[Display(Name="Id")]
[XmlAttribute]
public int Id { get; set; }
[Display(Name="Account Id")]
[XmlAttribute]
public int AccountId { get; set; }
[Display(Name="Active Account Id")]
[XmlAttribute]
public int ActiveAccountId { get; set; }
}
public class RqInitializer : System.Data.Entity.DropCreateDatabaseAlways<RqDbContext>
{
protected override void Seed(RqDbContext context)
{
var testData = ReadTestData();
AddIdentityRoles(context, testData);
AddUsers(context, testData);
MvcUtil.SaveChanges(context);
}
private void AddUsers(RqDbContext context, TestDataDo testData)
{
var userStore = new UserStore<ApplicationUser>(context);
var userManager = new UserManager<ApplicationUser>(userStore);
//Roles.Enabled("user","member");
var userIndex = 0;
foreach (var applicationUser in testData.ApplicationUsers)
{
var user = new ApplicationUser
{
UserName = applicationUser.UserName,
Email = applicationUser.Email,
PhoneNumber = applicationUser.PhoneNumber
};
if (userIndex > testData.ApplicationUserExtends.Count)
{
throw new Exception("Make sure you the number of rows in ApplicationUserExtends, matches the number of rows in Users");
}
user.Extend = new ApplicationUserExtend
{
AccountId = testData.ApplicationUserExtends[userIndex++].AccountId
};
userManager.Create(user, applicationUser.Password);
//set User Role
userManager.AddToRole(user.Id, applicationUser.Role);
//context.Users.Add(user);
}
context.SaveChanges();
}
private void AddIdentityRoles(RqDbContext context, TestDataDo testData)
{
var roleStore = new RoleStore<IdentityRole>(context);
var roleManager = new RoleManager<IdentityRole>(roleStore);
foreach (var role in testData.IdentityRoles)
{
var identity = new IdentityRole(role.Name);
roleManager.Create(identity);
}
context.SaveChanges();
}
public static TestDataDo ReadTestData()
{
var xml = GetResource("Rq.Web.App_Specification.Rq-TestData.xml");
return XmlUtil.SerializeFromString<TestDataDo>(xml);
}
private static string GetResource(string file)
{
var assembly = Assembly.GetExecutingAssembly();
return ResourceUtil.GetAsString(assembly, file);
}
}
// Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application.
public class ApplicationUserManager : UserManager<ApplicationUser>
{
public ApplicationUserManager(IUserStore<ApplicationUser> store)
: base(store)
{
}
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<RqDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = true
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
RequireNonLetterOrDigit = true,
RequireDigit = true,
RequireLowercase = true,
RequireUppercase = true,
};
// Configure user lockout defaults
manager.UserLockoutEnabledByDefault = true;
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
manager.MaxFailedAccessAttemptsBeforeLockout = 5;
// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
// You can write your own provider and plug it in here.
manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
{
MessageFormat = "Your security code is {0}"
});
manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
{
Subject = "Security Code",
BodyFormat = "Your security code is {0}"
});
manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
}
return manager;
}
}
// Configure the application sign-in manager which is used in this application.
public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
{
public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager)
: base(userManager, authenticationManager)
{
}
public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
{
return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
}
public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
{
return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
}
}
The code below will fix IdentityUserRoles table Foreign Keys issue.
var user = modelBuilder.Entity<TUser>()
.ToTable("AspNetUsers");
user.HasMany(u => u.Roles).WithRequired().HasForeignKey(ur => ur.UserId);
user.HasMany(u => u.Claims).WithRequired().HasForeignKey(uc => uc.UserId);
user.HasMany(u => u.Logins).WithRequired().HasForeignKey(ul => ul.UserId);
user.Property(u => u.UserName).IsRequired();
modelBuilder.Entity<TUserRole>()
.HasKey(r => new { r.UserId, r.RoleId })
.ToTable("AspNetUserRoles");
modelBuilder.Entity<TUserLogin>()
.HasKey(l => new { l.UserId, l.LoginProvider, l.ProviderKey})
.ToTable("AspNetUserLogins");
modelBuilder.Entity<TUserClaim>()
.ToTable("AspNetUserClaims");
var role = modelBuilder.Entity<TRole>()
.ToTable("AspNetRoles");
role.Property(r => r.Name).IsRequired();
role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId);
I found my answer here. Create ASP.NET Identity tables using SQL script!