├── .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 |
--------------------------------------------------------------------------------