Skip to content

Commit 0f5622b

Browse files
Merge pull request omid-ahmadpour#9 from ahmedanwar100/refresh-token-endpoint
add refreshToken endpoint.
2 parents 76b80bb + cf4a603 commit 0f5622b

29 files changed

+1957
-36
lines changed

src/Common/General/SiteSettings.cs

+1
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ public class JwtSettings
2424
public string Audience { get; set; }
2525
public int NotBeforeMinutes { get; set; }
2626
public int ExpirationMinutes { get; set; }
27+
public int RefreshTokenValidityInDays { get; set; }
2728
}
2829
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using MediatR;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace CleanTemplate.Application.Users.Command.RefreshToken
9+
{
10+
public class RefreshTokenCommand : IRequest<RefreshTokenResponse>
11+
{
12+
public string RefreshToken { get; set; }
13+
public string AccessToken { get; set; }
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace CleanTemplate.Application.Users.Command.RefreshToken
8+
{
9+
public class RefreshTokenResponse
10+
{
11+
public string RefreshToken { get; set; }
12+
public string AccessToken { get; set; }
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.DataAnnotations.Schema;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace CleanTemplate.Domain.Entities.Users
9+
{
10+
public class RefreshToken : IEntity<int>
11+
{
12+
public int Id { get; set; }
13+
14+
[ForeignKey(name: nameof(User))]
15+
public int UserId { get; set; }
16+
public User User { get; set; }
17+
public string Token { get; set; }
18+
public DateTime Created { get; set; }
19+
public DateTime Updated { get; set; } = DateTime.Now;
20+
public DateTime ExpiryTime { get; set; }
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using CleanTemplate.Domain.Entities.Users;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace CleanTemplate.Domain.IRepositories
6+
{
7+
public interface IRefreshTokenRepository : IRepository<RefreshToken>
8+
{
9+
Task AddOrUpdateRefreshTokenAsync(RefreshToken refreshToken, CancellationToken cancellationToken);
10+
Task<bool> ValidateRefreshTokenAsync(RefreshToken refreshToken, CancellationToken cancellationToken);
11+
}
12+
}

src/Infrastructure/Persistance/CommandHandlers/Users/LoginCommandHandler.cs

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using CleanTemplate.Common;
33
using CleanTemplate.Common.Exceptions;
44
using CleanTemplate.Domain.Entities.Users;
5+
using CleanTemplate.Domain.IRepositories;
56
using CleanTemplate.Persistance.Jwt;
67
using MediatR;
78
using Microsoft.AspNetCore.Identity;
@@ -15,12 +16,15 @@ public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponse>
1516
{
1617
private readonly UserManager<User> _userManager;
1718
private readonly IJwtService _jwtService;
19+
private readonly IRefreshTokenRepository _refreshTokenRepository;
1820

1921
public LoginCommandHandler(UserManager<User> userManager,
20-
IJwtService jwtService)
22+
IJwtService jwtService,
23+
IRefreshTokenRepository refreshTokenRepository)
2124
{
2225
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
2326
_jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService));
27+
_refreshTokenRepository = refreshTokenRepository ?? throw new ArgumentNullException(nameof(refreshTokenRepository));
2428
}
2529

2630
public async Task<LoginResponse> Handle(LoginCommand request, CancellationToken cancellationToken)
@@ -37,7 +41,13 @@ public async Task<LoginResponse> Handle(LoginCommand request, CancellationToken
3741
throw new CleanArchAppException("username or password is incorrect");
3842

3943
var jwt = await _jwtService.GenerateAsync(user);
40-
44+
var refreshToken = new RefreshToken
45+
{
46+
UserId = user.Id,
47+
ExpiryTime = DateTime.Now.AddDays(jwt.refreshToken_expiresIn),
48+
Token = jwt.refresh_token
49+
};
50+
await _refreshTokenRepository.AddOrUpdateRefreshTokenAsync(refreshToken: refreshToken, cancellationToken);
4151
return new LoginResponse
4252
{
4353
accessToken = jwt.access_token,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using CleanTemplate.Application.Users.Command.RefreshToken;
2+
using CleanTemplate.Common;
3+
using CleanTemplate.Common.Exceptions;
4+
using CleanTemplate.Domain.Entities.Users;
5+
using CleanTemplate.Domain.IRepositories;
6+
using CleanTemplate.Persistance.Jwt;
7+
using MediatR;
8+
using Microsoft.AspNetCore.Identity;
9+
using System;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
13+
namespace CleanTemplate.Persistance.CommandHandlers.Users
14+
{
15+
public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, RefreshTokenResponse>
16+
{
17+
private readonly UserManager<User> _userManager;
18+
private readonly IJwtService _jwtService;
19+
private readonly IRefreshTokenRepository _refreshTokenRepository;
20+
21+
public RefreshTokenCommandHandler(UserManager<User> userManager,
22+
IJwtService jwtService,
23+
IRefreshTokenRepository refreshTokenRepository)
24+
{
25+
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
26+
_jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService));
27+
_refreshTokenRepository = refreshTokenRepository ?? throw new ArgumentNullException(nameof(refreshTokenRepository));
28+
}
29+
public async Task<RefreshTokenResponse> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
30+
{
31+
if (request is null)
32+
throw new InvalidNullInputException(nameof(request));
33+
34+
var userId = _jwtService.ValidateJwtAccessTokenAsync(request.AccessToken);
35+
if (userId == null)
36+
throw new CleanArchAppException("AccessToken is not valid");
37+
38+
var refreshToken = new RefreshToken
39+
{
40+
UserId = userId.Value,
41+
Token = request.RefreshToken
42+
};
43+
await _refreshTokenRepository.ValidateRefreshTokenAsync(refreshToken, cancellationToken);
44+
45+
var user = await _userManager.FindByIdAsync(userId.ToString());
46+
var jwt = await _jwtService.GenerateAsync(user);
47+
var updateRefreshToken = new RefreshToken
48+
{
49+
UserId = user.Id,
50+
ExpiryTime = DateTime.Now.AddDays(jwt.refreshToken_expiresIn),
51+
Token = jwt.refresh_token
52+
};
53+
await _refreshTokenRepository.AddOrUpdateRefreshTokenAsync(refreshToken: updateRefreshToken, cancellationToken);
54+
return new RefreshTokenResponse
55+
{
56+
AccessToken = jwt.access_token,
57+
RefreshToken = jwt.refresh_token
58+
};
59+
}
60+
}
61+
}

src/Infrastructure/Persistance/Jwt/AccessToken.cs

+9
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ public class AccessToken
99
public string refresh_token { get; set; }
1010
public string token_type { get; set; }
1111
public int expires_in { get; set; }
12+
public int refreshToken_expiresIn { get; set; }
1213

1314
public AccessToken(JwtSecurityToken securityToken)
1415
{
1516
access_token = new JwtSecurityTokenHandler().WriteToken(securityToken);
1617
token_type = "Bearer";
1718
expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds;
1819
}
20+
public AccessToken(JwtSecurityToken securityToken,string refreshToken, int refreshTokenExpiresIn)
21+
{
22+
access_token = new JwtSecurityTokenHandler().WriteToken(securityToken);
23+
token_type = "Bearer";
24+
expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds;
25+
refresh_token = refreshToken;
26+
refreshToken_expiresIn = refreshTokenExpiresIn;
27+
}
1928
}
2029
}

src/Infrastructure/Persistance/Jwt/IJwtService.cs

+1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ namespace CleanTemplate.Persistance.Jwt
66
public interface IJwtService
77
{
88
Task<AccessToken> GenerateAsync(User user);
9+
int? ValidateJwtAccessTokenAsync(string token);
910
}
1011
}

src/Infrastructure/Persistance/Jwt/JwtService.cs

+43-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
using System;
88
using System.Collections.Generic;
99
using System.IdentityModel.Tokens.Jwt;
10+
using System.Linq;
1011
using System.Security.Claims;
12+
using System.Security.Cryptography;
1113
using System.Text;
1214
using System.Threading.Tasks;
1315

@@ -30,8 +32,8 @@ public async Task<AccessToken> GenerateAsync(User user)
3032
var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character
3133
var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature);
3234

