0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-08-23 03:38:36 +00:00

Add User auth

This commit is contained in:
Alexander Konietzko 2023-03-17 08:44:37 +01:00
parent 74d546f6c7
commit b36aaff112
No known key found for this signature in database
GPG Key ID: BA6905F37AEC2B5B
18 changed files with 221 additions and 19 deletions

View File

@ -3,9 +3,11 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UserSecretsId>64377c40-44d6-4989-9662-5d778f8b3b92</UserSecretsId>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />

View File

@ -4,6 +4,7 @@ using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Notifications; using CleanArchitecture.Domain.Notifications;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace CleanArchitecture.Api.Controllers; namespace CleanArchitecture.Api.Controllers;
@ -21,13 +22,15 @@ public class UserController : ApiController
_userService = userService; _userService = userService;
} }
[Authorize]
[HttpGet] [HttpGet]
public async Task<IActionResult> GetAllUsersAsync() public async Task<IActionResult> GetAllUsersAsync()
{ {
var users = await _userService.GetAllUsersAsync(); var users = await _userService.GetAllUsersAsync();
return Response(users); return Response(users);
} }
[Authorize]
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<IActionResult> GetUserByIdAsync( public async Task<IActionResult> GetUserByIdAsync(
[FromRoute] Guid id, [FromRoute] Guid id,
@ -43,14 +46,16 @@ public class UserController : ApiController
var userId = await _userService.CreateUserAsync(viewModel); var userId = await _userService.CreateUserAsync(viewModel);
return Response(userId); return Response(userId);
} }
[Authorize]
[HttpDelete("{id}")] [HttpDelete("{id}")]
public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid id) public async Task<IActionResult> DeleteUserAsync([FromRoute] Guid id)
{ {
await _userService.DeleteUserAsync(id); await _userService.DeleteUserAsync(id);
return Response(id); return Response(id);
} }
[Authorize]
[HttpPut] [HttpPut]
public async Task<IActionResult> UpdateUserAsync([FromBody] UpdateUserViewModel viewModel) public async Task<IActionResult> UpdateUserAsync([FromBody] UpdateUserViewModel viewModel)
{ {

View File

@ -1,12 +1,16 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using CleanArchitecture.Application.Extensions; using CleanArchitecture.Application.Extensions;
using CleanArchitecture.Domain.Extensions; using CleanArchitecture.Domain.Extensions;
using CleanArchitecture.gRPC; using CleanArchitecture.gRPC;
using CleanArchitecture.Infrastructure.Database; using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions; using CleanArchitecture.Infrastructure.Extensions;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -24,11 +28,23 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
b => b.MigrationsAssembly("CleanArchitecture.Infrastructure")); b => b.MigrationsAssembly("CleanArchitecture.Infrastructure"));
}); });
builder.Services.AddAuthentication(
options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(
jwtOptions =>
{
jwtOptions.TokenValidationParameters = CreateTokenValidationParameters();
});
builder.Services.AddInfrastructure(); builder.Services.AddInfrastructure();
builder.Services.AddQueryHandlers(); builder.Services.AddQueryHandlers();
builder.Services.AddServices(); builder.Services.AddServices();
builder.Services.AddCommandHandlers(); builder.Services.AddCommandHandlers();
builder.Services.AddNotificationHandlers(); builder.Services.AddNotificationHandlers();
builder.Services.AddApiUser();
builder.Services.AddMediatR(cfg => builder.Services.AddMediatR(cfg =>
{ {
@ -42,6 +58,7 @@ app.UseSwaggerUI();
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
@ -57,5 +74,24 @@ using (IServiceScope scope = app.Services.CreateScope())
app.Run(); app.Run();
TokenValidationParameters CreateTokenValidationParameters()
{
var result = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Auth:Issuer"],
ValidAudience = builder.Configuration["Auth:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["Auth:Secret"]!)),
RequireSignedTokens = false
};
return result;
}
// Needed for integration tests webapplication factory // Needed for integration tests webapplication factory
public partial class Program { } public partial class Program { }

View File

@ -4,5 +4,13 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"Auth": {
"Issuer": "CleanArchitectureServer",
"Audience": "CleanArchitectureClient",
"Secret": "sD3v061gf8BxXgmxcHss"
} }
} }

View File

@ -8,5 +8,10 @@
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" "DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"Auth": {
"Issuer": "CleanArchitectureServer",
"Audience": "CleanArchitectureClient",
"Secret": "sD3v061gf8BxXgmxcHss"
} }
} }

View File

@ -3,4 +3,5 @@ namespace CleanArchitecture.Application.ViewModels.Users;
public sealed record CreateUserViewModel( public sealed record CreateUserViewModel(
string Email, string Email,
string Surname, string Surname,
string GivenName); string GivenName,
string Password);

