Commit 2ea3eb22 authored by Almouhannad's avatar Almouhannad

(B) Added login service, JWT config, auth config

parent fb8a91f1
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Persistence.Identity.Authentication.JWT;
using System.Text;
namespace API.Options.JWT;
public class JWTBearerOptionsSetup : IPostConfigureOptions<JwtBearerOptions>
{
#region JWT Options CTOR DI
private readonly JWTOptions _jwtOptions;
public JWTBearerOptionsSetup(IOptions<JWTOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
}
#endregion
public void PostConfigure(string? name, JwtBearerOptions options)
{
options.TokenValidationParameters.ValidIssuer = _jwtOptions.Issuer;
options.TokenValidationParameters.ValidAudience = _jwtOptions.Audience;
options.TokenValidationParameters.IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecretKey));
}
}
using Microsoft.Extensions.Options;
using Persistence.Identity.Authentication.JWT;
namespace API.Options.JWT;
public class JWTOptionsSetup : IConfigureOptions<JWTOptions>
{
private const string _cofigurationSectionName = "JWTOptions"; // From appsettings.json
#region Using ctor DI to access configuration
private readonly IConfiguration _configuration;
public JWTOptionsSetup(IConfiguration configuration)
{
_configuration = configuration;
}
#endregion
public void Configure(JWTOptions options)
{
_configuration.GetSection(_cofigurationSectionName).Bind(options);
}
}
using API.Options.Database; using API.Options.Database;
using API.Options.JWT;
using API.SeedDatabaseHelper; using API.SeedDatabaseHelper;
using Application.Behaviors; using Application.Behaviors;
using FluentValidation; using FluentValidation;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Persistence.Context; using Persistence.Context;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
...@@ -65,9 +68,47 @@ builder.Services.AddControllers() ...@@ -65,9 +68,47 @@ builder.Services.AddControllers()
.AddApplicationPart(Presentation.AssemblyReference.Assembly); .AddApplicationPart(Presentation.AssemblyReference.Assembly);
#endregion #endregion
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
#region Swagger with JWT authorization
builder.Services.AddSwaggerGen(opt =>
{
opt.SwaggerDoc("v1", new OpenApiInfo { Title = "MyAPI", Version = "v1" });
opt.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "bearer"
});
opt.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type=ReferenceType.SecurityScheme,
Id="Bearer"
}
},
new string[]{}
}
});
});
#endregion
#region Authentication options
builder.Services.ConfigureOptions<JWTOptionsSetup>();
builder.Services.ConfigureOptions<JWTBearerOptionsSetup>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
#endregion
var app = builder.Build(); var app = builder.Build();
...@@ -85,6 +126,8 @@ if (app.Environment.IsDevelopment()) ...@@ -85,6 +126,8 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
......
...@@ -15,5 +15,10 @@ ...@@ -15,5 +15,10 @@
"MaxRetryCount": 3, "MaxRetryCount": 3,
"CommandTimeout": 30, "CommandTimeout": 30,
"EnableDetailedErrors": true "EnableDetailedErrors": true
},
"JWTOptions": {
"SecretKey": "MoHafez11@SecretKeyAPIVeryLongSecretKeyMoHafez11@SecretKeyAPIVeryLongSecretKey",
"Audience": "clinics-front-end.hiast.edy.sy",
"Issuer": "clinics-back-end.hiast.edy.sy"
} }
} }
using Domain.Entities.Identity.Users;
namespace Application.Abstractions.JWT;
public interface IJWTProvider
{
string Generate(User user);
}
using Application.Abstractions.CQRS.Commands;
namespace Application.Users.Commands.Login;
public class LoginCommand : ICommand<string>
{
public string UserName { get; set; } = null!;
public string Password { get; set; } = null!;
}
using Application.Abstractions.CQRS.Commands;
using Application.Abstractions.JWT;
using Domain.Entities.Identity.Users;
using Domain.Errors;
using Domain.Repositories;
using Domain.Shared;
using Domain.UnitOfWork;
namespace Application.Users.Commands.Login;
public class LoginCommandHandler : CommandHandlerBase<LoginCommand, string>
{
#region CTOR DI
private readonly IUserRepository _userRepository;
private readonly IJWTProvider _jwtProvider;
public LoginCommandHandler(IUnitOfWork unitOfWork, IUserRepository userRepository, IJWTProvider jwtProvider) : base(unitOfWork)
{
_userRepository = userRepository;
_jwtProvider = jwtProvider;
}
#endregion
public override async Task<Result<string>> HandleHelper(LoginCommand request, CancellationToken cancellationToken)
{
#region 1. Check username and password are correct
Result<User?> loginResult = await _userRepository.VerifyPasswordAsync(request.UserName, request.Password);
if (loginResult.IsFailure)
return Result.Failure<string>(loginResult.Error); // Not found username
if (loginResult.Value is null) // Invalid password
return Result.Failure<string>(IdentityErrors.PasswordMismatch);
#endregion
#region 2. Generate JWT
User user = loginResult.Value!;
string token = _jwtProvider.Generate(user);
#endregion
return Result.Success<string>(token);
}
}
...@@ -6,4 +6,5 @@ public static class IdentityErrors ...@@ -6,4 +6,5 @@ public static class IdentityErrors
{ {
public static Error InvalidRole => new("Identity.InvalidRole", "Role specified for user is invalid"); public static Error InvalidRole => new("Identity.InvalidRole", "Role specified for user is invalid");
public static Error NotFound => new("Identity.NotFound", "المستخدم غير مسجّل في النظام"); public static Error NotFound => new("Identity.NotFound", "المستخدم غير مسجّل في النظام");
public static Error PasswordMismatch => new("Identity.PasswordMismatch", "كلمة السر غير صحيحة");
} }
...@@ -6,5 +6,8 @@ namespace Domain.Repositories; ...@@ -6,5 +6,8 @@ namespace Domain.Repositories;
public interface IUserRepository : IRepository<User> public interface IUserRepository : IRepository<User>
{ {
public Task<Result<User>> GetByUserNameFullAsync(string userName); public Task<Result<User>> GetByUserNameFullAsync(string userName);
public Task<Result<User?>> VerifyPasswordAsync(string userName, string password);
} }
namespace Persistence.Identity.Authentication.JWT;
// Using option pattern
public class JWTOptions
{
public string Issuer { get; init; } = null!;
public string Audience { get; init; } = null!;
public string SecretKey { get; init; } = null!;
}
using Application.Abstractions.JWT;
using Domain.Entities.Identity.Users;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Persistence.Identity.Authentication.JWT;
public sealed class JWTProvider : IJWTProvider
{
private readonly JWTOptions _options;
public JWTProvider(IOptions<JWTOptions> options)
{
_options = options.Value;
}
public string Generate(User user)
{
var claims = new Claim[]
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(JwtRegisteredClaimNames.UniqueName, user.UserName),
};
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_options.SecretKey)),
SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
_options.Issuer,
_options.Audience,
claims,
null,
DateTime.UtcNow.AddDays(30),
signingCredentials);
var tokenValue = new JwtSecurityTokenHandler()
.WriteToken(token);
return tokenValue;
}
}
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
......
...@@ -18,7 +18,8 @@ public static class SpecificationEvaluator ...@@ -18,7 +18,8 @@ public static class SpecificationEvaluator
queryable = queryable.Where(specification.Criteria); queryable = queryable.Where(specification.Criteria);
} }
specification.IncludeExpressions.Aggregate(
queryable = specification.IncludeExpressions.Aggregate(
queryable, queryable,
(current, includeExpression) => (current, includeExpression) =>
current.Include(includeExpression) current.Include(includeExpression)
......
...@@ -4,6 +4,7 @@ using Domain.Repositories; ...@@ -4,6 +4,7 @@ using Domain.Repositories;
using Domain.Shared; using Domain.Shared;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Persistence.Context; using Persistence.Context;
using Persistence.Identity.PasswordsHashing;
using Persistence.Repositories.Base; using Persistence.Repositories.Base;
namespace Persistence.Repositories.Users; namespace Persistence.Repositories.Users;
...@@ -11,8 +12,10 @@ namespace Persistence.Repositories.Users; ...@@ -11,8 +12,10 @@ namespace Persistence.Repositories.Users;
public class UserRepository : Repositroy<User>, IUserRepository public class UserRepository : Repositroy<User>, IUserRepository
{ {
#region Ctor DI #region Ctor DI
public UserRepository(ClinicsDbContext context) : base(context) private readonly IPasswordHasher _passwordHasher;
public UserRepository(ClinicsDbContext context, IPasswordHasher passwordHasher) : base(context)
{ {
_passwordHasher = passwordHasher;
} }
#endregion #endregion
...@@ -39,4 +42,18 @@ public class UserRepository : Repositroy<User>, IUserRepository ...@@ -39,4 +42,18 @@ public class UserRepository : Repositroy<User>, IUserRepository
} }
#endregion #endregion
#region Verify password
public async Task<Result<User?>> VerifyPasswordAsync(string userName, string password)
{
var userResult = await GetByUserNameFullAsync(userName);
if (userResult.IsFailure)
return Result.Failure<User?>(userResult.Error);
if (!_passwordHasher.Verify(password, userResult.Value.HashedPassword))
return Result.Success<User?>(null);
return Result.Success<User?>(userResult.Value);
}
#endregion
} }
using Application.Employees.Commands.AttachFamilyMemberToEmployee; using Application.Employees.Commands.AttachFamilyMemberToEmployee;
using Application.Employees.Commands.CreateEmployee; using Application.Employees.Commands.CreateEmployee;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Presentation.Controllers.Base; using Presentation.Controllers.Base;
...@@ -16,7 +17,7 @@ public class EmployeesController : ApiController ...@@ -16,7 +17,7 @@ public class EmployeesController : ApiController
} }
#endregion #endregion
[Authorize]
[HttpPost] [HttpPost]
public async Task<IActionResult> Create([FromBody] CreateEmployeeCommand command) public async Task<IActionResult> Create([FromBody] CreateEmployeeCommand command)
{ {
......
using Application.Users.Commands.Login;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Presentation.Controllers.Base;
namespace Presentation.Controllers;
[Route("api/Users")]
public class UsersController : ApiController
{
#region CTOR DI
public UsersController(ISender sender) : base(sender)
{
}
#endregion
[HttpPost]
public async Task<IActionResult> LoginUser([FromBody] LoginCommand command)
{
var result = await _sender.Send(command);
if (result.IsFailure)
return HandleFailure(result);
return Ok(result.Value);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment