Skip to content

Commit da7beb8

Browse files
Authenticate Api added. JWT Authentication added. Roles and functions copied to the claims. JWTOptions are read from appsettings. Hash password is stored in DB Seed.
1 parent c50a98b commit da7beb8

File tree

10 files changed

+195
-29
lines changed

10 files changed

+195
-29
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"ConnectionStrings": {
3-
"DefaultConnection": "Server=db,1433;Database=CentralizedLoggingDB;User=sa;Password=Bisp@123;MultipleActiveResultSets=true;TrustServerCertificate=True;Encrypt=False;"
3+
"DefaultConnection": "Server=db,1433;Database=UserManagementDB;User=sa;Password=Bisp@123;MultipleActiveResultSets=true;TrustServerCertificate=True;Encrypt=False;"
44
}
55
}
Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Identity.Data;
3+
using Microsoft.AspNetCore.Mvc;
24
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.Extensions.Options;
6+
using Microsoft.IdentityModel.Tokens;
7+
using System.IdentityModel.Tokens.Jwt;
8+
using System.Security.Claims;
9+
using System.Text;
310
using UserManagementApi.Data;
411
using UserManagementApi.DTO;
12+
using UserManagementApi.DTO.Auth;
13+
using UserManagementApi.Models;
514

615
namespace UserManagementApi.Controllers
716
{
@@ -11,17 +20,68 @@ namespace UserManagementApi.Controllers
1120
public class UsersController : ControllerBase
1221
{
1322
private readonly AppDbContext _db;
14-
public UsersController(AppDbContext db) => _db = db;
23+
private readonly JwtOptions _jwt;
1524

16-
// GET: api/users/{userId}/permissions
25+
public UsersController(AppDbContext db, IOptions<JwtOptions> jwtOptions)
26+
{
27+
_db = db;
28+
_jwt = jwtOptions.Value;
29+
}
30+
// --------- NEW: POST /api/users/authenticate ----------
31+
[HttpPost("authenticate")]
32+
public async Task<ActionResult<AuthResponse>> Authenticate([FromBody] DTO.LoginRequest req)
33+
{
34+
if (string.IsNullOrWhiteSpace(req.UserName) || string.IsNullOrWhiteSpace(req.Password))
35+
return BadRequest("Username and password are required.");
36+
37+
// Validate user (plain text as per your seed)
38+
var user = await _db.Users
39+
.Include(u => u.UserRoles)
40+
.ThenInclude(ur => ur.Role)
41+
.FirstOrDefaultAsync(u => u.UserName == req.UserName && u.Password == req.Password);
42+
43+
if (user == null)
44+
return Unauthorized("Invalid credentials.");
45+
46+
// Collect roles & function codes for claims (optional but handy)
47+
var roleIds = user.UserRoles.Select(ur => ur.RoleId).ToList();
48+
49+
var functionCodes = await _db.RoleFunctions
50+
.Where(rf => roleIds.Contains(rf.RoleId))
51+
.Select(rf => rf.Function.Code)
52+
.Distinct()
53+
.ToListAsync();
54+
55+
var token = GenerateJwt(user, user.UserRoles.Select(ur => ur.Role.Name).Distinct().ToList(), functionCodes, out var expiresAtUtc);
56+
57+
// Get the same permissions tree you already expose
58+
var permissions = await BuildPermissionsForUser(user.Id);
59+
60+
return Ok(new AuthResponse(user.Id, user.UserName, token, expiresAtUtc, permissions));
61+
}
62+
63+
// --------- (Existing) GET /api/users/{userId}/permissions ----------
64+
// Now protected by JWT; call with Bearer token returned by /authenticate
65+
[Authorize]
1766
[HttpGet("{userId:int}/permissions")]
1867
public async Task<ActionResult<UserPermissionsDto>> GetPermissions(int userId)
1968
{
69+
// Optional: you can enforce that a user can only view their own permissions
70+
// by comparing userId with the token's sub, if desired.
71+
2072
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
2173
if (user == null) return NotFound($"User {userId} not found.");
2274

23-
// Build a single LINQ query that filters functions to those reachable
24-
// via the user's roles (no client-side filtering).
75+
var dto = await BuildPermissionsForUser(userId);
76+
return Ok(dto);
77+
}
78+
79+
// ----- helpers -----
80+
81+
private async Task<UserPermissionsDto> BuildPermissionsForUser(int userId)
82+
{
83+
var user = await _db.Users.FirstAsync(u => u.Id == userId);
84+
2585
var categories = await _db.Categories
2686
.Select(c => new CategoryDto(
2787
c.Id,
@@ -30,19 +90,50 @@ public async Task<ActionResult<UserPermissionsDto>> GetPermissions(int userId)
3090
.Select(m => new ModuleDto(
3191
m.Id, m.Name, m.Area, m.Controller, m.Action,
3292
m.Functions
33-
.Where(f => f.RoleFunctions
34-
.Any(rf => rf.Role.UserRoles.Any(ur => ur.UserId == userId)))
35-
.Select(f => new FunctionDto(f.Id, f.Code, f.DisplayName))
36-
.ToList()
93+
.Where(f => f.RoleFunctions
94+
.Any(rf => rf.Role.UserRoles.Any(ur => ur.UserId == userId)))
95+
.Select(f => new FunctionDto(f.Id, f.Code, f.DisplayName))
96+
.ToList()
3797
))
38-
.Where(md => md.Functions.Any()) // keep only modules with at least one permitted function
98+
.Where(md => md.Functions.Any())
3999
.ToList()
40100
))
41-
.Where(cd => cd.Modules.Any()) // keep only categories with at least one permitted module
101+
.Where(cd => cd.Modules.Any())
42102
.ToListAsync();
43103

44-
var dto = new UserPermissionsDto(user.Id, user.UserName, categories);
45-
return Ok(dto);
104+
return new UserPermissionsDto(user.Id, user.UserName, categories);
105+
}
106+
107+
private string GenerateJwt(AppUser user, IEnumerable<string> roles, IEnumerable<string> functionCodes, out DateTime expiresAtUtc)
108+
{
109+
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key));
110+
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
111+
112+
var claims = new List<Claim>
113+
{
114+
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
115+
new(JwtRegisteredClaimNames.UniqueName, user.UserName)
116+
};
117+
118+
// optional: add role claims
119+
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
120+
121+
// optional: add function claims (careful: keep token size reasonable)
122+
foreach (var fn in functionCodes)
123+
claims.Add(new Claim("perm", fn));
124+
125+
var now = DateTime.UtcNow;
126+
expiresAtUtc = now.AddMinutes(_jwt.ExpiresMinutes);
127+
128+
var token = new JwtSecurityToken(
129+
issuer: _jwt.Issuer,
130+
audience: _jwt.Audience,
131+
claims: claims,
132+
notBefore: now,
133+
expires: expiresAtUtc,
134+
signingCredentials: creds);
135+
136+
return new JwtSecurityTokenHandler().WriteToken(token);
46137
}
47138
}
48139
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace UserManagementApi.DTO.Auth
2+
{
3+
public class JwtOptions
4+
{
5+
public string Issuer { get; set; } = null!;
6+
public string Audience { get; set; } = null!;
7+
public string Key { get; set; } = null!;
8+
public int ExpiresMinutes { get; set; }
9+
}
10+
}

