Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add logout api #19

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/Application/Interfaces/IJwtGenerator.cs

This file was deleted.

10 changes: 10 additions & 0 deletions src/Application/Interfaces/ITokenService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Domain.Entities;

namespace Application.Interfaces;

public interface ITokenService
{
string GenerateToken(AppUser user, string role);
Task<bool> IsTokenInvalidatedAsync(string token);
Task AddInvalidatedTokenAsync(string token);
}
1 change: 1 addition & 0 deletions src/Application/Interfaces/Services/IIdentityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public interface IIdentityService
Task<Result<LoginUserResponse>> Login(LoginUserRequest loginDto);
Task<Result> ChangeRole(ChangeRoleRequest changeRoleRequest);
Task<List<GetUserResponse>> GetUsersAsync();
Task<Result> Logout(string token);
}
14 changes: 10 additions & 4 deletions src/Application/Services/DomainService/IdentityService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ public class IdentityService : IIdentityService
{
private readonly IUserManagerRepository _userManagerRepository;
private readonly IRoleManagerRepository _roleManagerRepository;
private readonly IJwtGenerator _jwtGenerator;
private readonly ITokenService _tokenService;
public IdentityService(IUserManagerRepository userManagerRepository,
IRoleManagerRepository roleManagerRepository,
IJwtGenerator jwtGenerator)
ITokenService tokenService)
{
_userManagerRepository = userManagerRepository;
_roleManagerRepository = roleManagerRepository;
_jwtGenerator = jwtGenerator;
_tokenService = tokenService;
}

public async Task<Result<CreateUserResponse>> SignUpUser(CreateUserRequest createUserRequest)
Expand Down Expand Up @@ -74,7 +74,7 @@ public async Task<Result<LoginUserResponse>> Login(LoginUserRequest loginUserReq
if (!succeed) return Result<LoginUserResponse>.Fail("Username/Email not found and/or password incorrect");

var role = await _userManagerRepository.GetRoleAsync(appUser);
var token = _jwtGenerator.GenerateToken(appUser, role);
var token = _tokenService.GenerateToken(appUser, role);

return Result<LoginUserResponse>.Ok(appUser.ToLoginUserResponse(role, token));
}
Expand Down Expand Up @@ -108,4 +108,10 @@ public async Task<List<GetUserResponse>> GetUsersAsync()

return userWithRoles;
}

public async Task<Result> Logout(string token)
{
await _tokenService.AddInvalidatedTokenAsync(token);
return Result.Ok();
}
}
14 changes: 14 additions & 0 deletions src/Web/Controllers/IdentityController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,18 @@ public async Task<IActionResult> GetUsersAsync()

return Ok(appUsersWithRoles);
}

[HttpPost]
[Authorize]
public async Task<IActionResult> Logout()
{
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var result = await _identityService.Logout(token);
if (!result.Succeed)
{
return BadRequest(Errors.New(nameof(Logout), result.Message));
}

return Ok("Logged out successfully!");
}
}
6 changes: 6 additions & 0 deletions src/Web/Identity/RequiresClaimAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public void OnAuthorization(AuthorizationFilterContext context)
{
var user = context.HttpContext.User;

if (user.Identity is { IsAuthenticated: false })
{
context.Result = new UnauthorizedResult();
return;
}

var hasRequiredRole = _roles.Any(role => user.HasClaim(_claimName, role));

if (!hasRequiredRole)
Expand Down
35 changes: 35 additions & 0 deletions src/Web/Middleware/TokenValidationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Application.Interfaces;

namespace Web.Middleware;

public class TokenValidationMiddleware
{
private readonly RequestDelegate _next;
private readonly IServiceProvider _serviceProvider;

public TokenValidationMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
{
_next = next;
_serviceProvider = serviceProvider;
}

public async Task InvokeAsync(HttpContext context)
{
// Create a scope to resolve scoped services
using var scope = _serviceProvider.CreateScope();
var tokenService = scope.ServiceProvider.GetRequiredService<ITokenService>();

var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader != null && authHeader.StartsWith("Bearer "))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
if (await tokenService.IsTokenInvalidatedAsync(token))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
}

await _next(context);
}
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,60 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Application.Interfaces;
using Domain.Entities;
using Microsoft.IdentityModel.Tokens;
using Web.Identity;

namespace Web.Services;

