Integrating ASP.NET Core Identity with WebServices and a Custom Identity Provider

22 hours ago 1
ARTICLE AD BOX

This is long, and some of the things I am not 100% sure about.

I have written a custom ASP.NET Core identity provider for a customer. Their user system is at least 20 years old and is deeply ingrained into their products, so it is not going to be changed to an ASP.NET Core Identity user.

Anyway, everythinh works fine there. I need to expand that to a allow for remote login via a mobile app. I'm working on a set of web services. I have a login working that returns a jwt token, an expiration date, and a userid. I can decode via jwt.io and see the expected data.

Here is my login controller. While I have a rolemanager, I don't use it. FSMUser is my custom user class. I can call this from a mobile app as well as Postman and get back data.

[Route("api/[controller]")] [ApiController] public class LoginController : ControllerBase { private readonly SignInManager<FSMUser> _signInManager; private readonly UserManager<FSMUser> _userManager; private readonly RoleManager<IdentityRole> _roleManager; private readonly IConfiguration _configuration; public LoginController( SignInManager<FSMUser> signInManager, UserManager<FSMUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration) { _signInManager = signInManager; _userManager = userManager; _roleManager = roleManager; _configuration = configuration; } [HttpPost] public async Task<IActionResult> Login([FromBody] Login model) { var user = await _userManager.FindByNameAsync(model.Username); if (user != null && await _userManager.CheckPasswordAsync(user, model.Password)) { //var userRoles = await _userManager.GetRolesAsync(user); var authClaims = new List<Claim> { new Claim(ClaimTypes.Name, user.UserName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; authClaims.Add(new Claim("UserId", user.Id)); var token = GetToken(authClaims); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token), expiration = token.ValidTo, Id = user.Id.Trim(), }); } return Unauthorized(); } private JwtSecurityToken GetToken(List<Claim> authClaims) { var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(s: _configuration["Jwt:Key"])); var token = new JwtSecurityToken( issuer: _configuration["Jwt:ValidIssuer"], audience: _configuration["Jwt:ValidAudience"], expires: DateTime.Now.AddYears(30), claims: authClaims, signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256) ); return token; } }

Here is my web service call to get user info. Unfortunately, I am getting back a "401 Unauthorized" http error code. This is where I am completely confused at. I've tried a bunch of things, but I've never understood what is going on. In thinking through this, I've kinda decided that the problem is that there is a method within my custom provider that I need to implement.

Is there something that someone can think of in this area?

[Route("api/[controller]")] [ApiController] [Authorize] public class UserInfoController : ControllerBase { public UserInfoController() { } [HttpGet] public async Task<IActionResult> Get() { var identity = HttpContext.User.Identity as ClaimsIdentity; if (identity == null) { return BadRequest("User ID not found in claims."); } var userClaims = identity.Claims; var userName = userClaims.FirstOrDefault()?.Value; var da = new UserManagement(); var user = await da.UserInfoAsync(userName); return Ok(new { UserId = user?.Userid, CustNo = user?.Custno.Trim(), CustName = user?.Custname.Trim(), FirstName = user?.Firstname.Trim(), LastName = user?.Lastname.Trim(), Initial = user?.Initial.Trim(), Honorific = user?.Honorific.Trim(), }); } }

Here are my custom provider classes

public class FSMUser : IdentityUser { #pragma warning disable CS8764 // Nullability of return type doesn't match overridden member (possibly because of nullability attributes). public override string? Id { get; set; } #pragma warning restore CS8764 // Nullability of return type doesn't match overridden member (possibly because of nullability attributes). public override string? UserName { get; set; } public override string? NormalizedUserName { get; set; } public string? Password { get; set; } public override string? PasswordHash { get; set; } } public class FSMUserStore : IUserStore<FSMUser>, IUserPasswordStore<FSMUser> { private bool disposedValue; public async Task<IdentityResult> CreateAsync(FSMUser user, CancellationToken cancellationToken) { var ctx = new PoaCsmContext(); var fsmu = new Arcustmr(); fsmu.Email = user?.UserName; fsmu.Userid = user?.UserName; fsmu.Code = user?.PasswordHash; ctx.Arcustmrs.Add(fsmu); await ctx.SaveChangesAsync(); return IdentityResult.Success; } public async Task<IdentityResult> DeleteAsync(FSMUser user, CancellationToken cancellationToken) { var ctx = new PoaCsmContext(); var users = await (from u in ctx.Arcustmrs where u.Email == user.UserName select u).ToListAsync(); ctx.Arcustmrs.RemoveRange(users); await ctx.SaveChangesAsync(); return IdentityResult.Success; } public async Task<FSMUser?> FindByIdAsync(string userId, CancellationToken cancellationToken) { var ctx = new PoaCsmContext(); var user = await (from c in ctx.Arcustmrs where c.Custno == userId select new FSMUser() { Id = c.Custno, UserName = c.Email, NormalizedUserName = c.Email, PasswordHash = c.Code }).FirstOrDefaultAsync(); return user; } public async Task<FSMUser?> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { var ctx = new PoaCsmContext(); var user = await (from c in ctx.Arcustmrs where c.Email == normalizedUserName select new FSMUser() { Id = c.Custno, NormalizedUserName = c.Email, UserName = c.Email, PasswordHash = c.Code }).FirstOrDefaultAsync(); return user; } public Task<string?> GetNormalizedUserNameAsync(FSMUser user, CancellationToken cancellationToken) { return Task.FromResult(user.NormalizedUserName); } public Task<string> GetUserIdAsync(FSMUser user, CancellationToken cancellationToken) { return Task.FromResult(user.Id); } public Task<string?> GetUserNameAsync(FSMUser user, CancellationToken cancellationToken) { return Task.FromResult(user.UserName); } public Task SetNormalizedUserNameAsync(FSMUser user, string? normalizedName, CancellationToken cancellationToken) { user.NormalizedUserName = normalizedName; return Task.CompletedTask; } public Task SetUserNameAsync(FSMUser user, string? userName, CancellationToken cancellationToken) { user.UserName = userName; return Task.CompletedTask; } public Task<IdentityResult> UpdateAsync(FSMUser user, CancellationToken cancellationToken) { var ctx = new PoaCsmContext(); return Task.FromResult(IdentityResult.Success); } protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects) } // TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null disposedValue = true; } } // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources // ~FSMUserStore() // { // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method // Dispose(disposing: false); // } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } public Task SetPasswordHashAsync(FSMUser user, string? passwordHash, CancellationToken cancellationToken) { var ctx = new PoaCsmContext(); var us = (from u in ctx.Arcustmrs where u.Email == user.UserName select u).SingleOrDefault(); if (us != null) { us.Code = passwordHash; ctx.SaveChanges(); } return Task.CompletedTask; } public Task<string?> GetPasswordHashAsync(FSMUser user, CancellationToken cancellationToken) { return Task.FromResult(user.PasswordHash); } public async Task<bool> HasPasswordAsync(FSMUser user, CancellationToken cancellationToken) { var result = false; var ctx = new PoaCsmContext(); var email = user.UserName; var use = await (from u in ctx.Arcustmrs where u.Email.Contains(email) select u).SingleOrDefaultAsync(cancellationToken: cancellationToken); if (use != null) { if((use.Code == null) || (String.IsNullOrEmpty(use.Code))) { result = false; } else { result = true; } } else { result = false; } return result; } public async Task<List<string>> GetRolesAsync(FSMUser user) { var rl = new List<string>(); rl.Add("User"); return rl; } }

Here is my program.cs file for my ASP.NET Core web services project.

var builder = WebApplication.CreateBuilder(args); ConfigurationManager configuration = builder.Configuration; // Add services to the container. // For Entity Framework builder.Services.AddDbContext<Portal.Models.PoaCsmContext>(options => options.UseSqlServer(configuration.GetConnectionString("FSMDatabaseConnection"))); builder.Services.AddScoped<IUserStore<FSMUser>, FSMUserStore>(); builder.Services.AddIdentity<FSMUser, IdentityRole>() .AddEntityFrameworkStores<PoaCsmContext>() .AddDefaultTokenProviders(); // Adding Authentication builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) // Adding Jwt Bearer .AddJwtBearer(options => { options.SaveToken = true; options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = true, ValidateAudience = true, ValidAudience = configuration["Jwt:ValidAudience"], ValidIssuer = configuration["Jwt:ValidIssuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])) }; }); builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
Read Entire Article