Skip to content

Commit c87be7e

Browse files
committed
jwt refresh token implemented
1 parent d70a5d8 commit c87be7e

File tree

5 files changed

+121
-18
lines changed

5 files changed

+121
-18
lines changed

ReactwithDotnetCore/Controllers/LoginController.cs

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Data;
88
using System.IdentityModel.Tokens.Jwt;
99
using System.Security.Claims;
10+
using System.Security.Cryptography;
1011
using System.Text;
1112

1213
namespace ReactwithDotnetCore.Controllers
@@ -17,14 +18,14 @@ public class LoginController(IConfiguration configuration) : Controller
1718

1819
[AllowAnonymous]
1920
[HttpPost("userlogin")]
20-
public IActionResult UserLogin([FromBody] User login)
21+
public async Task<IActionResult> UserLogin([FromBody] User login)
2122
{
2223
IActionResult response = Unauthorized();
23-
var user = AuthenticateUser(login);
24+
var user = await AuthenticateUser(login);
2425
if (user != null)
2526
{
26-
var tokenString = GenerateJSONWebToken(user);
27-
response = Ok(new { message = "success", token = tokenString });
27+
var (tokenString, refreshToken) = GenerateTokens(user);
28+
response = Ok(new { message = "success", token = tokenString, refreshToken });
2829
}
2930
return response;
3031
}
@@ -56,10 +57,50 @@ public async Task<IActionResult> UserRegister([FromBody] User register)
5657
}
5758
}
5859

59-
private string GenerateJSONWebToken(User userInfo)
60+
/// <summary>
61+
/*
62+
*
63+
The purpose of a refresh token is to provide a way to obtain a new access token without requiring the
64+
user to re-enter their credentials.Access tokens have a limited lifespan, and when they expire, the
65+
user would typically need to log in again to get a new access token.
66+
67+
With a refresh token mechanism, when the access token expires, the client can use the refresh token
68+
to obtain a new access token without requiring the user's credentials. This helps in maintaining a
69+
balance between security and user convenience. The refresh token is a long-lived token that can
70+
be securely stored by the client and used to request new access tokens as needed.
71+
*
72+
*/
73+
/// </summary>
74+
/// <param name="refreshTokenRequest"></param>
75+
/// <returns></returns>
76+
[AllowAnonymous]
77+
[HttpPost("refreshtoken")]
78+
public IActionResult RefreshToken([FromBody] RefreshTokenRequest refreshTokenRequest)
79+
{
80+
IActionResult response = BadRequest("Invalid token");
81+
var principal = GetPrincipalFromExpiredToken(refreshTokenRequest.Token);
82+
83+
if (principal != null)
84+
{
85+
var username = principal?.Claims?.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier")?.Value;
86+
if (username != null)
87+
{
88+
var user = GetUserByUsername(username);
89+
90+
if (user != null && refreshTokenRequest.RefreshToken == user.RefreshToken)
91+
{
92+
var (tokenString, newRefreshToken) = GenerateTokens(user);
93+
response = Ok(new { token = tokenString, refreshToken = newRefreshToken });
94+
}
95+
}
96+
}
97+
98+
return response;
99+
}
100+
101+
private (string tokenString, string refreshToken) GenerateTokens(User userInfo)
60102
{
61-
// Ensure the key has at least 256 bits
62-
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration?["Jwt:Key"]?.PadRight(32)));
103+
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]?.PadRight(32) ?? string.Empty));
63104
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
64105

65106
var claims = new[] {
@@ -75,20 +116,74 @@ private string GenerateJSONWebToken(User userInfo)
75116
expires: DateTime.Now.AddMinutes(120),
76117
signingCredentials: credentials);
77118

78-
return new JwtSecurityTokenHandler().WriteToken(token);
119+
var refreshToken = GenerateRefreshToken();
120+
userInfo.RefreshToken = refreshToken; // Save refresh token to user in your data store
121+
122+
// Update the refresh token in the database
123+
UpdateRefreshTokenInDatabase(userInfo.Username, refreshToken);
124+
125+
return (new JwtSecurityTokenHandler().WriteToken(token), refreshToken);
79126
}
80127

