Skip to content

Commit

Permalink
Merge pull request #46 from AngeloDotNet/develop
Browse files Browse the repository at this point in the history
Sync Main from Develop
  • Loading branch information
AngeloDotNet authored Dec 3, 2024
2 parents f81c179 + e4563c0 commit 6bf6234
Show file tree
Hide file tree
Showing 23 changed files with 809 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AutenticazioneSvc.DataAccessLayer\AutenticazioneSvc.DataAccessLayer.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Security.Claims;
using System.Security.Principal;

namespace AutenticazioneSvc.BusinessLayer.Extensions;

public static class ClaimsExtensions
{
public static Guid GetId(this IPrincipal user)
{
var value = GetClaimValue(user, ClaimTypes.NameIdentifier);
return Guid.Parse(value);
}

public static string GetClaimValue(this IPrincipal user, string claimType)
{
var value = ((ClaimsPrincipal)user).FindFirst(claimType)?.Value;

return value!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using AutenticazioneSvc.DataAccessLayer.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AutenticazioneSvc.BusinessLayer.HostedService;

public class AuthStartupTask(IServiceProvider serviceProvider) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = serviceProvider.CreateScope();

var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
await GenerateRolesAsync(roleManager);

//TODO: Manca la generazione dell'utente amministratore
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

private static async Task GenerateRolesAsync(RoleManager<ApplicationRole> roleManager)
{
var roleNames = new string[] { RoleNames.Administrator, RoleNames.PowerUser, RoleNames.User };

foreach (var roleName in roleNames)
{
var roleExists = await roleManager.RoleExistsAsync(roleName);

if (!roleExists)
{
await roleManager.CreateAsync(new ApplicationRole(roleName));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Security.Claims;
using AutenticazioneSvc.BusinessLayer.Extensions;
using AutenticazioneSvc.DataAccessLayer.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;

namespace AutenticazioneSvc.BusinessLayer.Requirements;

public class UserActiveHandler(UserManager<ApplicationUser> userManager) : AuthorizationHandler<UserActiveRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, UserActiveRequirement requirement)
{
if (context.User.Identity.IsAuthenticated)
{
var userId = context.User.GetId();
var user = await userManager.FindByIdAsync(userId.ToString());
var securityStamp = context.User.GetClaimValue(ClaimTypes.SerialNumber);

if (user != null && user.LockoutEnd.GetValueOrDefault() <= DateTimeOffset.UtcNow && securityStamp == user.SecurityStamp)
{
context.Succeed(requirement);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Microsoft.AspNetCore.Authorization;

namespace AutenticazioneSvc.BusinessLayer.Requirements;

public class UserActiveRequirement : IAuthorizationRequirement
{ }
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AutenticazioneSvc.BusinessLayer;

public static class RoleNames
{
public const string Administrator = nameof(Administrator);
public const string PowerUser = nameof(PowerUser);
public const string User = nameof(User);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using AutenticazioneSvc.Shared.DTO;

namespace AutenticazioneSvc.BusinessLayer.Services;

public interface IIdentityService
{
Task<RegisterResponse> RegisterAsync(RegisterRequest request);
Task<AuthResponse> LoginAsync(LoginRequest request);
Task<AuthResponse> RefreshTokenAsync(RefreshTokenRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using AutenticazioneSvc.BusinessLayer.Extensions;
using AutenticazioneSvc.BusinessLayer.Settings;
using AutenticazioneSvc.DataAccessLayer.Entities;
using AutenticazioneSvc.Shared.DTO;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace AutenticazioneSvc.BusinessLayer.Services;

public class IdentityService(IOptions<JwtOptions> jwtOptions, UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager) : IIdentityService
{
private readonly JwtOptions jwtSettings = jwtOptions.Value;

public async Task<AuthResponse> LoginAsync(LoginRequest request)
{
var signInResult = await signInManager.PasswordSignInAsync(request.UserName, request.Password, false, false);

if (!signInResult.Succeeded)
{
return null!;
}

var user = await userManager.FindByNameAsync(request.UserName);

if (user == null)
{
return null!;
}

await userManager.UpdateSecurityStampAsync(user);

var userRoles = await userManager.GetRolesAsync(user);

var claims = new List<Claim>()
{
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, request.UserName),
new(ClaimTypes.Email, user.Email ?? string.Empty),
new(ClaimTypes.SerialNumber, user.SecurityStamp!.ToString()),

new("FullName", string.Join(" ", user.LastName, user.FirstName))

//TODO: Manca il claim per la licenza

//TODO: Manca la union della lista dei permessi

//TODO: Manca la union della lista dei moduli a cui l'utente può accedere
}
.Union(userRoles.Select(role => new Claim(ClaimTypes.Role, role))).ToList();

var loginResponse = CreateToken(claims);

user.RefreshToken = loginResponse.RefreshToken;
user.RefreshTokenExpirationDate = DateTime.UtcNow.AddMinutes(jwtSettings.RefreshTokenExpirationMinutes);

await userManager.UpdateAsync(user);

return loginResponse;
}

public async Task<AuthResponse> RefreshTokenAsync(RefreshTokenRequest request)
{
var user = ValidateAccessToken(request.AccessToken);

if (user != null)
{
var userId = user.GetId();
var dbUser = await userManager.FindByIdAsync(userId.ToString());

if (dbUser?.RefreshToken == null || dbUser?.RefreshTokenExpirationDate < DateTime.UtcNow || dbUser?.RefreshToken != request.RefreshToken)
{
return null!;
}

var loginResponse = CreateToken(user.Claims.ToList());

dbUser.RefreshToken = loginResponse.RefreshToken;
dbUser.RefreshTokenExpirationDate = DateTime.UtcNow.AddMinutes(jwtSettings.RefreshTokenExpirationMinutes);

await userManager.UpdateAsync(dbUser);

return loginResponse;
}

return null!;
}

private AuthResponse CreateToken(IList<Claim> claims)
{
var audienceClaim = claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Aud);
claims.Remove(audienceClaim!);

var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecurityKey));
var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256);

var jwtSecurityToken = new JwtSecurityToken(jwtSettings.Issuer, jwtSettings.Audience, claims,
DateTime.UtcNow, DateTime.UtcNow.AddMinutes(jwtSettings.AccessTokenExpirationMinutes), signingCredentials);

var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);

var italyTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time");
var expiredLocalNow = TimeZoneInfo.ConvertTimeFromUtc(jwtSecurityToken.ValidTo, italyTimeZone);

var response = new AuthResponse
{
AccessToken = accessToken,
RefreshToken = GenerateRefreshToken(),
ExpiredToken = expiredLocalNow
};

return response;

static string GenerateRefreshToken()
{
var randomNumber = new byte[256];
using var generator = RandomNumberGenerator.Create();
generator.GetBytes(randomNumber);

return Convert.ToBase64String(randomNumber);
}
}

private ClaimsPrincipal ValidateAccessToken(string accessToken)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Audience,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecurityKey)),
RequireExpirationTime = true,
ClockSkew = TimeSpan.Zero
};

var tokenHandler = new JwtSecurityTokenHandler();

try
{
var user = tokenHandler.ValidateToken(accessToken, tokenValidationParameters, out var securityToken);

if (securityToken is JwtSecurityToken jwtSecurityToken && jwtSecurityToken.Header.Alg == SecurityAlgorithms.HmacSha256)
{
return user;
}
}
catch
{ }

return null!;
}

public async Task<RegisterResponse> RegisterAsync(RegisterRequest request)
{
var user = new ApplicationUser
{
FirstName = request.FirstName,
LastName = request.LastName,
Email = request.Email,
UserName = request.Email,

EmailConfirmed = true
};

var result = await userManager.CreateAsync(user, request.Password);

if (result.Succeeded)
{
result = await userManager.AddToRoleAsync(user, RoleNames.User);
}

var response = new RegisterResponse
{
Succeeded = result.Succeeded,
Errors = result.Errors.Select(e => e.Description)
};

return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace AutenticazioneSvc.BusinessLayer.Settings;

public class JwtOptions
{
public string Issuer { get; init; } = null!;
public string Audience { get; init; } = null!;
public string SecurityKey { get; init; } = null!;
public int AccessTokenExpirationMinutes { get; init; }
public int RefreshTokenExpirationMinutes { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Reflection;
using AutenticazioneSvc.DataAccessLayer.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace AutenticazioneSvc.DataAccessLayer;

public class AppDbContext(DbContextOptions<AppDbContext> options) : IdentityDbContext<ApplicationUser, ApplicationRole, Guid,
IdentityUserClaim<Guid>, ApplicationUserRole, IdentityUserLogin<Guid>, IdentityRoleClaim<Guid>, IdentityUserToken<Guid>>(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AutenticazioneSvc.Shared\AutenticazioneSvc.Shared.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Identity;

namespace AutenticazioneSvc.DataAccessLayer.Entities;

public class ApplicationRole : IdentityRole<Guid>
{
public ApplicationRole()
{ }

public ApplicationRole(string roleName) : base(roleName)
{ }

public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}
Loading

0 comments on commit 6bf6234

Please sign in to comment.