UserManagementApi/DTO/PermissionDtos.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
namespace UserManagementApi.DTO
22
{
3+
public record LoginRequest(string UserName, string Password);
4+
5+
public record AuthResponse(
6+
int UserId,
7+
string UserName,
8+
string Token,
9+
DateTime ExpiresAtUtc,
10+
UserPermissionsDto Permissions // reuse your existing permissions dto
11+
);
12+
313
public record FunctionDto(int Id, string Code, string DisplayName);
414
public record ModuleDto(int Id, string Name, string Area, string Controller, string Action, List<FunctionDto> Functions);
515
public record CategoryDto(int Id, string Name, List<ModuleDto> Modules);

UserManagementApi/DbSeeder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ public static void Seed(IServiceProvider serviceProvider)
4747

4848
var users = new List<AppUser>
4949
{
50-
new AppUser { Id = 1, UserName = "alice", Password = "alice" },
51-
new AppUser { Id = 2, UserName = "bob", Password = "bob" }
50+
new AppUser { Id = 1, UserName = "alice", Password = BCrypt.Net.BCrypt.HashPassword("alice") },
51+
new AppUser { Id = 2, UserName = "bob", Password = BCrypt.Net.BCrypt.HashPassword("boob") }
5252
};
5353

5454
var userRoles = new List<UserRole>

UserManagementApi/Program.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
using UserManagementApi;
2-
using UserManagementApi.Data;
3-
using UserManagementApi.Middlewares;
1+
using Microsoft.AspNetCore.Authentication.JwtBearer;
42
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.IdentityModel.Tokens;
54
using Microsoft.OpenApi.Models;
65
using Serilog;
76
using System.Reflection;
7+
using System.Text;
8+
using UserManagementApi;
9+
using UserManagementApi.Data;
10+
using UserManagementApi.DTO.Auth;
11+
using UserManagementApi.Middlewares;
812

913
var builder = WebApplication.CreateBuilder(args);
1014

@@ -22,6 +26,26 @@
2226
builder.Services.AddDbContext<AppDbContext>(options =>
2327
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
2428

29+
// Add JwtOptions + Authentication
30+
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("Jwt"));
31+
var jwt = builder.Configuration.GetSection("Jwt").Get<JwtOptions>()!;
32+
33+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
34+
.AddJwtBearer(opt =>
35+
{
36+
opt.TokenValidationParameters = new TokenValidationParameters
37+
{
38+
ValidateIssuer = true,
39+
ValidateAudience = true,
40+
ValidateIssuerSigningKey = true,
41+
ValidIssuer = jwt.Issuer,
42+
ValidAudience = jwt.Audience,
43+
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt.Key)),
44+
ClockSkew = TimeSpan.Zero
45+
};
46+
});
47+
48+
builder.Services.AddAuthorization();
2549