33-
var encryptionkey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character
34-
var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionkey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256);
35+
var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character
36+
var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256);
3537

3638
var claims = await GetClaimsAsync(user);
3739

@@ -51,7 +53,38 @@ public async Task<AccessToken> GenerateAsync(User user)
5153

5254
var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor);
5355

54-
return new AccessToken(securityToken);
56+
return new AccessToken(securityToken: securityToken,
57+
refreshToken: GenerateRefreshToken(),
58+
refreshTokenExpiresIn: _siteSetting.JwtSettings.RefreshTokenValidityInDays);
59+
}
60+
61+
public int? ValidateJwtAccessTokenAsync(string token)
62+
{
63+
var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character
64+
var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character
65+
66+
var tokenHandler = new JwtSecurityTokenHandler();
67+
try
68+
{
69+
tokenHandler.ValidateToken(token, new TokenValidationParameters
70+
{
71+
ValidateIssuerSigningKey = true,
72+
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
73+
TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey),
74+
ValidateIssuer = false,
75+
ValidateAudience = false,
76+
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
77+
ClockSkew = TimeSpan.Zero
78+
}, out SecurityToken validatedToken);
79+
80+
var jwtSecurityToken = (JwtSecurityToken)validatedToken;
81+
var userId = int.Parse(jwtSecurityToken.Claims.First(claim => claim.Type == "nameid").Value);
82+
return userId;
83+
}
84+
catch
85+
{
86+
return null;
87+
}
5588
}
5689

5790
private async Task<IEnumerable<Claim>> GetClaimsAsync(User user)
@@ -69,5 +102,12 @@ private async Task<IEnumerable<Claim>> GetClaimsAsync(User user)
69102

70103
return claims;
71104
}
105+
private static string GenerateRefreshToken()
106+
{
107+
var randomNumber = new byte[64];
108+
using var rng = RandomNumberGenerator.Create();
109+
rng.GetBytes(randomNumber);
110+
return Convert.ToBase64String(randomNumber);
111+
}
72112
}
73113
}

0 commit comments

Comments
 (0)