Refractor token auth stuff to use identiycore framework

This commit is contained in:
Andrew Song 2020-12-21 09:24:21 -06:00
parent f8d7581a12
commit 8f7df85d49
14 changed files with 377 additions and 50 deletions

View File

@ -8,6 +8,7 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.0" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" NoWarn="NU1605" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" NoWarn="NU1605" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -1,11 +1,10 @@
using System; using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data;
using API.DTOs; using API.DTOs;
using API.Entities; using API.Entities;
using API.Interfaces; using API.Interfaces;
using AutoMapper;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -14,17 +13,25 @@ namespace API.Controllers
{ {
public class AccountController : BaseApiController public class AccountController : BaseApiController
{ {
private readonly DataContext _context; private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ILogger<AccountController> _logger; private readonly ILogger<AccountController> _logger;
private readonly IMapper _mapper;
public AccountController(DataContext context, ITokenService tokenService, IUserRepository userRepository, ILogger<AccountController> logger) public AccountController(UserManager<AppUser> userManager,
SignInManager<AppUser> signInManager,
ITokenService tokenService, IUserRepository userRepository,
ILogger<AccountController> logger,
IMapper mapper)
{ {
_context = context; _userManager = userManager;
_signInManager = signInManager;
_tokenService = tokenService; _tokenService = tokenService;
_userRepository = userRepository; _userRepository = userRepository;
_logger = logger; _logger = logger;
_mapper = mapper;
} }
[HttpPost("register")] [HttpPost("register")]
@ -35,24 +42,21 @@ namespace API.Controllers
{ {
return BadRequest("Username is taken."); return BadRequest("Username is taken.");
} }
var user = _mapper.Map<AppUser>(registerDto);
var result = await _userManager.CreateAsync(user, registerDto.Password);
if (!result.Succeeded) return BadRequest(result.Errors);
var roleResult = await _userManager.AddToRoleAsync(user, "Pleb");
if (!roleResult.Succeeded) return BadRequest(result.Errors);
using var hmac = new HMACSHA512();
var user = new AppUser
{
UserName = registerDto.Username.ToLower(),
PasswordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(registerDto.Password)),
PasswordSalt = hmac.Key,
IsAdmin = registerDto.IsAdmin,
LastActive = DateTime.Now
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
return new UserDto() return new UserDto()
{ {
Username = user.UserName, Username = user.UserName,
Token = _tokenService.CreateToken(user), Token = await _tokenService.CreateToken(user),
IsAdmin = user.IsAdmin IsAdmin = user.IsAdmin
}; };
} }
@ -60,18 +64,15 @@ namespace API.Controllers
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto) public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
{ {
var user = await _userRepository.GetUserByUsernameAsync(loginDto.Username); var user = await _userManager.Users
.SingleOrDefaultAsync(x => x.UserName == loginDto.Username.ToLower());
if (user == null) return Unauthorized("Invalid username"); if (user == null) return Unauthorized("Invalid username");
using var hmac = new HMACSHA512(user.PasswordSalt);
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(loginDto.Password)); var result = await _signInManager
.CheckPasswordSignInAsync(user, loginDto.Password, false);
for (int i = 0; i < computedHash.Length; i++) if (!result.Succeeded) return Unauthorized();
{
if (computedHash[i] != user.PasswordHash[i]) return Unauthorized("Invalid password");
}
// Update LastActive on account // Update LastActive on account
user.LastActive = DateTime.Now; user.LastActive = DateTime.Now;
@ -81,14 +82,14 @@ namespace API.Controllers
return new UserDto() return new UserDto()
{ {
Username = user.UserName, Username = user.UserName,
Token = _tokenService.CreateToken(user), Token = await _tokenService.CreateToken(user),
IsAdmin = user.IsAdmin IsAdmin = user.IsAdmin
}; };
} }
private async Task<bool> UserExists(string username) private async Task<bool> UserExists(string username)
{ {
return await _context.Users.AnyAsync(user => user.UserName == username.ToLower()); return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower());
} }
} }
} }

View File

@ -100,8 +100,5 @@ namespace API.Controllers
return BadRequest("Not Implemented"); return BadRequest("Not Implemented");
} }
} }
} }

View File

@ -1,17 +1,36 @@
using System; using System;
using API.Entities; using API.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace API.Data namespace API.Data
{ {
public class DataContext : DbContext public class DataContext : IdentityDbContext<AppUser, AppRole, int,
IdentityUserClaim<int>, AppUserRole, IdentityUserLogin<int>,
IdentityRoleClaim<int>, IdentityUserToken<int>>
{ {
public DataContext(DbContextOptions options) : base(options) public DataContext(DbContextOptions options) : base(options)
{ {
} }
public DbSet<AppUser> Users { get; set; }
public DbSet<Library> Library { get; set; } public DbSet<Library> Library { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<AppUser>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.User)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
builder.Entity<AppRole>()
.HasMany(ur => ur.UserRoles)
.WithOne(u => u.Role)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
}
} }
} }

View File