2650
builder.Services.AddHttpContextAccessor();
2751

@@ -68,6 +92,8 @@
6892

6993
app.UseHttpsRedirection();
7094

95+
96+
app.UseAuthentication(); // must be before UseAuthorization
7197
app.UseAuthorization();
7298

7399
app.MapControllers();

UserManagementApi/UserManagementApi.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13+
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
14+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.8" />
1315
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
1416
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
1517
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
2-
"Logging": {
3-
"LogLevel": {
4-
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
6-
}
2+
"ConnectionStrings": {
3+
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=UserManagementDB;Trusted_Connection=True;MultipleActiveResultSets=true"
74
}
85
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"ConnectionStrings": {
3+
"DefaultConnection": "Server=db,1433;Database=CentralizedLoggingDB;User=sa;Password=Bisp@123;MultipleActiveResultSets=true;TrustServerCertificate=True;Encrypt=False;"
4+
}
5+
}

UserManagementApi/appsettings.json

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
{
2-
"Logging": {
3-
"LogLevel": {
2+
"Serilog": {
3+
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Formatting.Compact", "Serilog.Filters.Expressions" ],
4+
"MinimumLevel": {
45
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
6-
}
6+
"Override": {
7+
"Microsoft": "Warning",
8+
"System": "Warning"
9+
}
10+
},
11+
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId", "WithProcessId" ],
12+
"WriteTo": [
13+
{
14+
"Name": "File",
15+
"Args": {
16+
"path": "logs/dev-app-.clef",
17+
"rollingInterval": "Day",
18+
"rollOnFileSizeLimit": true,
19+
"retainedFileCountLimit": 7,
20+
"shared": true,
21+
"formatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact"
22+
},
23+
"restrictedToMinimumLevel": "Information"
24+
}
25+
]
726
},
8-
"AllowedHosts": "*"
27+
"Jwt": {
28+
"Issuer": "PermsApi",
29+
"Audience": "PermsApiAudience",
30+
"Key": "very_long_dev_key_change_in_prod_1234567890",
31+
"ExpiresMinutes": 60
32+
}
33+
934
}

0 commit comments

Comments
 (0)