├── .gitignore ├── .idea └── .idea.JwtRoleAuthentication │ └── .idea │ ├── .gitignore │ ├── dataSources.xml │ ├── encodings.xml │ └── indexLayout.xml ├── README.md └── src ├── .idea └── .idea.JwtRoleAuthentication │ └── .idea │ ├── .gitignore │ ├── .name │ ├── encodings.xml │ ├── indexLayout.xml │ └── vcs.xml ├── Controllers ├── PagesController.cs └── UsersController.cs ├── Data ├── ApplicationDbContext.cs └── Migrations │ ├── 20231125225221_Initial.Designer.cs │ ├── 20231125225221_Initial.cs │ └── ApplicationDbContextModelSnapshot.cs ├── Enums └── Role.cs ├── JwtRoleAuthentication.csproj ├── JwtRoleAuthentication.sln ├── Models ├── ApplicationUser.cs ├── AuthRequest.cs ├── AuthResponse.cs ├── Page.cs ├── PageDto.cs ├── PagesDto.cs └── RegistrationRequest.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Services └── TokenService.cs ├── appsettings.Development.json └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Common IntelliJ Platform excludes 2 | 3 | # User specific 4 | **/.idea/**/workspace.xml 5 | **/.idea/**/tasks.xml 6 | **/.idea/shelf/* 7 | **/.idea/dictionaries 8 | **/.idea/httpRequests/ 9 | 10 | # Sensitive or high-churn files 11 | **/.idea/**/dataSources/ 12 | **/.idea/**/dataSources.ids 13 | **/.idea/**/dataSources.xml 14 | **/.idea/**/dataSources.local.xml 15 | **/.idea/**/sqlDataSources.xml 16 | **/.idea/**/dynamic.xml 17 | 18 | # Rider 19 | # Rider auto-generates .iml files, and contentModel.xml 20 | **/.idea/**/*.iml 21 | **/.idea/**/contentModel.xml 22 | **/.idea/**/modules.xml 23 | 24 | *.suo 25 | *.user 26 | .vs/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | _UpgradeReport_Files/ 30 | [Pp]ackages/ 31 | 32 | Thumbs.db 33 | Desktop.ini 34 | .DS_Store -------------------------------------------------------------------------------- /.idea/.idea.JwtRoleAuthentication/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /projectSettingsUpdater.xml 6 | /contentModel.xml 7 | /modules.xml 8 | /.idea.JwtRoleAuthentication.iml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.JwtRoleAuthentication/.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | postgresql 6 | true 7 | true 8 | org.postgresql.Driver 9 | jdbc:postgresql://localhost:5432/postgres 10 | $ProjectFileDir$ 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/.idea.JwtRoleAuthentication/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.JwtRoleAuthentication/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example Role-Based Authorization Using JWT Tokens in ASP .NET Core 8 2 | 3 | This repository is part of the following guide: 4 | 5 | [Securing an ASP .NET Core Web API using JWT and Role-based Authentication](https://markjames.dev/blog/jwt-authorization-asp-net-core) 6 | -------------------------------------------------------------------------------- /src/.idea/.idea.JwtRoleAuthentication/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /.idea.JwtRoleAuthentication.iml 6 | /contentModel.xml 7 | /projectSettingsUpdater.xml 8 | /modules.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /src/.idea/.idea.JwtRoleAuthentication/.idea/.name: -------------------------------------------------------------------------------- 1 | JwtRoleAuthentication -------------------------------------------------------------------------------- /src/.idea/.idea.JwtRoleAuthentication/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/.idea/.idea.JwtRoleAuthentication/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ../../../AspNet8-jwt-role-auth 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/.idea/.idea.JwtRoleAuthentication/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Controllers/PagesController.cs: -------------------------------------------------------------------------------- 1 | using JwtRoleAuthentication.Data; 2 | using JwtRoleAuthentication.Models; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Asp.Versioning; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace JwtRoleAuthentication.Controllers; 9 | 10 | [ApiVersion( 1.0 )] 11 | [ApiController] 12 | [Route("api/[controller]" )] 13 | public class PagesController : ControllerBase 14 | { 15 | private readonly ApplicationDbContext _dbContext; 16 | 17 | public PagesController(ILogger logger, ApplicationDbContext dbContext) 18 | { 19 | _dbContext = dbContext; 20 | } 21 | 22 | [Authorize (Roles = "Admin")] 23 | [HttpPost("new")] 24 | public async Task> CreatePage(PageDto pageDto) 25 | { 26 | if (!ModelState.IsValid) 27 | { 28 | return BadRequest(ModelState); 29 | } 30 | 31 | var page = new Page 32 | { 33 | Id = pageDto.Id, 34 | Title = pageDto.Title, 35 | Author = pageDto.Author, 36 | Body = pageDto.Body, 37 | }; 38 | 39 | _dbContext.Pages.Add(page); 40 | await _dbContext.SaveChangesAsync(); 41 | 42 | return CreatedAtAction(nameof(GetPage), new { id = page.Id }, page); 43 | } 44 | 45 | 46 | [HttpGet("{id:int}")] 47 | public async Task> GetPage(int id) 48 | { 49 | var page = await _dbContext.Pages.FindAsync(id); 50 | 51 | if (page is null) 52 | { 53 | return NotFound(); 54 | } 55 | 56 | var pageDto = new PageDto 57 | { 58 | Id = page.Id, 59 | Author = page.Author, 60 | Body = page.Body, 61 | Title = page.Title 62 | }; 63 | 64 | return pageDto; 65 | } 66 | 67 | 68 | [HttpGet] 69 | public async Task ListPages() 70 | { 71 | var pagesFromDb = await _dbContext.Pages.ToListAsync(); 72 | 73 | var pagesDto = new PagesDto(); 74 | 75 | foreach (var page in pagesFromDb) 76 | { 77 | var pageDto = new PageDto 78 | { 79 | Id = page.Id, 80 | Author = page.Author, 81 | Body = page.Body, 82 | Title = page.Title 83 | }; 84 | 85 | pagesDto.Pages.Add(pageDto); 86 | } 87 | 88 | return pagesDto; 89 | } 90 | } -------------------------------------------------------------------------------- /src/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using JwtRoleAuthentication.Data; 2 | using JwtRoleAuthentication.Enums; 3 | using JwtRoleAuthentication.Models; 4 | using JwtRoleAuthentication.Services; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace JwtRoleAuthentication.Controllers; 9 | 10 | [ApiController] 11 | [Route("/api/[controller]")] 12 | public class UsersController : ControllerBase 13 | { 14 | private readonly UserManager _userManager; 15 | private readonly ApplicationDbContext _context; 16 | private readonly TokenService _tokenService; 17 | 18 | public UsersController(UserManager userManager, ApplicationDbContext context, 19 | TokenService tokenService, ILogger logger) 20 | { 21 | _userManager = userManager; 22 | _context = context; 23 | _tokenService = tokenService; 24 | } 25 | 26 | 27 | [HttpPost] 28 | [Route("register")] 29 | public async Task Register(RegistrationRequest request) 30 | { 31 | if (!ModelState.IsValid) 32 | { 33 | return BadRequest(ModelState); 34 | } 35 | 36 | var result = await _userManager.CreateAsync( 37 | new ApplicationUser { UserName = request.Username, Email = request.Email, Role = request.Role }, 38 | request.Password! 39 | ); 40 | 41 | if (result.Succeeded) 42 | { 43 | request.Password = ""; 44 | return CreatedAtAction(nameof(Register), new { email = request.Email, role = Role.User }, request); 45 | } 46 | 47 | foreach (var error in result.Errors) 48 | { 49 | ModelState.AddModelError(error.Code, error.Description); 50 | } 51 | 52 | return BadRequest(ModelState); 53 | } 54 | 55 | 56 | [HttpPost] 57 | [Route("login")] 58 | public async Task> Authenticate([FromBody] AuthRequest request) 59 | { 60 | if (!ModelState.IsValid) 61 | { 62 | return BadRequest(ModelState); 63 | } 64 | 65 | var managedUser = await _userManager.FindByEmailAsync(request.Email!); 66 | 67 | if (managedUser == null) 68 | { 69 | return BadRequest("Bad credentials"); 70 | } 71 | 72 | var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password!); 73 | 74 | if (!isPasswordValid) 75 | { 76 | return BadRequest("Bad credentials"); 77 | } 78 | 79 | var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email); 80 | 81 | if (userInDb is null) 82 | { 83 | return Unauthorized(); 84 | } 85 | 86 | var accessToken = _tokenService.CreateToken(userInDb); 87 | await _context.SaveChangesAsync(); 88 | 89 | return Ok(new AuthResponse 90 | { 91 | Username = userInDb.UserName, 92 | Email = userInDb.Email, 93 | Token = accessToken, 94 | }); 95 | } 96 | } -------------------------------------------------------------------------------- /src/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using JwtRoleAuthentication.Enums; 2 | using JwtRoleAuthentication.Models; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace JwtRoleAuthentication.Data; 8 | 9 | public class ApplicationDbContext : IdentityUserContext 10 | { 11 | public DbSet Pages => Set(); 12 | 13 | public ApplicationDbContext (DbContextOptions options) 14 | : base(options) 15 | { 16 | 17 | } 18 | 19 | protected override void OnModelCreating(ModelBuilder modelBuilder) 20 | { 21 | base.OnModelCreating(modelBuilder); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Data/Migrations/20231125225221_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using JwtRoleAuthentication.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 9 | 10 | #nullable disable 11 | 12 | namespace JwtRoleAuthentication.Data.Migrations 13 | { 14 | [DbContext(typeof(ApplicationDbContext))] 15 | [Migration("20231125225221_Initial")] 16 | partial class Initial 17 | { 18 | /// 19 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 20 | { 21 | #pragma warning disable 612, 618 22 | modelBuilder 23 | .HasAnnotation("ProductVersion", "8.0.0") 24 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 25 | 26 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 27 | 28 | modelBuilder.Entity("JwtRoleAuthentication.Models.ApplicationUser", b => 29 | { 30 | b.Property("Id") 31 | .HasColumnType("text"); 32 | 33 | b.Property("AccessFailedCount") 34 | .HasColumnType("integer"); 35 | 36 | b.Property("ConcurrencyStamp") 37 | .IsConcurrencyToken() 38 | .HasColumnType("text"); 39 | 40 | b.Property("Email") 41 | .HasMaxLength(256) 42 | .HasColumnType("character varying(256)"); 43 | 44 | b.Property("EmailConfirmed") 45 | .HasColumnType("boolean"); 46 | 47 | b.Property("LockoutEnabled") 48 | .HasColumnType("boolean"); 49 | 50 | b.Property("LockoutEnd") 51 | .HasColumnType("timestamp with time zone"); 52 | 53 | b.Property("NormalizedEmail") 54 | .HasMaxLength(256) 55 | .HasColumnType("character varying(256)"); 56 | 57 | b.Property("NormalizedUserName") 58 | .HasMaxLength(256) 59 | .HasColumnType("character varying(256)"); 60 | 61 | b.Property("PasswordHash") 62 | .HasColumnType("text"); 63 | 64 | b.Property("PhoneNumber") 65 | .HasColumnType("text"); 66 | 67 | b.Property("PhoneNumberConfirmed") 68 | .HasColumnType("boolean"); 69 | 70 | b.Property("Role") 71 | .HasColumnType("integer"); 72 | 73 | b.Property("SecurityStamp") 74 | .HasColumnType("text"); 75 | 76 | b.Property("TwoFactorEnabled") 77 | .HasColumnType("boolean"); 78 | 79 | b.Property("UserName") 80 | .HasMaxLength(256) 81 | .HasColumnType("character varying(256)"); 82 | 83 | b.HasKey("Id"); 84 | 85 | b.HasIndex("NormalizedEmail") 86 | .HasDatabaseName("EmailIndex"); 87 | 88 | b.HasIndex("NormalizedUserName") 89 | .IsUnique() 90 | .HasDatabaseName("UserNameIndex"); 91 | 92 | b.ToTable("AspNetUsers", (string)null); 93 | }); 94 | 95 | modelBuilder.Entity("JwtRoleAuthentication.Models.Page", b => 96 | { 97 | b.Property("Id") 98 | .ValueGeneratedOnAdd() 99 | .HasColumnType("integer"); 100 | 101 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 102 | 103 | b.Property("Author") 104 | .HasColumnType("text"); 105 | 106 | b.Property("Body") 107 | .HasColumnType("text"); 108 | 109 | b.Property("Title") 110 | .HasColumnType("text"); 111 | 112 | b.HasKey("Id"); 113 | 114 | b.ToTable("Pages"); 115 | }); 116 | 117 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 118 | { 119 | b.Property("Id") 120 | .ValueGeneratedOnAdd() 121 | .HasColumnType("integer"); 122 | 123 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 124 | 125 | b.Property("ClaimType") 126 | .HasColumnType("text"); 127 | 128 | b.Property("ClaimValue") 129 | .HasColumnType("text"); 130 | 131 | b.Property("UserId") 132 | .IsRequired() 133 | .HasColumnType("text"); 134 | 135 | b.HasKey("Id"); 136 | 137 | b.HasIndex("UserId"); 138 | 139 | b.ToTable("AspNetUserClaims", (string)null); 140 | }); 141 | 142 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 143 | { 144 | b.Property("LoginProvider") 145 | .HasColumnType("text"); 146 | 147 | b.Property("ProviderKey") 148 | .HasColumnType("text"); 149 | 150 | b.Property("ProviderDisplayName") 151 | .HasColumnType("text"); 152 | 153 | b.Property("UserId") 154 | .IsRequired() 155 | .HasColumnType("text"); 156 | 157 | b.HasKey("LoginProvider", "ProviderKey"); 158 | 159 | b.HasIndex("UserId"); 160 | 161 | b.ToTable("AspNetUserLogins", (string)null); 162 | }); 163 | 164 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 165 | { 166 | b.Property("UserId") 167 | .HasColumnType("text"); 168 | 169 | b.Property("LoginProvider") 170 | .HasColumnType("text"); 171 | 172 | b.Property("Name") 173 | .HasColumnType("text"); 174 | 175 | b.Property("Value") 176 | .HasColumnType("text"); 177 | 178 | b.HasKey("UserId", "LoginProvider", "Name"); 179 | 180 | b.ToTable("AspNetUserTokens", (string)null); 181 | }); 182 | 183 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 184 | { 185 | b.HasOne("JwtRoleAuthentication.Models.ApplicationUser", null) 186 | .WithMany() 187 | .HasForeignKey("UserId") 188 | .OnDelete(DeleteBehavior.Cascade) 189 | .IsRequired(); 190 | }); 191 | 192 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 193 | { 194 | b.HasOne("JwtRoleAuthentication.Models.ApplicationUser", null) 195 | .WithMany() 196 | .HasForeignKey("UserId") 197 | .OnDelete(DeleteBehavior.Cascade) 198 | .IsRequired(); 199 | }); 200 | 201 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 202 | { 203 | b.HasOne("JwtRoleAuthentication.Models.ApplicationUser", null) 204 | .WithMany() 205 | .HasForeignKey("UserId") 206 | .OnDelete(DeleteBehavior.Cascade) 207 | .IsRequired(); 208 | }); 209 | #pragma warning restore 612, 618 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Data/Migrations/20231125225221_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 4 | 5 | #nullable disable 6 | 7 | namespace JwtRoleAuthentication.Data.Migrations 8 | { 9 | /// 10 | public partial class Initial : Migration 11 | { 12 | /// 13 | protected override void Up(MigrationBuilder migrationBuilder) 14 | { 15 | migrationBuilder.CreateTable( 16 | name: "AspNetUsers", 17 | columns: table => new 18 | { 19 | Id = table.Column(type: "text", nullable: false), 20 | Role = table.Column(type: "integer", nullable: false), 21 | UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), 22 | NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), 23 | Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), 24 | NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), 25 | EmailConfirmed = table.Column(type: "boolean", nullable: false), 26 | PasswordHash = table.Column(type: "text", nullable: true), 27 | SecurityStamp = table.Column(type: "text", nullable: true), 28 | ConcurrencyStamp = table.Column(type: "text", nullable: true), 29 | PhoneNumber = table.Column(type: "text", nullable: true), 30 | PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), 31 | TwoFactorEnabled = table.Column(type: "boolean", nullable: false), 32 | LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), 33 | LockoutEnabled = table.Column(type: "boolean", nullable: false), 34 | AccessFailedCount = table.Column(type: "integer", nullable: false) 35 | }, 36 | constraints: table => 37 | { 38 | table.PrimaryKey("PK_AspNetUsers", x => x.Id); 39 | }); 40 | 41 | migrationBuilder.CreateTable( 42 | name: "Pages", 43 | columns: table => new 44 | { 45 | Id = table.Column(type: "integer", nullable: false) 46 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 47 | Title = table.Column(type: "text", nullable: true), 48 | Body = table.Column(type: "text", nullable: true), 49 | Author = table.Column(type: "text", nullable: true) 50 | }, 51 | constraints: table => 52 | { 53 | table.PrimaryKey("PK_Pages", x => x.Id); 54 | }); 55 | 56 | migrationBuilder.CreateTable( 57 | name: "AspNetUserClaims", 58 | columns: table => new 59 | { 60 | Id = table.Column(type: "integer", nullable: false) 61 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), 62 | UserId = table.Column(type: "text", nullable: false), 63 | ClaimType = table.Column(type: "text", nullable: true), 64 | ClaimValue = table.Column(type: "text", nullable: true) 65 | }, 66 | constraints: table => 67 | { 68 | table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); 69 | table.ForeignKey( 70 | name: "FK_AspNetUserClaims_AspNetUsers_UserId", 71 | column: x => x.UserId, 72 | principalTable: "AspNetUsers", 73 | principalColumn: "Id", 74 | onDelete: ReferentialAction.Cascade); 75 | }); 76 | 77 | migrationBuilder.CreateTable( 78 | name: "AspNetUserLogins", 79 | columns: table => new 80 | { 81 | LoginProvider = table.Column(type: "text", nullable: false), 82 | ProviderKey = table.Column(type: "text", nullable: false), 83 | ProviderDisplayName = table.Column(type: "text", nullable: true), 84 | UserId = table.Column(type: "text", nullable: false) 85 | }, 86 | constraints: table => 87 | { 88 | table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); 89 | table.ForeignKey( 90 | name: "FK_AspNetUserLogins_AspNetUsers_UserId", 91 | column: x => x.UserId, 92 | principalTable: "AspNetUsers", 93 | principalColumn: "Id", 94 | onDelete: ReferentialAction.Cascade); 95 | }); 96 | 97 | migrationBuilder.CreateTable( 98 | name: "AspNetUserTokens", 99 | columns: table => new 100 | { 101 | UserId = table.Column(type: "text", nullable: false), 102 | LoginProvider = table.Column(type: "text", nullable: false), 103 | Name = table.Column(type: "text", nullable: false), 104 | Value = table.Column(type: "text", nullable: true) 105 | }, 106 | constraints: table => 107 | { 108 | table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); 109 | table.ForeignKey( 110 | name: "FK_AspNetUserTokens_AspNetUsers_UserId", 111 | column: x => x.UserId, 112 | principalTable: "AspNetUsers", 113 | principalColumn: "Id", 114 | onDelete: ReferentialAction.Cascade); 115 | }); 116 | 117 | migrationBuilder.CreateIndex( 118 | name: "IX_AspNetUserClaims_UserId", 119 | table: "AspNetUserClaims", 120 | column: "UserId"); 121 | 122 | migrationBuilder.CreateIndex( 123 | name: "IX_AspNetUserLogins_UserId", 124 | table: "AspNetUserLogins", 125 | column: "UserId"); 126 | 127 | migrationBuilder.CreateIndex( 128 | name: "EmailIndex", 129 | table: "AspNetUsers", 130 | column: "NormalizedEmail"); 131 | 132 | migrationBuilder.CreateIndex( 133 | name: "UserNameIndex", 134 | table: "AspNetUsers", 135 | column: "NormalizedUserName", 136 | unique: true); 137 | } 138 | 139 | /// 140 | protected override void Down(MigrationBuilder migrationBuilder) 141 | { 142 | migrationBuilder.DropTable( 143 | name: "AspNetUserClaims"); 144 | 145 | migrationBuilder.DropTable( 146 | name: "AspNetUserLogins"); 147 | 148 | migrationBuilder.DropTable( 149 | name: "AspNetUserTokens"); 150 | 151 | migrationBuilder.DropTable( 152 | name: "Pages"); 153 | 154 | migrationBuilder.DropTable( 155 | name: "AspNetUsers"); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Data/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using JwtRoleAuthentication.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | #nullable disable 10 | 11 | namespace JwtRoleAuthentication.Data.Migrations 12 | { 13 | [DbContext(typeof(ApplicationDbContext))] 14 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "8.0.0") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 22 | 23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); 24 | 25 | modelBuilder.Entity("JwtRoleAuthentication.Models.ApplicationUser", b => 26 | { 27 | b.Property("Id") 28 | .HasColumnType("text"); 29 | 30 | b.Property("AccessFailedCount") 31 | .HasColumnType("integer"); 32 | 33 | b.Property("ConcurrencyStamp") 34 | .IsConcurrencyToken() 35 | .HasColumnType("text"); 36 | 37 | b.Property("Email") 38 | .HasMaxLength(256) 39 | .HasColumnType("character varying(256)"); 40 | 41 | b.Property("EmailConfirmed") 42 | .HasColumnType("boolean"); 43 | 44 | b.Property("LockoutEnabled") 45 | .HasColumnType("boolean"); 46 | 47 | b.Property("LockoutEnd") 48 | .HasColumnType("timestamp with time zone"); 49 | 50 | b.Property("NormalizedEmail") 51 | .HasMaxLength(256) 52 | .HasColumnType("character varying(256)"); 53 | 54 | b.Property("NormalizedUserName") 55 | .HasMaxLength(256) 56 | .HasColumnType("character varying(256)"); 57 | 58 | b.Property("PasswordHash") 59 | .HasColumnType("text"); 60 | 61 | b.Property("PhoneNumber") 62 | .HasColumnType("text"); 63 | 64 | b.Property("PhoneNumberConfirmed") 65 | .HasColumnType("boolean"); 66 | 67 | b.Property("Role") 68 | .HasColumnType("integer"); 69 | 70 | b.Property("SecurityStamp") 71 | .HasColumnType("text"); 72 | 73 | b.Property("TwoFactorEnabled") 74 | .HasColumnType("boolean"); 75 | 76 | b.Property("UserName") 77 | .HasMaxLength(256) 78 | .HasColumnType("character varying(256)"); 79 | 80 | b.HasKey("Id"); 81 | 82 | b.HasIndex("NormalizedEmail") 83 | .HasDatabaseName("EmailIndex"); 84 | 85 | b.HasIndex("NormalizedUserName") 86 | .IsUnique() 87 | .HasDatabaseName("UserNameIndex"); 88 | 89 | b.ToTable("AspNetUsers", (string)null); 90 | }); 91 | 92 | modelBuilder.Entity("JwtRoleAuthentication.Models.Page", b => 93 | { 94 | b.Property("Id") 95 | .ValueGeneratedOnAdd() 96 | .HasColumnType("integer"); 97 | 98 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 99 | 100 | b.Property("Author") 101 | .HasColumnType("text"); 102 | 103 | b.Property("Body") 104 | .HasColumnType("text"); 105 | 106 | b.Property("Title") 107 | .HasColumnType("text"); 108 | 109 | b.HasKey("Id"); 110 | 111 | b.ToTable("Pages"); 112 | }); 113 | 114 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 115 | { 116 | b.Property("Id") 117 | .ValueGeneratedOnAdd() 118 | .HasColumnType("integer"); 119 | 120 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); 121 | 122 | b.Property("ClaimType") 123 | .HasColumnType("text"); 124 | 125 | b.Property("ClaimValue") 126 | .HasColumnType("text"); 127 | 128 | b.Property("UserId") 129 | .IsRequired() 130 | .HasColumnType("text"); 131 | 132 | b.HasKey("Id"); 133 | 134 | b.HasIndex("UserId"); 135 | 136 | b.ToTable("AspNetUserClaims", (string)null); 137 | }); 138 | 139 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 140 | { 141 | b.Property("LoginProvider") 142 | .HasColumnType("text"); 143 | 144 | b.Property("ProviderKey") 145 | .HasColumnType("text"); 146 | 147 | b.Property("ProviderDisplayName") 148 | .HasColumnType("text"); 149 | 150 | b.Property("UserId") 151 | .IsRequired() 152 | .HasColumnType("text"); 153 | 154 | b.HasKey("LoginProvider", "ProviderKey"); 155 | 156 | b.HasIndex("UserId"); 157 | 158 | b.ToTable("AspNetUserLogins", (string)null); 159 | }); 160 | 161 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 162 | { 163 | b.Property("UserId") 164 | .HasColumnType("text"); 165 | 166 | b.Property("LoginProvider") 167 | .HasColumnType("text"); 168 | 169 | b.Property("Name") 170 | .HasColumnType("text"); 171 | 172 | b.Property("Value") 173 | .HasColumnType("text"); 174 | 175 | b.HasKey("UserId", "LoginProvider", "Name"); 176 | 177 | b.ToTable("AspNetUserTokens", (string)null); 178 | }); 179 | 180 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 181 | { 182 | b.HasOne("JwtRoleAuthentication.Models.ApplicationUser", null) 183 | .WithMany() 184 | .HasForeignKey("UserId") 185 | .OnDelete(DeleteBehavior.Cascade) 186 | .IsRequired(); 187 | }); 188 | 189 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 190 | { 191 | b.HasOne("JwtRoleAuthentication.Models.ApplicationUser", null) 192 | .WithMany() 193 | .HasForeignKey("UserId") 194 | .OnDelete(DeleteBehavior.Cascade) 195 | .IsRequired(); 196 | }); 197 | 198 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 199 | { 200 | b.HasOne("JwtRoleAuthentication.Models.ApplicationUser", null) 201 | .WithMany() 202 | .HasForeignKey("UserId") 203 | .OnDelete(DeleteBehavior.Cascade) 204 | .IsRequired(); 205 | }); 206 | #pragma warning restore 612, 618 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Enums/Role.cs: -------------------------------------------------------------------------------- 1 | namespace JwtRoleAuthentication.Enums; 2 | 3 | public enum Role 4 | { 5 | Admin, 6 | User 7 | } -------------------------------------------------------------------------------- /src/JwtRoleAuthentication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/JwtRoleAuthentication.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JwtRoleAuthentication", "JwtRoleAuthentication.csproj", "{D11B1FCE-97C3-413C-817B-A2861F0AEC5A}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {D11B1FCE-97C3-413C-817B-A2861F0AEC5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {D11B1FCE-97C3-413C-817B-A2861F0AEC5A}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {D11B1FCE-97C3-413C-817B-A2861F0AEC5A}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {D11B1FCE-97C3-413C-817B-A2861F0AEC5A}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /src/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using JwtRoleAuthentication.Enums; 2 | using Microsoft.AspNetCore.Identity; 3 | 4 | namespace JwtRoleAuthentication.Models; 5 | 6 | public class ApplicationUser : IdentityUser 7 | { 8 | public Role Role { get; set; } 9 | } -------------------------------------------------------------------------------- /src/Models/AuthRequest.cs: -------------------------------------------------------------------------------- 1 | namespace JwtRoleAuthentication.Models; 2 | 3 | public class AuthRequest 4 | { 5 | public string? Email { get; set; } 6 | public string? Password { get; set; } 7 | } -------------------------------------------------------------------------------- /src/Models/AuthResponse.cs: -------------------------------------------------------------------------------- 1 | namespace JwtRoleAuthentication.Models; 2 | 3 | public class AuthResponse 4 | { 5 | public string? Username { get; set; } 6 | public string? Email { get; set; } 7 | public string? Token { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Models/Page.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace JwtRoleAuthentication.Models; 5 | 6 | public class Page 7 | { 8 | [Key] 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | public int Id { get; set; } 11 | public string? Title { get; set; } 12 | public string? Body { get; set; } 13 | public string? Author { get; set; } 14 | } -------------------------------------------------------------------------------- /src/Models/PageDto.cs: -------------------------------------------------------------------------------- 1 | namespace JwtRoleAuthentication.Models; 2 | 3 | public class PageDto 4 | { 5 | public required int Id { get; set; } 6 | public required string? Title { get; set; } 7 | public required string? Body { get; set; } 8 | public required string? Author { get; set; } 9 | } -------------------------------------------------------------------------------- /src/Models/PagesDto.cs: -------------------------------------------------------------------------------- 1 | namespace JwtRoleAuthentication.Models; 2 | 3 | public class PagesDto 4 | { 5 | public List Pages { get; set; } = new List(); 6 | } -------------------------------------------------------------------------------- /src/Models/RegistrationRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using JwtRoleAuthentication.Enums; 3 | 4 | namespace JwtRoleAuthentication.Models; 5 | 6 | public class RegistrationRequest 7 | { 8 | [Required] 9 | public string? Email { get; set; } 10 | 11 | [Required] 12 | public string? Username { get; set; } 13 | 14 | [Required] 15 | public string? Password { get; set; } 16 | 17 | public Role Role { get; set; } = Role.User; 18 | } -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.Json.Serialization; 3 | using JwtRoleAuthentication.Data; 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; 5 | using Microsoft.AspNetCore.Identity; 6 | using JwtRoleAuthentication.Models; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.IdentityModel.Tokens; 9 | using Microsoft.OpenApi.Models; 10 | using JwtRoleAuthentication.Services; 11 | 12 | 13 | var builder = WebApplication.CreateBuilder(args); 14 | 15 | // Add services 16 | builder.Services.AddControllers(); 17 | builder.Services.AddEndpointsApiExplorer(); 18 | 19 | builder.Services.AddSwaggerGen(option => 20 | { 21 | option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); 22 | option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme 23 | { 24 | In = ParameterLocation.Header, 25 | Description = "Please enter a valid token", 26 | Name = "Authorization", 27 | Type = SecuritySchemeType.Http, 28 | BearerFormat = "JWT", 29 | Scheme = "Bearer" 30 | }); 31 | option.AddSecurityRequirement(new OpenApiSecurityRequirement 32 | { 33 | { 34 | new OpenApiSecurityScheme 35 | { 36 | Reference = new OpenApiReference 37 | { 38 | Type = ReferenceType.SecurityScheme, 39 | Id = "Bearer" 40 | } 41 | }, 42 | new string[] { } 43 | } 44 | }); 45 | }); 46 | 47 | builder.Services.AddProblemDetails(); 48 | builder.Services.AddApiVersioning(); 49 | builder.Services.AddRouting(options => options.LowercaseUrls = true); 50 | 51 | // Add DB Contexts 52 | // Move the connection string to user secrets for release 53 | builder.Services.AddDbContext(opt => 54 | opt.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=devpass")); 55 | 56 | // Register our TokenService dependency 57 | builder.Services.AddScoped(); 58 | 59 | // Support string to enum conversions 60 | builder.Services.AddControllers().AddJsonOptions(opt => 61 | { 62 | opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); 63 | }); 64 | 65 | 66 | // Specify identity requirements 67 | // Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints 68 | builder.Services 69 | .AddIdentity(options => 70 | { 71 | options.SignIn.RequireConfirmedAccount = false; 72 | options.User.RequireUniqueEmail = true; 73 | options.Password.RequireDigit = false; 74 | options.Password.RequiredLength = 6; 75 | options.Password.RequireNonAlphanumeric = false; 76 | options.Password.RequireUppercase = false; 77 | }) 78 | .AddRoles() 79 | .AddEntityFrameworkStores(); 80 | 81 | 82 | // These will eventually be moved to a secrets file, but for alpha development appsettings is fine 83 | var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); 84 | var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); 85 | var symmetricSecurityKey = builder.Configuration.GetValue("JwtTokenSettings:SymmetricSecurityKey"); 86 | 87 | builder.Services.AddAuthentication(options => 88 | { 89 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 90 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 91 | options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; 92 | }) 93 | .AddJwtBearer(options => 94 | { 95 | options.IncludeErrorDetails = true; 96 | options.TokenValidationParameters = new TokenValidationParameters() 97 | { 98 | ClockSkew = TimeSpan.Zero, 99 | ValidateIssuer = true, 100 | ValidateAudience = true, 101 | ValidateLifetime = true, 102 | ValidateIssuerSigningKey = true, 103 | ValidIssuer = validIssuer, 104 | ValidAudience = validAudience, 105 | IssuerSigningKey = new SymmetricSecurityKey( 106 | Encoding.UTF8.GetBytes(symmetricSecurityKey) 107 | ), 108 | }; 109 | }); 110 | 111 | // Build the app 112 | var app = builder.Build(); 113 | 114 | 115 | // Configure the HTTP request pipeline 116 | if (app.Environment.IsDevelopment()) 117 | { 118 | app.UseSwagger(); 119 | app.UseSwaggerUI(); 120 | } 121 | 122 | app.UseHttpsRedirection(); 123 | app.UseStatusCodePages(); 124 | 125 | app.UseAuthentication(); 126 | app.UseAuthorization(); 127 | app.MapControllers(); 128 | app.Run(); -------------------------------------------------------------------------------- /src/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:10419", 8 | "sslPort": 44390 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5064", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7084;http://localhost:5064", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Services/TokenService.cs: -------------------------------------------------------------------------------- 1 | using JwtRoleAuthentication.Models; 2 | using Microsoft.AspNetCore.Http.HttpResults; 3 | 4 | namespace JwtRoleAuthentication.Services; 5 | 6 | using System.IdentityModel.Tokens.Jwt; 7 | using Microsoft.IdentityModel.Tokens; 8 | using System.Security.Claims; 9 | using System.Text; 10 | 11 | public class TokenService 12 | { 13 | private const int ExpirationMinutes = 60; 14 | private readonly ILogger _logger; 15 | 16 | public TokenService(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | public string CreateToken(ApplicationUser user) 22 | { 23 | var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); 24 | var token = CreateJwtToken( 25 | CreateClaims(user), 26 | CreateSigningCredentials(), 27 | expiration 28 | ); 29 | var tokenHandler = new JwtSecurityTokenHandler(); 30 | 31 | _logger.LogInformation("JWT Token created"); 32 | 33 | return tokenHandler.WriteToken(token); 34 | } 35 | 36 | private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, 37 | DateTime expiration) => 38 | new( 39 | new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], 40 | new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], 41 | claims, 42 | expires: expiration, 43 | signingCredentials: credentials 44 | ); 45 | 46 | private List CreateClaims(ApplicationUser user) 47 | { 48 | var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; 49 | 50 | try 51 | { 52 | var claims = new List 53 | { 54 | new Claim(JwtRegisteredClaimNames.Sub, jwtSub), 55 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 56 | new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), 57 | new Claim(ClaimTypes.NameIdentifier, user.Id), 58 | new Claim(ClaimTypes.Name, user.UserName), 59 | new Claim(ClaimTypes.Email, user.Email), 60 | new Claim(ClaimTypes.Role, user.Role.ToString()) 61 | }; 62 | 63 | return claims; 64 | } 65 | catch (Exception e) 66 | { 67 | Console.WriteLine(e); 68 | throw; 69 | } 70 | } 71 | 72 | private SigningCredentials CreateSigningCredentials() 73 | { 74 | var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; 75 | 76 | return new SigningCredentials( 77 | new SymmetricSecurityKey( 78 | Encoding.UTF8.GetBytes(symmetricSecurityKey) 79 | ), 80 | SecurityAlgorithms.HmacSha256 81 | ); 82 | } 83 | } -------------------------------------------------------------------------------- /src/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "SiteSettings": { 3 | "AdminEmail": "example@test.com", 4 | "AdminPassword": "administrator" 5 | }, 6 | 7 | "JwtTokenSettings": { 8 | "ValidIssuer": "ExampleIssuer", 9 | "ValidAudience": "ExampleAudience", 10 | "SymmetricSecurityKey": "v89h3bh89vh9ve8hc89nv98nn899cnccn998ev80vi809jberh89b", 11 | "JwtRegisteredClaimNamesSub": "rbveer3h535nn3n35nyny5umbbt" 12 | }, 13 | 14 | "Logging": { 15 | "LogLevel": { 16 | "Default": "Information", 17 | "Microsoft.AspNetCore": "Warning" 18 | } 19 | }, 20 | "AllowedHosts": "*" 21 | } 22 | --------------------------------------------------------------------------------