diff --git a/API/API.csproj b/API/API.csproj
index 784cff6b4..52664396f 100644
--- a/API/API.csproj
+++ b/API/API.csproj
@@ -8,6 +8,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs
index a621c1086..a007bfc6c 100644
--- a/API/Controllers/AccountController.cs
+++ b/API/Controllers/AccountController.cs
@@ -1,11 +1,10 @@
using System;
-using System.Security.Cryptography;
-using System.Text;
using System.Threading.Tasks;
-using API.Data;
using API.DTOs;
using API.Entities;
using API.Interfaces;
+using AutoMapper;
+using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@@ -14,17 +13,25 @@ namespace API.Controllers
{
public class AccountController : BaseApiController
{
- private readonly DataContext _context;
+ private readonly UserManager _userManager;
+ private readonly SignInManager _signInManager;
private readonly ITokenService _tokenService;
private readonly IUserRepository _userRepository;
private readonly ILogger _logger;
+ private readonly IMapper _mapper;
- public AccountController(DataContext context, ITokenService tokenService, IUserRepository userRepository, ILogger logger)
+ public AccountController(UserManager userManager,
+ SignInManager signInManager,
+ ITokenService tokenService, IUserRepository userRepository,
+ ILogger logger,
+ IMapper mapper)
{
- _context = context;
+ _userManager = userManager;
+ _signInManager = signInManager;
_tokenService = tokenService;
_userRepository = userRepository;
_logger = logger;
+ _mapper = mapper;
}
[HttpPost("register")]
@@ -35,24 +42,21 @@ namespace API.Controllers
{
return BadRequest("Username is taken.");
}
+
+ var user = _mapper.Map(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()
{
Username = user.UserName,
- Token = _tokenService.CreateToken(user),
+ Token = await _tokenService.CreateToken(user),
IsAdmin = user.IsAdmin
};
}
@@ -60,18 +64,15 @@ namespace API.Controllers
[HttpPost("login")]
public async Task> 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");
-
- 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 (computedHash[i] != user.PasswordHash[i]) return Unauthorized("Invalid password");
- }
+ if (!result.Succeeded) return Unauthorized();
// Update LastActive on account
user.LastActive = DateTime.Now;
@@ -81,14 +82,14 @@ namespace API.Controllers
return new UserDto()
{
Username = user.UserName,
- Token = _tokenService.CreateToken(user),
+ Token = await _tokenService.CreateToken(user),
IsAdmin = user.IsAdmin
};
}
private async Task UserExists(string username)
{
- return await _context.Users.AnyAsync(user => user.UserName == username.ToLower());
+ return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower());
}
}
}
\ No newline at end of file
diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs
index 2ff43ec52..93e3cdf06 100644
--- a/API/Controllers/LibraryController.cs
+++ b/API/Controllers/LibraryController.cs
@@ -100,8 +100,5 @@ namespace API.Controllers
return BadRequest("Not Implemented");
}
-
-
-
}
}
\ No newline at end of file
diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs
index 92ce19955..e29f5758a 100644
--- a/API/Data/DataContext.cs
+++ b/API/Data/DataContext.cs
@@ -1,17 +1,36 @@
using System;
using API.Entities;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace API.Data
{
- public class DataContext : DbContext
+ public class DataContext : IdentityDbContext, AppUserRole, IdentityUserLogin,
+ IdentityRoleClaim, IdentityUserToken>
{
public DataContext(DbContextOptions options) : base(options)
{
}
-
- public DbSet Users { get; set; }
public DbSet Library { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+
+ builder.Entity()
+ .HasMany(ur => ur.UserRoles)
+ .WithOne(u => u.User)
+ .HasForeignKey(ur => ur.UserId)
+ .IsRequired();
+
+ builder.Entity()
+ .HasMany(ur => ur.UserRoles)
+ .WithOne(u => u.Role)
+ .HasForeignKey(ur => ur.RoleId)
+ .IsRequired();
+ }
}
}
\ No newline at end of file
diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs
index c19ace39f..fd6f137ac 100644
--- a/API/Data/Migrations/DataContextModelSnapshot.cs
+++ b/API/Data/Migrations/DataContextModelSnapshot.cs
@@ -16,37 +16,127 @@ namespace API.Data.Migrations
modelBuilder
.HasAnnotation("ProductVersion", "5.0.1");
+ modelBuilder.Entity("API.Entities.AppRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasDatabaseName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles");
+ });
+
modelBuilder.Entity("API.Entities.AppUser", b =>
{
b.Property("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken()
+ .HasColumnType("TEXT");
+
b.Property("Created")
.HasColumnType("TEXT");
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
b.Property("IsAdmin")
.HasColumnType("INTEGER");
b.Property("LastActive")
.HasColumnType("TEXT");
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
b.Property("PasswordHash")
.HasColumnType("BLOB");
b.Property("PasswordSalt")
.HasColumnType("BLOB");
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
b.Property("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
b.Property("UserName")
+ .HasMaxLength(256)
.HasColumnType("TEXT");
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("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("API.Entities.FolderPath", b =>
@@ -103,6 +193,109 @@ namespace API.Data.Migrations
b.ToTable("AppUserLibrary");
});
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("RoleId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ClaimType")
+ .HasColumnType("TEXT");
+
+ b.Property("ClaimValue")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderKey")
+ .HasColumnType("TEXT");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.Property("LoginProvider")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("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 =>
{
b.HasOne("API.Entities.Library", "Library")
@@ -129,6 +322,52 @@ namespace API.Data.Migrations
.IsRequired();
});
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.HasOne("API.Entities.AppRole", null)
+ .WithMany()
+ .HasForeignKey("RoleId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.HasOne("API.Entities.AppUser", null)
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", 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 =>
{
b.Navigation("Folders");
diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs
new file mode 100644
index 000000000..4bc4ebbc6
--- /dev/null
+++ b/API/Data/Seed.cs
@@ -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 roleManager)
+ {
+ var roles = new List
+ {
+ new AppRole {Name = "Admin"},
+ new AppRole {Name = "Pleb"}
+ };
+
+ foreach (var role in roles)
+ {
+ await roleManager.CreateAsync(role);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/API/Entities/AppRole.cs b/API/Entities/AppRole.cs
new file mode 100644
index 000000000..8c0d07f96
--- /dev/null
+++ b/API/Entities/AppRole.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Identity;
+
+namespace API.Entities
+{
+ public class AppRole : IdentityRole
+ {
+ public ICollection UserRoles { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs
index a67272ccd..ea7ad0245 100644
--- a/API/Entities/AppUser.cs
+++ b/API/Entities/AppUser.cs
@@ -1,18 +1,13 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using API.Entities.Interfaces;
+using Microsoft.AspNetCore.Identity;
namespace API.Entities
{
- public class AppUser : IHasConcurrencyToken
+ public class AppUser : IdentityUser
{
- 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 LastActive { get; set; }
public bool IsAdmin { get; set; }
@@ -20,6 +15,8 @@ namespace API.Entities
[ConcurrencyCheck]
public uint RowVersion { get; set; }
+
+ public ICollection UserRoles { get; set; }
public void OnSavingChanges()
{
diff --git a/API/Entities/AppUserRole.cs b/API/Entities/AppUserRole.cs
new file mode 100644
index 000000000..b4c73f87e
--- /dev/null
+++ b/API/Entities/AppUserRole.cs
@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace API.Entities
+{
+ public class AppUserRole : IdentityUserRole
+ {
+ public AppUser User { get; set; }
+ public AppRole Role { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs
index 386efc0f2..9669c6822 100644
--- a/API/Extensions/IdentityServiceExtensions.cs
+++ b/API/Extensions/IdentityServiceExtensions.cs
@@ -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.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
@@ -11,6 +13,17 @@ namespace API.Extensions
{
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
{
+ services.AddIdentityCore(opt =>
+ {
+ // Change password / signin requirements here
+ opt.Password.RequireNonAlphanumeric = false;
+ })
+ .AddRoles()
+ .AddRoleManager>()
+ .AddSignInManager>()
+ .AddRoleValidator>()
+ .AddEntityFrameworkStores();
+
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs
index 4e2f2a100..4e203ab04 100644
--- a/API/Helpers/AutoMapperProfiles.cs
+++ b/API/Helpers/AutoMapperProfiles.cs
@@ -19,6 +19,8 @@ namespace API.Helpers
CreateMap()
.AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries));
+
+ CreateMap();
}
}
}
\ No newline at end of file
diff --git a/API/Interfaces/ITokenService.cs b/API/Interfaces/ITokenService.cs
index e721d9ade..042426964 100644
--- a/API/Interfaces/ITokenService.cs
+++ b/API/Interfaces/ITokenService.cs
@@ -1,9 +1,10 @@
-using API.Entities;
+using System.Threading.Tasks;
+using API.Entities;
namespace API.Interfaces
{
public interface ITokenService
{
- string CreateToken(AppUser user);
+ Task CreateToken(AppUser user);
}
}
\ No newline at end of file
diff --git a/API/Program.cs b/API/Program.cs
index f3a23a6c1..7ed8e7175 100644
--- a/API/Program.cs
+++ b/API/Program.cs
@@ -1,7 +1,9 @@
using System;
using System.Threading.Tasks;
using API.Data;
+using API.Entities;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -21,8 +23,10 @@ namespace API
try
{
var context = services.GetRequiredService();
+ var roleManager = services.GetRequiredService>();
// Apply all migrations on startup
await context.Database.MigrateAsync();
+ await Seed.SeedRoles(roleManager);
}
catch (Exception ex)
{
diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs
index 195af02d2..45673dae8 100644
--- a/API/Services/TokenService.cs
+++ b/API/Services/TokenService.cs
@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
using System.Security.Claims;
using System.Text;
+using System.Threading.Tasks;
using API.Entities;
using API.Interfaces;
+using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;
@@ -14,19 +17,25 @@ namespace API.Services
{
public class TokenService : ITokenService
{
+ private readonly UserManager _userManager;
private readonly SymmetricSecurityKey _key;
- public TokenService(IConfiguration config)
+ public TokenService(IConfiguration config, UserManager userManager)
{
+ _userManager = userManager;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
}
- public string CreateToken(AppUser user)
+ public async Task CreateToken(AppUser user)
{
var claims = new List
{
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);