View File

@ -1,5 +1,6 @@
using System; using System;
using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Application.ViewModels.Users; namespace CleanArchitecture.Application.ViewModels.Users;
@ -9,6 +10,7 @@ public sealed class UserViewModel
public string Email { get; set; } = string.Empty; public string Email { get; set; } = string.Empty;
public string GivenName { get; set; } = string.Empty; public string GivenName { get; set; } = string.Empty;
public string Surname { get; set; } = string.Empty; public string Surname { get; set; } = string.Empty;
public UserRole Role { get; set; }
public static UserViewModel FromUser(User user) public static UserViewModel FromUser(User user)
{ {
@ -17,7 +19,8 @@ public sealed class UserViewModel
Id = user.Id, Id = user.Id,
Email = user.Email, Email = user.Email,
GivenName = user.GivenName, GivenName = user.GivenName,
Surname = user.Surname Surname = user.Surname,
Role = user.Role
}; };
} }
} }

View File

@ -0,0 +1,46 @@
using System;
using System.Linq;
using System.Security.Claims;
using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Interfaces;
using Microsoft.AspNetCore.Http;
namespace CleanArchitecture.Domain;
public sealed class ApiUser : IUser
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ApiUser(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Guid GetUserId()
{
var claim = _httpContextAccessor.HttpContext?.User.Claims
.FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier));
if (Guid.TryParse(claim?.Value, out var userId))
{
return userId;
}
throw new ArgumentException("Could not parse user id to guid");
}
public UserRole GetUserRole()
{
var claim = _httpContextAccessor.HttpContext?.User.Claims
.FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.Role));
if (Enum.TryParse(claim?.Value, out UserRole userRole))
{
return userRole;
}
throw new ArgumentException("Could not parse user role");
}
public string Name => _httpContextAccessor.HttpContext?.User.Identity?.Name ?? string.Empty;
}

View File

@ -6,8 +6,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FluentValidation" Version="11.5.1" /> <PackageReference Include="FluentValidation" Version="11.5.1" />
<PackageReference Include="MediatR" Version="12.0.1" /> <PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -10,17 +10,20 @@ public sealed class CreateUserCommand : CommandBase
public string Email { get; } public string Email { get; }
public string Surname { get; } public string Surname { get; }
public string GivenName { get; } public string GivenName { get; }
public string Password { get; }
public CreateUserCommand( public CreateUserCommand(
Guid userId, Guid userId,
string email, string email,
string surname, string surname,
string givenName) : base(userId) string givenName,
string password) : base(userId)
{ {
UserId = userId; UserId = userId;
Email = email; Email = email;
Surname = surname; Surname = surname;
GivenName = givenName; GivenName = givenName;
Password = password;
} }
public override bool IsValid() public override bool IsValid()

View File

@ -1,12 +1,14 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Interfaces.Repositories;
using CleanArchitecture.Domain.Notifications; using CleanArchitecture.Domain.Notifications;
using MediatR; using MediatR;
using BC = BCrypt.Net.BCrypt;
namespace CleanArchitecture.Domain.Commands.Users.CreateUser; namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
@ -43,11 +45,15 @@ public sealed class CreateUserCommandHandler : CommandHandlerBase,
return; return;
} }
var passwordHash = BC.HashPassword(request.Password);
var user = new User( var user = new User(
request.UserId, request.UserId,
request.Email, request.Email,
request.Surname, request.Surname,
request.GivenName); request.GivenName,
passwordHash,
UserRole.User);
_userRepository.Add(user); _userRepository.Add(user);

View File

@ -1,5 +1,6 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces;
@ -13,14 +14,17 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
IRequestHandler<DeleteUserCommand> IRequestHandler<DeleteUserCommand>
{ {
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IUser _user;
public DeleteUserCommandHandler( public DeleteUserCommandHandler(
IMediatorHandler bus, IMediatorHandler bus,
IUnitOfWork unitOfWork, IUnitOfWork unitOfWork,
INotificationHandler<DomainNotification> notifications, INotificationHandler<DomainNotification> notifications,
IUserRepository userRepository) : base(bus, unitOfWork, notifications) IUserRepository userRepository,
IUser user) : base(bus, unitOfWork, notifications)
{ {
_userRepository = userRepository; _userRepository = userRepository;
_user = user;
} }
public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken)
@ -43,6 +47,11 @@ public sealed class DeleteUserCommandHandler : CommandHandlerBase,
return; return;
} }
if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin)
{
return;
}
_userRepository.Remove(user); _userRepository.Remove(user);
if (await CommitAsync()) if (await CommitAsync())

View File

