From 03eef8172da25397e4e92908faf707fafb61dbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Fi=C5=A1era?= Date: Sun, 24 Feb 2019 21:47:13 +0100 Subject: [PATCH] #218 - Integrating identity into API project. --- src/Money.Api/Controllers/UserController.cs | 70 ++++++ .../Controllers/UserLoginController.cs | 64 ----- src/Money.Api/Controllers/ValuesController.cs | 2 +- src/Money.Api/Data/ApplicationDataSeeder.cs | 73 ++++++ src/Money.Api/Data/ApplicationDbContext.cs | 27 +++ ...000000000_CreateIdentitySchema.Designer.cs | 216 +++++++++++++++++ .../00000000000000_CreateIdentitySchema.cs | 219 ++++++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 215 +++++++++++++++++ src/Money.Api/Models/ApplicationUser.cs | 18 ++ .../Models/ClaimsPrincipalExtensions.cs | 18 ++ src/Money.Api/Models/ConnectionStrings.cs | 15 ++ src/Money.Api/Models/JwtOptions.cs | 28 +++ src/Money.Api/Models/LoginRequest.cs | 14 ++ src/Money.Api/Models/LoginResponse.cs | 13 ++ src/Money.Api/Money.Api.csproj | 7 + src/Money.Api/Program.cs | 7 +- src/Money.Api/Startup.cs | 54 +++-- src/Money.Api/appsettings.Development.json | 5 + src/Money.Api/appsettings.json | 11 +- .../Controllers/AccountController.cs | 2 + src/Money.UI.Backend/Startup.cs | 22 +- .../appsettings.Development.json | 10 + 22 files changed, 1019 insertions(+), 91 deletions(-) create mode 100644 src/Money.Api/Controllers/UserController.cs delete mode 100644 src/Money.Api/Controllers/UserLoginController.cs create mode 100644 src/Money.Api/Data/ApplicationDataSeeder.cs create mode 100644 src/Money.Api/Data/ApplicationDbContext.cs create mode 100644 src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs create mode 100644 src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.cs create mode 100644 src/Money.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 src/Money.Api/Models/ApplicationUser.cs create mode 100644 src/Money.Api/Models/ClaimsPrincipalExtensions.cs create mode 100644 src/Money.Api/Models/ConnectionStrings.cs create mode 100644 src/Money.Api/Models/JwtOptions.cs create mode 100644 src/Money.Api/Models/LoginRequest.cs create mode 100644 src/Money.Api/Models/LoginResponse.cs diff --git a/src/Money.Api/Controllers/UserController.cs b/src/Money.Api/Controllers/UserController.cs new file mode 100644 index 00000000..9b957bb4 --- /dev/null +++ b/src/Money.Api/Controllers/UserController.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Money.Models; +using Neptuo; +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; + +namespace Money.Controllers +{ + [Route("api/[controller]/[action]")] + public class UserController : ControllerBase + { + private readonly JwtOptions configuration; + private readonly UserManager userManager; + private readonly JwtSecurityTokenHandler tokenHandler; + + public UserController(IOptions configuration, UserManager userManager, JwtSecurityTokenHandler tokenHandler) + { + Ensure.NotNull(configuration, "configuration"); + Ensure.NotNull(userManager, "userManager"); + Ensure.NotNull(tokenHandler, "tokenHandler"); + this.configuration = configuration.Value; + this.userManager = userManager; + this.tokenHandler = tokenHandler; + } + + [HttpPost] + public async Task Login([FromBody] LoginRequest model) + { + ApplicationUser user = await userManager.FindByNameAsync(model.UserName); + if (user != null) + { + if (await userManager.CheckPasswordAsync(user, model.Password)) + { + var claims = new[] + { + new Claim(ClaimTypes.Name, model.UserName) + }; + + var credentials = new SigningCredentials(configuration.GetSecurityKey(), SecurityAlgorithms.HmacSha256); + var expiry = DateTime.Now.Add(configuration.GetExpiry()); + + var token = new JwtSecurityToken( + configuration.Issuer, + configuration.Issuer, + claims, + expires: expiry, + signingCredentials: credentials + ); + + var response = new LoginResponse() + { + Token = tokenHandler.WriteToken(token) + }; + + return Ok(response); + } + } + + return BadRequest(); + } + } +} diff --git a/src/Money.Api/Controllers/UserLoginController.cs b/src/Money.Api/Controllers/UserLoginController.cs deleted file mode 100644 index ecc6324b..00000000 --- a/src/Money.Api/Controllers/UserLoginController.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.IdentityModel.Tokens; -using Neptuo; -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; - -namespace Money.Controllers -{ - [Route("api/user/login")] - [ApiController] - public class UserLoginController : ControllerBase - { - public class LoginModel - { - public string UserName { get; set; } - public string Password { get; set; } - } - - private readonly IConfiguration configuration; - - public UserLoginController(IConfiguration configuration) - { - Ensure.NotNull(configuration, "configuration"); - this.configuration = configuration.GetSection("Jwt"); - } - - [HttpPost] - public IActionResult Post([FromBody] LoginModel model) - { - if (model.UserName == "admin" && model.Password == "admin") - { - var claims = new[] - { - new Claim(ClaimTypes.Name, model.UserName) - }; - - var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["SecurityKey"])); - var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); - var expiry = DateTime.Now.AddDays(Convert.ToInt32(configuration["ExpiryInDays"])); - - var token = new JwtSecurityToken( - configuration["Issuer"], - configuration["Issuer"], - claims, - expires: expiry, - signingCredentials: creds - ); - - JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); - string serializedToken = handler.WriteToken(token); - - return base.Ok(new { token = serializedToken }); - } - - return BadRequest("Username and password are invalid."); - } - } -} diff --git a/src/Money.Api/Controllers/ValuesController.cs b/src/Money.Api/Controllers/ValuesController.cs index fcf8fdc0..960c7b08 100644 --- a/src/Money.Api/Controllers/ValuesController.cs +++ b/src/Money.Api/Controllers/ValuesController.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Money.Api.Controllers +namespace Money.Controllers { [Route("api/[controller]")] [ApiController] diff --git a/src/Money.Api/Data/ApplicationDataSeeder.cs b/src/Money.Api/Data/ApplicationDataSeeder.cs new file mode 100644 index 00000000..5e383616 --- /dev/null +++ b/src/Money.Api/Data/ApplicationDataSeeder.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Money.Commands; +using Money.Models; +using Neptuo; +using Neptuo.Commands; +using Neptuo.Models.Keys; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Money.Data +{ + public static class ApplicationDataSeeder + { + public static IWebHost SeedData(this IWebHost host) + { + try + { + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + var userManager = services.GetService>(); + var db = services.GetService(); + + db.Database.EnsureCreated(); + + if (!userManager.Users.Any()) + userManager.CreateAsync(new ApplicationUser(ClaimsPrincipalExtensions.DemoUserName), ClaimsPrincipalExtensions.DemoUserPassword).Wait(); + } + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + return host; + } + + public static async Task InitializeAsync(UserManager userManager, ICommandDispatcher commands) + { + IdentityResult userResult = await userManager.CreateAsync( + new ApplicationUser(ClaimsPrincipalExtensions.DemoUserName), + ClaimsPrincipalExtensions.DemoUserPassword + ); + + if (!userResult.Succeeded) + throw Ensure.Exception.InvalidOperation("Unnable to create demo user."); + + ApplicationUser user = await userManager.FindByNameAsync(ClaimsPrincipalExtensions.DemoUserName); + if (user == null) + throw Ensure.Exception.InvalidOperation("Unnable find created demo user."); + + IKey userKey = StringKey.Create(user.Id, "User"); + + await commands.HandleAsync(WrapCommand(userKey, new CreateCurrency("USD", "$"))); + await commands.HandleAsync(WrapCommand(userKey, new CreateCategory("Car", "Gas etc.", Color.FromArgb(255, 145, 206, 234)))); + await commands.HandleAsync(WrapCommand(userKey, new CreateCategory("Home", "DIY", Color.FromArgb(255, 207, 180, 141)))); + await commands.HandleAsync(WrapCommand(userKey, new CreateCategory("Food", "Ingredients for home made meals", Color.FromArgb(255, 155, 237, 144)))); + } + + private static Envelope WrapCommand(IKey userKey, T command) + { + var envelope = Envelope.Create(command); + envelope.Metadata.Add("UserKey", userKey); + return envelope; + } + } +} diff --git a/src/Money.Api/Data/ApplicationDbContext.cs b/src/Money.Api/Data/ApplicationDbContext.cs new file mode 100644 index 00000000..a4362a82 --- /dev/null +++ b/src/Money.Api/Data/ApplicationDbContext.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Money.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Money.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // Customize the ASP.NET Identity model and override the defaults if needed. + // For example, you can rename the ASP.NET Identity table names and more. + // Add your customizations after calling base.OnModelCreating(builder); + } + } +} diff --git a/src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 00000000..a65937ec --- /dev/null +++ b/src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Money.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Money.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasAnnotation("MaxLength", 256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedUserName") + .HasAnnotation("MaxLength", 256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Money.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Money.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Money.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 00000000..bb37eec7 --- /dev/null +++ b/src/Money.Api/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Money.Data.Migrations +{ + public partial class CreateIdentitySchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false), + ConcurrencyStamp = table.Column(nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + LockoutEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + PasswordHash = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + SecurityStamp = table.Column(nullable: true), + TwoFactorEnabled = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_UserId", + table: "AspNetUserRoles", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Money.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Money.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 00000000..065a8875 --- /dev/null +++ b/src/Money.Api/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Money.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { + modelBuilder + .HasAnnotation("ProductVersion", "1.0.0-rc3") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole", b => + { + b.Property("Id"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Name") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .HasName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("RoleId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType"); + + b.Property("ClaimValue"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.Property("LoginProvider"); + + b.Property("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Money.Models.ApplicationUser", b => + { + b.Property("Id"); + + b.Property("AccessFailedCount"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken(); + + b.Property("Email") + .HasAnnotation("MaxLength", 256); + + b.Property("EmailConfirmed"); + + b.Property("LockoutEnabled"); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasAnnotation("MaxLength", 256); + + b.Property("NormalizedUserName") + .HasAnnotation("MaxLength", 256); + + b.Property("PasswordHash"); + + b.Property("PhoneNumber"); + + b.Property("PhoneNumberConfirmed"); + + b.Property("SecurityStamp"); + + b.Property("TwoFactorEnabled"); + + b.Property("UserName") + .HasAnnotation("MaxLength", 256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserClaim", b => + { + b.HasOne("Money.Models.ApplicationUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserLogin", b => + { + b.HasOne("Money.Models.ApplicationUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.EntityFrameworkCore.IdentityRole") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Money.Models.ApplicationUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + } + } +} diff --git a/src/Money.Api/Models/ApplicationUser.cs b/src/Money.Api/Models/ApplicationUser.cs new file mode 100644 index 00000000..b2e4817c --- /dev/null +++ b/src/Money.Api/Models/ApplicationUser.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; + +namespace Money.Models +{ + public class ApplicationUser : IdentityUser + { + public ApplicationUser() + { } + + public ApplicationUser(string userName) + : base(userName) + { } + } +} diff --git a/src/Money.Api/Models/ClaimsPrincipalExtensions.cs b/src/Money.Api/Models/ClaimsPrincipalExtensions.cs new file mode 100644 index 00000000..112db747 --- /dev/null +++ b/src/Money.Api/Models/ClaimsPrincipalExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace Money.Models +{ + public static class ClaimsPrincipalExtensions + { + public const string DemoUserName = "demo"; + public const string DemoUserPassword = "demo"; + + public static bool IsDemo(this ClaimsPrincipal user) => user.Identity.Name == DemoUserName; + public static bool IsDemo(this ApplicationUser user) => user.UserName == DemoUserName; + } +} diff --git a/src/Money.Api/Models/ConnectionStrings.cs b/src/Money.Api/Models/ConnectionStrings.cs new file mode 100644 index 00000000..118790de --- /dev/null +++ b/src/Money.Api/Models/ConnectionStrings.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Money.Models +{ + public class ConnectionStrings + { + public string Application { get; set; } + public string EventSourcing { get; set; } + public string ReadModel { get; set; } + } +} diff --git a/src/Money.Api/Models/JwtOptions.cs b/src/Money.Api/Models/JwtOptions.cs new file mode 100644 index 00000000..5c0e442d --- /dev/null +++ b/src/Money.Api/Models/JwtOptions.cs @@ -0,0 +1,28 @@ +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Money.Models +{ + public class JwtOptions + { + public string Issuer { get; set; } + public string SecurityKey { get; set; } + public int ExpiryInDays { get; set; } + + private SecurityKey securityKey; + + public SecurityKey GetSecurityKey() + { + if (securityKey == null) + securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey)); + + return securityKey; + } + + public TimeSpan GetExpiry() => TimeSpan.FromDays(ExpiryInDays); + } +} diff --git a/src/Money.Api/Models/LoginRequest.cs b/src/Money.Api/Models/LoginRequest.cs new file mode 100644 index 00000000..c4c9be2e --- /dev/null +++ b/src/Money.Api/Models/LoginRequest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Money.Models +{ + public class LoginRequest + { + public string UserName { get; set; } + public string Password { get; set; } + } +} diff --git a/src/Money.Api/Models/LoginResponse.cs b/src/Money.Api/Models/LoginResponse.cs new file mode 100644 index 00000000..e651016f --- /dev/null +++ b/src/Money.Api/Models/LoginResponse.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Money.Models +{ + public class LoginResponse + { + public string Token { get; set; } + } +} diff --git a/src/Money.Api/Money.Api.csproj b/src/Money.Api/Money.Api.csproj index 1f7f5bb4..4619a222 100644 --- a/src/Money.Api/Money.Api.csproj +++ b/src/Money.Api/Money.Api.csproj @@ -3,6 +3,7 @@ netcoreapp2.2 InProcess + Money @@ -11,6 +12,12 @@ + + + + + + diff --git a/src/Money.Api/Program.cs b/src/Money.Api/Program.cs index 03df757a..700a7f7d 100644 --- a/src/Money.Api/Program.cs +++ b/src/Money.Api/Program.cs @@ -5,16 +5,15 @@ using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; +using Money.Data; -namespace Money.Api +namespace Money { public class Program { public static void Main(string[] args) { - CreateWebHostBuilder(args).Build().Run(); + CreateWebHostBuilder(args).Build().SeedData().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => diff --git a/src/Money.Api/Startup.cs b/src/Money.Api/Startup.cs index 14614328..d8bb296e 100644 --- a/src/Money.Api/Startup.cs +++ b/src/Money.Api/Startup.cs @@ -1,18 +1,24 @@ using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; +using Money.Data; +using Money.Models; -namespace Money.Api +namespace Money { public class Startup { @@ -22,11 +28,18 @@ public class Startup public void ConfigureServices(IServiceCollection services) { + ConnectionStrings connectionStrings = Configuration + .GetSection("ConnectionStrings") + .Get(); + + services + .AddDbContext(options => options.UseSqlite(connectionStrings.Application)); + services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { - IConfiguration jwtConfiguration = Configuration.GetSection("Jwt"); + JwtOptions configuration = Configuration.GetSection("Jwt").Get(); options.TokenValidationParameters = new TokenValidationParameters { @@ -34,26 +47,31 @@ public void ConfigureServices(IServiceCollection services) ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, - ValidIssuer = jwtConfiguration["Issuer"], - ValidAudience = jwtConfiguration["Issuer"], - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfiguration["SecurityKey"])) + ValidIssuer = configuration.Issuer, + ValidAudience = configuration.Issuer, + IssuerSigningKey = configuration.GetSecurityKey() }; - options.Events = new JwtBearerEvents() - { - OnAuthenticationFailed = c => - { - c.NoResult(); - - c.Response.StatusCode = 401; - c.Response.ContentType = "text/plain"; + options.SaveToken = true; + }); - return c.Response.WriteAsync("There was an issue authorizing you."); - } - }; + services + .AddAuthorization(options => + { + options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme) + .RequireAuthenticatedUser() + .Build(); }); + + services + .AddIdentityCore(options => Configuration.GetSection("Identity").GetSection("Password").Bind(options.Password)) + .AddEntityFrameworkStores(); + + services + .AddRouting(options => options.LowercaseUrls = true) + .AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + services.AddTransient(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) diff --git a/src/Money.Api/appsettings.Development.json b/src/Money.Api/appsettings.Development.json index 05894414..f2d8fe0d 100644 --- a/src/Money.Api/appsettings.Development.json +++ b/src/Money.Api/appsettings.Development.json @@ -11,5 +11,10 @@ "SecurityKey": "abcdefghijklmnop", "Issuer": "https://localhost", "ExpiryInDays": 14 + }, + "ConnectionStrings": { + "Application": "Filename=data/Application.db", + "EventSourcing": "Filename=data/EventSourcing.db", + "ReadModel": "Filename=data/ReadModel.db" } } diff --git a/src/Money.Api/appsettings.json b/src/Money.Api/appsettings.json index def9159a..61c25639 100644 --- a/src/Money.Api/appsettings.json +++ b/src/Money.Api/appsettings.json @@ -4,5 +4,14 @@ "Default": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Identity": { + "Password": { + "RequireDigit": false, + "RequiredLength": 4, + "RequireLowercase": false, + "RequireNonAlphanumeric": false, + "RequireUppercase": false + } + } } diff --git a/src/Money.UI.Backend/Controllers/AccountController.cs b/src/Money.UI.Backend/Controllers/AccountController.cs index b9757a95..45d58c84 100644 --- a/src/Money.UI.Backend/Controllers/AccountController.cs +++ b/src/Money.UI.Backend/Controllers/AccountController.cs @@ -67,6 +67,8 @@ public async Task Login(LoginViewModel model, string returnUrl = return View(model); } + + [HttpGet] [AllowAnonymous] public async Task LoginWithRecoveryCode(string returnUrl = null) diff --git a/src/Money.UI.Backend/Startup.cs b/src/Money.UI.Backend/Startup.cs index 31da3737..bad1c987 100644 --- a/src/Money.UI.Backend/Startup.cs +++ b/src/Money.UI.Backend/Startup.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Policy; using Microsoft.AspNetCore.Blazor.Server; @@ -51,13 +52,26 @@ public void ConfigureServices(IServiceCollection services) }); }); - services.AddAuthentication().AddCookie(); + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => options.Events = new JwtBearerEvents() + { + OnAuthenticationFailed = c => + { + c.NoResult(); + + c.Response.StatusCode = 401; + c.Response.ContentType = "text/plain"; + + return c.Response.WriteAsync("There was an issue authorizing you."); + } + }); services.AddAuthorization(options => { options.AddPolicy("Api", policy => { - policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme); + policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); policy.RequireAuthenticatedUser(); }); }); @@ -95,7 +109,9 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.UseExceptionHandler("/error"); } - app.UseStaticFiles(); + app.UseStatusCodePages(); + + //app.UseStaticFiles(); app.UseAuthentication(); diff --git a/src/Money.UI.Backend/appsettings.Development.json b/src/Money.UI.Backend/appsettings.Development.json index fa8ce71a..f2d8fe0d 100644 --- a/src/Money.UI.Backend/appsettings.Development.json +++ b/src/Money.UI.Backend/appsettings.Development.json @@ -6,5 +6,15 @@ "System": "Information", "Microsoft": "Information" } + }, + "Jwt": { + "SecurityKey": "abcdefghijklmnop", + "Issuer": "https://localhost", + "ExpiryInDays": 14 + }, + "ConnectionStrings": { + "Application": "Filename=data/Application.db", + "EventSourcing": "Filename=data/EventSourcing.db", + "ReadModel": "Filename=data/ReadModel.db" } }