@ -16,37 +16,127 @@ namespace API.Data.Migrations
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "5.0.1"); .HasAnnotation("ProductVersion", "5.0.1");
modelBuilder.Entity("API.Entities.AppRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b => modelBuilder.Entity("API.Entities.AppUser", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<DateTime>("Created") b.Property<DateTime>("Created")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("IsAdmin") b.Property<bool>("IsAdmin")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<DateTime>("LastActive") b.Property<DateTime>("LastActive")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<byte[]>("PasswordHash") b.Property<byte[]>("PasswordHash")
.HasColumnType("BLOB"); .HasColumnType("BLOB");
b.Property<byte[]>("PasswordSalt") b.Property<byte[]>("PasswordSalt")
.HasColumnType("BLOB"); .HasColumnType("BLOB");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion") b.Property<uint>("RowVersion")
.IsConcurrencyToken() .IsConcurrencyToken()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName") b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("Users"); b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
}); });
modelBuilder.Entity("API.Entities.FolderPath", b => modelBuilder.Entity("API.Entities.FolderPath", b =>
@ -103,6 +193,109 @@ namespace API.Data.Migrations
b.ToTable("AppUserLibrary"); b.ToTable("AppUserLibrary");
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("API.Entities.AppUserRole", b =>
{
b.HasOne("API.Entities.AppRole", "Role")
.WithMany("UserRoles")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("API.Entities.AppUser", "User")
.WithMany("UserRoles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("API.Entities.FolderPath", b => modelBuilder.Entity("API.Entities.FolderPath", b =>
{ {
b.HasOne("API.Entities.Library", "Library") b.HasOne("API.Entities.Library", "Library")
@ -129,6 +322,52 @@ namespace API.Data.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<int>", b =>
{
b.HasOne("API.Entities.AppRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<int>", b =>
{
b.HasOne("API.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("API.Entities.AppRole", b =>
{
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Navigation("UserRoles");
});
modelBuilder.Entity("API.Entities.Library", b => modelBuilder.Entity("API.Entities.Library", b =>
{ {
b.Navigation("Folders"); b.Navigation("Folders");

24
API/Data/Seed.cs Normal file
View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using API.Entities;
using Microsoft.AspNetCore.Identity;
namespace API.Data
{
public class Seed
{
public static async Task SeedRoles(RoleManager<AppRole> roleManager)
{
var roles = new List<AppRole>
{
new AppRole {Name = "Admin"},
new AppRole {Name = "Pleb"}
};
foreach (var role in roles)
{
await roleManager.CreateAsync(role);
}
}
}
}

10
API/Entities/AppRole.cs Normal file
View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
namespace API.Entities
{
public class AppRole : IdentityRole<int>
{
public ICollection<AppUserRole> UserRoles { get; set; }
}
}

View File

@ -1,18 +1,13 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using API.Entities.Interfaces; using Microsoft.AspNetCore.Identity;
namespace API.Entities namespace API.Entities
{ {
public class AppUser : IHasConcurrencyToken public class AppUser : IdentityUser<int>
{ {
public int Id { get; set; }
public string UserName { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public DateTime Created { get; set; } = DateTime.Now; public DateTime Created { get; set; } = DateTime.Now;
public DateTime LastActive { get; set; } public DateTime LastActive { get; set; }
public bool IsAdmin { get; set; } public bool IsAdmin { get; set; }
@ -20,6 +15,8 @@ namespace API.Entities
[ConcurrencyCheck] [ConcurrencyCheck]
public uint RowVersion { get; set; } public uint RowVersion { get; set; }
public ICollection<AppUserRole> UserRoles { get; set; }
public void OnSavingChanges() public void OnSavingChanges()
{ {

View File

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Identity;
namespace API.Entities
{
public class AppUserRole : IdentityUserRole<int>
{
public AppUser User { get; set; }
public AppRole Role { get; set; }
}
}

View File

@ -1,6 +1,8 @@
using System.Collections; using System.Text;
using System.Text; using API.Data;
using API.Entities;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -11,6 +13,17 @@ namespace API.Extensions
{ {
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config) public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
{ {
services.AddIdentityCore<AppUser>(opt =>
{
// Change password / signin requirements here
opt.Password.RequireNonAlphanumeric = false;
})
.AddRoles<AppRole>()
.AddRoleManager<RoleManager<AppRole>>()
.AddSignInManager<SignInManager<AppUser>>()
.AddRoleValidator<RoleValidator<AppRole>>()
.AddEntityFrameworkStores<DataContext>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {

View File

@ -19,6 +19,8 @@ namespace API.Helpers
CreateMap<AppUser, MemberDto>() CreateMap<AppUser, MemberDto>()
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries)); .AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
CreateMap<RegisterDto, AppUser>();
} }
} }
} }

View File

@ -1,9 +1,10 @@
using API.Entities; using System.Threading.Tasks;
using API.Entities;
namespace API.Interfaces namespace API.Interfaces
{ {
public interface ITokenService public interface ITokenService
{ {
string CreateToken(AppUser user); Task<string> CreateToken(AppUser user);
} }
} }

View File

@ -1,7 +1,9 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data; using API.Data;
using API.Entities;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@ -21,8 +23,10 @@ namespace API
try try
{ {
var context = services.GetRequiredService<DataContext>(); var context = services.GetRequiredService<DataContext>();
var roleManager = services.GetRequiredService<RoleManager<AppRole>>();
// Apply all migrations on startup // Apply all migrations on startup
await context.Database.MigrateAsync(); await context.Database.MigrateAsync();
await Seed.SeedRoles(roleManager);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks;
using API.Entities; using API.Entities;
using API.Interfaces; using API.Interfaces;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;
@ -14,19 +17,25 @@ namespace API.Services
{ {
public class TokenService : ITokenService public class TokenService : ITokenService
{ {
private readonly UserManager<AppUser> _userManager;
private readonly SymmetricSecurityKey _key; private readonly SymmetricSecurityKey _key;
public TokenService(IConfiguration config) public TokenService(IConfiguration config, UserManager<AppUser> userManager)
{ {
_userManager = userManager;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
} }
public string CreateToken(AppUser user) public async Task<string> CreateToken(AppUser user)
{ {
var claims = new List<Claim> var claims = new List<Claim>
{ {
new Claim(JwtRegisteredClaimNames.NameId, user.UserName) new Claim(JwtRegisteredClaimNames.NameId, user.UserName)
}; };
var roles = await _userManager.GetRolesAsync(user);
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);