@ -1,4 +1,5 @@
using System; using System;
using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Domain.Commands.Users.UpdateUser; namespace CleanArchitecture.Domain.Commands.Users.UpdateUser;
@ -10,17 +11,20 @@ public sealed class UpdateUserCommand : CommandBase
public string Email { get; } public string Email { get; }
public string Surname { get; } public string Surname { get; }
public string GivenName { get; } public string GivenName { get; }
public UserRole Role { get; }
public UpdateUserCommand( public UpdateUserCommand(
Guid userId, Guid userId,
string email, string email,
string surname, string surname,
string givenName) : base(userId) string givenName,
UserRole role) : base(userId)
{ {
UserId = userId; UserId = userId;
Email = email; Email = email;
Surname = surname; Surname = surname;
GivenName = givenName; GivenName = givenName;
Role = role;
} }
public override bool IsValid() public override bool IsValid()

View File

@ -1,5 +1,7 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Enums;
using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces;
@ -13,14 +15,17 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
IRequestHandler<UpdateUserCommand> IRequestHandler<UpdateUserCommand>
{ {
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IUser _user;
public UpdateUserCommandHandler( public UpdateUserCommandHandler(
IMediatorHandler bus, IMediatorHandler bus,
IUnitOfWork unitOfWork, IUnitOfWork unitOfWork,
INotificationHandler<DomainNotification> notifications, INotificationHandler<DomainNotification> notifications,
IUserRepository userRepository) : base(bus, unitOfWork, notifications) IUserRepository userRepository,
IUser user) : base(bus, unitOfWork, notifications)
{ {
_userRepository = userRepository; _userRepository = userRepository;
_user = user;
} }
public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken) public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken)
@ -41,11 +46,21 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase,
ErrorCodes.ObjectNotFound)); ErrorCodes.ObjectNotFound));
return; return;
} }
if (_user.GetUserId() != request.UserId || _user.GetUserRole() != UserRole.Admin)
{
return;
}
if (_user.GetUserRole() == UserRole.Admin)
{
user.SetRole(request.Role);
}
user.SetEmail(request.Email); user.SetEmail(request.Email);
user.SetSurname(request.Surname); user.SetSurname(request.Surname);
user.SetGivenName(request.GivenName); user.SetGivenName(request.GivenName);
_userRepository.Update(user); _userRepository.Update(user);
if (await CommitAsync()) if (await CommitAsync())

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Domain.Entities; namespace CleanArchitecture.Domain.Entities;
@ -8,20 +9,26 @@ public class User : Entity
public string Email { get; private set; } public string Email { get; private set; }
public string GivenName { get; private set; } public string GivenName { get; private set; }
public string Surname { get; private set; } public string Surname { get; private set; }
public string Password { get; private set; }
public UserRole Role { get; private set; }
public string FullName => $"{Surname}, {GivenName}"; public string FullName => $"{Surname}, {GivenName}";
public User( public User(
Guid id, Guid id,
string email, string email,
string surname, string surname,
string givenName) : base(id) string givenName,
string password,
UserRole role) : base(id)
{ {
Email = email; Email = email;
GivenName = givenName; GivenName = givenName;
Surname = surname; Surname = surname;
Password = password;
Role = role;
} }
[MemberNotNull(nameof(Email))] [MemberNotNull(nameof(Email))]
public void SetEmail(string email) public void SetEmail(string email)
{ {
@ -72,4 +79,26 @@ public class User : Entity
Surname = surname; Surname = surname;
} }
[MemberNotNull(nameof(Password))]
public void SetPassword(string password)
{
if (password == null)
{
throw new ArgumentNullException(nameof(password));
}
if (password.Length > 100)
{
throw new ArgumentException(
"Password may not be longer than 100 characters");
}
Password = password;
}
public void SetRole(UserRole role)
{
Role = role;
}
} }

View File

@ -0,0 +1,7 @@
namespace CleanArchitecture.Domain.Enums;
public enum UserRole
{
Admin,
User
}

View File

@ -3,6 +3,7 @@ using CleanArchitecture.Domain.Commands.Users.DeleteUser;
using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.Commands.Users.UpdateUser;
using CleanArchitecture.Domain.EventHandler; using CleanArchitecture.Domain.EventHandler;
using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Interfaces;
using MediatR; using MediatR;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -29,4 +30,12 @@ public static class ServiceCollectionExtension
return services; return services;
} }
public static IServiceCollection AddApiUser(this IServiceCollection services)
{
// User
services.AddScoped<IUser, ApiUser>();
return services;
}
} }

View File

@ -0,0 +1,12 @@
using System;
using CleanArchitecture.Domain.Enums;
namespace CleanArchitecture.Domain.Interfaces;
public interface IUser
{
Guid GetUserId();
UserRole GetUserRole();
string Name { get; }
}