81-
private User AuthenticateUser(User login)
128+
private static string GenerateRefreshToken()
129+
{
130+
// Generate a random refresh token (you may use a more sophisticated method)
131+
var randomNumber = new byte[32];
132+
using var rng = RandomNumberGenerator.Create();
133+
rng.GetBytes(randomNumber);
134+
return Convert.ToBase64String(randomNumber);
135+
}
136+
137+
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
138+
{
139+
var tokenValidationParameters = new TokenValidationParameters
140+
{
141+
ValidateIssuer = true,
142+
ValidateAudience = true,
143+
ValidateLifetime = false, // This will allow an expired token to be parsed
144+
ValidateIssuerSigningKey = true,
145+
ValidIssuer = configuration["Jwt:Issuer"],
146+
ValidAudience = configuration["Jwt:Audience"],
147+
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty))
148+
};
149+
150+
var tokenHandler = new JwtSecurityTokenHandler();
151+
152+
// The following line will throw an exception if the token is expired
153+
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
154+
155+
return principal;
156+
}
157+
158+
private async Task<User> AuthenticateUser(User login)
82159
{
83160
using IDbConnection dbConnection = new SqlConnection(_connectionString);
84161
dbConnection.Open();
85162

86-
// Example: Authenticate user based on Username and Password
87163
string query = "SELECT * FROM TBLB_User WITH(NOLOCK) WHERE Username = @Username AND Password = @Password";
88-
var users = dbConnection.Query<User>(query, new { login.Username, login.Password });
164+
var users = await dbConnection.QueryAsync<User>(query, new { login.Username, login.Password });
165+
166+
return users.FirstOrDefault() ?? new User();
167+
}
168+
169+
private User GetUserByUsername(string username)
170+
{
171+
using IDbConnection dbConnection = new SqlConnection(_connectionString);
172+
dbConnection.Open();
173+
174+
string query = "SELECT * FROM TBLB_User WITH(NOLOCK) WHERE Username = @Username";
175+
var user = dbConnection.Query<User>(query, new { Username = username }).FirstOrDefault();
176+
177+
return user ?? new User();
178+
}
179+
180+
private void UpdateRefreshTokenInDatabase(string username, string newRefreshToken)
181+
{
182+
using IDbConnection dbConnection = new SqlConnection(_connectionString);
183+
dbConnection.Open();
89184

90-
// Assuming there should be only one matching user
91-
return users.FirstOrDefault();
185+
string updateQuery = "UPDATE TBLB_User SET RefreshToken = @RefreshToken WHERE Username = @Username";
186+
dbConnection.Execute(updateQuery, new { RefreshToken = newRefreshToken, Username = username });
92187
}
93188
}
94189
}

ReactwithDotnetCore/Model/User.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,13 @@ public class User
66
public string Password { get; set; } = string.Empty;
77
public string EmailAddress { get; set; } = string.Empty;
88
public DateTime DateOfJoin { get; set; } = DateTime.Now;
9+
public string RefreshToken { get; set; } = string.Empty;
910
}
11+
12+
public class RefreshTokenRequest
13+
{
14+
public string Token { get; set; } = string.Empty;
15+
public string RefreshToken { get; set; } = string.Empty;
16+
}
17+
1018
}

ReactwithDotnetCore/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
using Microsoft.OpenApi.Models;
55
using System.Text;
66

7-
const string CorsPolicyName = "_myCorsPolicy";
7+
const string CorsPolicyName = "_APIPolicy";
88

99
var builder = WebApplication.CreateBuilder(args);
1010

@@ -17,11 +17,11 @@
1717
{
1818
ValidateIssuer = true,
1919
ValidateAudience = true,
20-
ValidateLifetime = true,
20+
ValidateLifetime = false, // This will allow an expired token to be parsed
2121
ValidateIssuerSigningKey = true,
2222
ValidIssuer = builder.Configuration["Jwt:Issuer"],
2323
ValidAudience = builder.Configuration["Jwt:Audience"],
24-
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
24+
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"] ?? string.Empty))
2525
};
2626
});
2727

ReactwithDotnetCore/ReactwithDotnetCore.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Dapper" Version="2.1.28" />
14-
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
14+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
1515
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
16-
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
16+
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
1717
</ItemGroup>
1818

1919
</Project>

ReactwithDotnetCore/db-script.sql

174 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)