public class JwtGeneratorService : IJwtGenerator
{
private readonly IConfiguration _configuration;
private readonly SymmetricSecurityKey _symmetricSecurityKey;
public JwtGeneratorService(IConfiguration configuration)
{
_configuration = configuration;
_symmetricSecurityKey = new SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSettings:Key"])
);
}
public string GenerateToken(AppUser user, string role)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(Claims.UserId, user.Id),
new Claim(Claims.Role, role)
};

var credentials = new SigningCredentials(_symmetricSecurityKey, SecurityAlgorithms.HmacSha512Signature);

var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddHours(8),
SigningCredentials = credentials,
Issuer = _configuration["JwtSettings:Issuer"],
Audience = _configuration["JwtSettings:Audience"]
};

var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
using System.Collections.Concurrent;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Application.Interfaces;
using Domain.Entities;
using Microsoft.IdentityModel.Tokens;
using Web.Identity;

namespace Web.Services;

public class TokenService : ITokenService
{
private readonly IConfiguration _configuration;
private readonly SymmetricSecurityKey _symmetricSecurityKey;
private static readonly ConcurrentDictionary<string, DateTime> _invalidatedTokens = new();

public TokenService(IConfiguration configuration)
{
_configuration = configuration;
_symmetricSecurityKey = new SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSettings:Key"])
);
}

public string GenerateToken(AppUser user, string role)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(Claims.UserId, user.Id),
new Claim(Claims.Role, role)
};

var credentials = new SigningCredentials(_symmetricSecurityKey, SecurityAlgorithms.HmacSha512Signature);

var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddHours(8),
SigningCredentials = credentials,
Issuer = _configuration["JwtSettings:Issuer"],
Audience = _configuration["JwtSettings:Audience"]
};

var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}

public Task<bool> IsTokenInvalidatedAsync(string token)
{
return Task.FromResult(_invalidatedTokens.ContainsKey(token));
}

public Task AddInvalidatedTokenAsync(string token)
{
_invalidatedTokens[token] = DateTime.UtcNow;
return Task.CompletedTask;
}
}
8 changes: 7 additions & 1 deletion src/Web/Startup/MiddlewareExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Web.Startup;
using Web.Middleware;

namespace Web.Startup;

public static class MiddlewareExtensions
{
Expand All @@ -12,8 +14,12 @@ public static WebApplication UseMiddlewareServices(this WebApplication app)

app.UseHttpsRedirection();
app.UseCors("AllowSpecificOrigins");

app.UseMiddleware<TokenValidationMiddleware>();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

return app;
Expand Down
2 changes: 1 addition & 1 deletion src/Web/Startup/ServiceExtensions.DI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static partial class ServiceExtensions
{
public static void AddApplicationServices(this IServiceCollection services)
{
services.AddScoped<IJwtGenerator, JwtGeneratorService>();
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IRoleManagerRepository, RoleManagerRepository>();
services.AddScoped<IUserManagerRepository, UserManagerRepository>();
services.AddScoped<IIdentityService, IdentityService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ public class IdentityServiceTests
{
private readonly IUserManagerRepository _userManagerRepository;
private readonly IRoleManagerRepository _roleManagerRepository;
private readonly IJwtGenerator _jwtGenerator;
private readonly ITokenService _tokenService;
private readonly IdentityService _identityService;

public IdentityServiceTests()
{
_userManagerRepository = Substitute.For<IUserManagerRepository>();
_roleManagerRepository = Substitute.For<IRoleManagerRepository>();
_jwtGenerator = Substitute.For<IJwtGenerator>();
_identityService = new IdentityService(_userManagerRepository, _roleManagerRepository, _jwtGenerator);
_tokenService = Substitute.For<ITokenService>();
_identityService = new IdentityService(_userManagerRepository, _roleManagerRepository, _tokenService);
}

// Signup Tests
Expand Down Expand Up @@ -230,7 +230,7 @@ public async Task Login_WhenLoginSucceeds_ReturnsSuccessResult()
.Returns(Task.FromResult(true));
_userManagerRepository.GetRoleAsync(appUser)
.Returns(Task.FromResult(role));
_jwtGenerator.GenerateToken(appUser, role)
_tokenService.GenerateToken(appUser, role)
.Returns(token);

// Act
Expand Down
Loading