Skip to content

Commit

Permalink
Merge pull request #19 from Star-Academy/feature/logout
Browse files Browse the repository at this point in the history
feat: add logout api
  • Loading branch information
mobinbr authored Aug 22, 2024
2 parents c4b2c4f + c59f1a3 commit 8057781
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 62 deletions.
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

0 comments on commit 8057781

Please sign in to comment.