diff --git a/Server/Constants/Authorization.cs b/Server/Constants/Authorization.cs new file mode 100644 index 0000000..75021d6 --- /dev/null +++ b/Server/Constants/Authorization.cs @@ -0,0 +1,15 @@ +namespace Server.Constants; + +public class Authorization +{ + public enum Roles + { + Admin, + User + } + + public const string DefaultUsername = "user"; + public const string DefaultEmail = "user@email.com"; + public const string DefaultPassword = "125ASgl^%@lsdgjk!@#%^12eas"; + public const Roles DefaultRole = Roles.User; +} \ No newline at end of file diff --git a/Server/Controllers/AuthenticationController.cs b/Server/Controllers/AuthenticationController.cs new file mode 100644 index 0000000..48a5260 --- /dev/null +++ b/Server/Controllers/AuthenticationController.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc; +using Server.Services; +using SharedModels.Requests; + +namespace Server.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class AuthenticationController : ControllerBase +{ + private readonly IAuthenticationService _authService; + + public AuthenticationController(IAuthenticationService authService) + { + _authService = authService; + } + + [HttpPost("register")] + public async Task RegisterAsync([FromBody] RegistrationRequest model) + { + var result = await _authService.RegisterAsync(model); + + if (!result.succeeded) + { + return BadRequest(result.message); + } + + return Ok(result); + } + + [HttpPost("token")] + public async Task GetTokenAsync(AuthenticationRequest authRequest) + { + var authResponse = await _authService.GetTokenAsync(authRequest); + + if (!authResponse.IsAuthenticated) + { + return BadRequest(authResponse); + } + + return Ok(authResponse); + } +} \ No newline at end of file diff --git a/Server/Controllers/SecuredController.cs b/Server/Controllers/SecuredController.cs new file mode 100644 index 0000000..ce1dc15 --- /dev/null +++ b/Server/Controllers/SecuredController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Server.Controllers; + +[Authorize(Roles = "Admin")] +[Route("api/[controller]")] +[ApiController] +public class SecuredController : ControllerBase +{ + [HttpGet] + public async Task GetSecuredData() + { + return Ok("This Secured Data is available only for Authenticated Users with Admin role."); + } +} \ No newline at end of file diff --git a/Server/Data/ApplicationDbContext.cs b/Server/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..36e4220 --- /dev/null +++ b/Server/Data/ApplicationDbContext.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Server.Models; + +namespace Server.Data; + +public class ApplicationDbContext : IdentityDbContext +{ + public ApplicationDbContext(DbContextOptions options) + : base(options) + { + } +} \ No newline at end of file diff --git a/Server/Data/ApplicationDbContextSeed.cs b/Server/Data/ApplicationDbContextSeed.cs new file mode 100644 index 0000000..12697bb --- /dev/null +++ b/Server/Data/ApplicationDbContextSeed.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Identity; +using Server.Constants; +using Server.Models; + +namespace Server.Data; + +public class ApplicationDbContextSeed +{ + public static async Task SeedEssentialsAsync(UserManager userManager, + RoleManager roleManager) + { + //Seed Roles + await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.Admin.ToString())); + await roleManager.CreateAsync(new IdentityRole(Authorization.Roles.User.ToString())); + + //Seed Default User + var defaultUser = new ApplicationUser + { + UserName = Authorization.DefaultUsername, + Email = Authorization.DefaultEmail, + EmailConfirmed = true, + PhoneNumberConfirmed = true + }; + + if (userManager.Users.All(u => u.Id != defaultUser.Id)) + { + await userManager.CreateAsync(defaultUser, Authorization.DefaultPassword); + await userManager.AddToRoleAsync(defaultUser, Authorization.DefaultRole.ToString()); + } + } +} \ No newline at end of file diff --git a/Server/Migrations/20220831121114_Initial.Designer.cs b/Server/Migrations/20220831121114_Initial.Designer.cs new file mode 100644 index 0000000..193bd20 --- /dev/null +++ b/Server/Migrations/20220831121114_Initial.Designer.cs @@ -0,0 +1,282 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.Data; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20220831121114_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + 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") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Server.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/20220831121114_Initial.cs b/Server/Migrations/20220831121114_Initial.cs new file mode 100644 index 0000000..48cb59a --- /dev/null +++ b/Server/Migrations/20220831121114_Initial.cs @@ -0,0 +1,222 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Server.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + FirstName = table.Column(type: "text", nullable: true), + LastName = table.Column(type: "text", nullable: true), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", nullable: false), + PasswordHash = table.Column(type: "text", nullable: true), + SecurityStamp = table.Column(type: "text", nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true), + PhoneNumber = table.Column(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + 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(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "text", nullable: false), + ClaimType = table.Column(type: "text", nullable: true), + ClaimValue = table.Column(type: "text", nullable: true) + }, + 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(type: "text", nullable: false), + ProviderKey = table.Column(type: "text", nullable: false), + ProviderDisplayName = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", 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(type: "text", nullable: false), + RoleId = table.Column(type: "text", 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.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "text", nullable: false), + LoginProvider = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + 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: "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/Server/Migrations/ApplicationDbContextModelSnapshot.cs b/Server/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..cf2f569 --- /dev/null +++ b/Server/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,280 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Server.Data; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + 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") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Server.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("LastName") + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Server.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Models/ApplicationUser.cs b/Server/Models/ApplicationUser.cs new file mode 100644 index 0000000..2cfd23b --- /dev/null +++ b/Server/Models/ApplicationUser.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace Server.Models; + +public class ApplicationUser : IdentityUser +{ + public string? FirstName { get; set; } + public string? LastName { get; set; } +} \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs index e780900..04ed1d3 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,11 +1,79 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Server.Data; +using Server.Models; +using Server.Services; +using Server.Settings; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => { + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Name = "Authorization", + Description = "Bearer Authentication with JWT Token", + Type = SecuritySchemeType.Http + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "Bearer", + Type = ReferenceType.SecurityScheme + } + }, + new List() + } + }); +}); + +//Configuration from AppSettings +builder.Services.Configure(builder.Configuration.GetSection("Jwt")); +//User Manager Service +builder.Services.AddIdentity().AddEntityFrameworkStores(); +builder.Services.AddScoped(); +//Adding Authentication - JWT +builder.Services.AddAuthentication(options => { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => { + // options.RequireHttpsMetadata = false; + // options.SaveToken = false; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + ValidateAudience = false, + ValidateIssuer = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) + }; + }); +builder.Services.AddAuthorization(); + +//Adding DB Context with PostgreSQL +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); var app = builder.Build(); @@ -18,6 +86,15 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); +// Data seeding +using var scope = app.Services.CreateScope(); +var userManager = (UserManager)scope.ServiceProvider.GetService(typeof(UserManager))!; +var roleManager = (RoleManager)scope.ServiceProvider.GetService(typeof(RoleManager))!; +await ApplicationDbContextSeed.SeedEssentialsAsync(userManager, roleManager); + app.MapControllers(); +app.UseAuthentication(); +app.UseAuthorization(); + app.Run(); \ No newline at end of file diff --git a/Server/Server.csproj b/Server/Server.csproj index 162ecee..c048fdc 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -10,15 +10,18 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - - - - - + diff --git a/Server/Services/AuthenticationService.cs b/Server/Services/AuthenticationService.cs new file mode 100644 index 0000000..5ec6a6c --- /dev/null +++ b/Server/Services/AuthenticationService.cs @@ -0,0 +1,115 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Server.Constants; +using Server.Models; +using Server.Settings; +using SharedModels.Requests; +using SharedModels.Responses; + +namespace Server.Services; + +public class AuthenticationService : IAuthenticationService +{ + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + private readonly Jwt _jwt; + + public AuthenticationService(UserManager userManager, RoleManager roleManager, IOptions jwt) + { + _userManager = userManager; + _roleManager = roleManager; + _jwt = jwt.Value; + } + + public async Task<(bool, string)> RegisterAsync(RegistrationRequest regRequest) + { + var userWithSameEmail = await _userManager.FindByEmailAsync(regRequest.Email); + if (userWithSameEmail != null) + { + return (false, $"Email {regRequest.Email} is already registered."); + } + + var user = new ApplicationUser {UserName = regRequest.Username, Email = regRequest.Email}; + + var result = await _userManager.CreateAsync(user, regRequest.Password); + if (result.Succeeded) + { + await _userManager.AddToRoleAsync(user, Authorization.DefaultRole.ToString()); + return (true, $"User registered with email {user.Email}."); + } + else + { + return (false, $"{result.Errors?.First().Description}."); + } + } + + public async Task GetTokenAsync(AuthenticationRequest authRequest) + { + var authResponse = new AuthenticationResponse(); + + var user = await _userManager.FindByEmailAsync(authRequest.Email); + + if (user == null) + { + authResponse.IsAuthenticated = false; + authResponse.Message = $"No accounts registered with {authRequest.Email}."; + return authResponse; + } + + if (!await _userManager.CheckPasswordAsync(user, authRequest.Password)) + { + authResponse.IsAuthenticated = false; + authResponse.Message = $"Incorrect login or password."; + return authResponse; + } + + authResponse.IsAuthenticated = true; + authResponse.Email = user.Email; + authResponse.UserName = user.UserName; + var roles = await _userManager.GetRolesAsync(user); + authResponse.Roles = roles.ToList(); + var jwtSecurityToken = await CreateJwtToken(user); + authResponse.Token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + return authResponse; + } + + private async Task CreateJwtToken(ApplicationUser user) + { + var userClaims = await _userManager.GetClaimsAsync(user); + var roles = await _userManager.GetRolesAsync(user); + + var roleClaims = new List(); + + foreach (var role in roles) + { + roleClaims.Add(new Claim("roles", role)); + } + + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, user.UserName), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Email, user.Email), + new Claim("uid", user.Id) + } + .Union(userClaims) + .Union(roleClaims); + + var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key)); + var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); + + var jwtSecurityToken = new JwtSecurityToken( + issuer: _jwt.Issuer, + audience: _jwt.Audience, + claims: claims, + expires: DateTime.UtcNow.AddMinutes(_jwt.ValidityInMinutes), + signingCredentials: signingCredentials); + + return jwtSecurityToken; + } +} \ No newline at end of file diff --git a/Server/Services/IAuthenticationService.cs b/Server/Services/IAuthenticationService.cs new file mode 100644 index 0000000..c8eaee0 --- /dev/null +++ b/Server/Services/IAuthenticationService.cs @@ -0,0 +1,12 @@ +using Server.Models; +using SharedModels.Requests; +using SharedModels.Responses; + +namespace Server.Services; + +public interface IAuthenticationService +{ + Task<(bool succeeded, string message)> RegisterAsync(RegistrationRequest regRequest); + + Task GetTokenAsync(AuthenticationRequest authRequest); +} \ No newline at end of file diff --git a/Server/Settings/Jwt.cs b/Server/Settings/Jwt.cs new file mode 100644 index 0000000..6b659d6 --- /dev/null +++ b/Server/Settings/Jwt.cs @@ -0,0 +1,9 @@ +namespace Server.Settings; + +public class Jwt +{ + public string Key { get; set; } = null!; + public string Audience { get; set; } = null!; + public string Issuer { get; set; } = null!; + public double ValidityInMinutes { get; set; } +} \ No newline at end of file diff --git a/Server/appsettings.Development.json b/Server/appsettings.Development.json index 0c208ae..4874308 100644 --- a/Server/appsettings.Development.json +++ b/Server/appsettings.Development.json @@ -4,5 +4,14 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ConnectionStrings": { + "DefaultConnection": "host=localhost;database=auto.bus;user id=postgres;password=postgres;" + }, + "Jwt": { + "Key": "Secret which will never be exposed", + "Audience": "Application URL", + "Issuer": "Application URL", + "ValidityInMinutes": 1 } } diff --git a/Server/appsettings.json b/Server/appsettings.json index 10f68b8..23d0f45 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -5,5 +5,14 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "host=localhost;database=auto.bus;user id=postgres;password=postgres;" + }, + "Jwt": { + "Key": "Secret which will never be exposed", + "Audience": "Application URL", + "Issuer": "Application URL", + "ValidityInMinutes": 1 + } } diff --git a/SharedModels/Requests/AuthenticationRequest.cs b/SharedModels/Requests/AuthenticationRequest.cs new file mode 100644 index 0000000..e2ec597 --- /dev/null +++ b/SharedModels/Requests/AuthenticationRequest.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.Requests; + +public class AuthenticationRequest +{ + [Required] + public string Email { get; set; } = null!; + [Required] + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/SharedModels/Requests/RegistrationRequest.cs b/SharedModels/Requests/RegistrationRequest.cs new file mode 100644 index 0000000..0d5083e --- /dev/null +++ b/SharedModels/Requests/RegistrationRequest.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace SharedModels.Requests; + +public class RegistrationRequest +{ + [Required] + public string Username { get; set; } = null!; + [Required] + public string Email { get; set; } = null!; + [Required] + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/SharedModels/Responses/AuthenticationResponse.cs b/SharedModels/Responses/AuthenticationResponse.cs new file mode 100644 index 0000000..4888fee --- /dev/null +++ b/SharedModels/Responses/AuthenticationResponse.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace SharedModels.Responses; + +public class AuthenticationResponse +{ + public string Message { get; set; } = null!; + public bool IsAuthenticated { get; set; } + public string UserName { get; set; } = null!; + public string Email { get; set; } = null!; + public List Roles { get; set; } = null!; + public string Token { get; set; } = null!; +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..87aef9f --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.0", + "rollForward": "latestMajor", + "allowPrerelease": false + } +} \ No newline at end of file