-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
274 lines (229 loc) · 9.1 KB
/
Program.cs
File metadata and controls
274 lines (229 loc) · 9.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// README: JWT + Refresh Token Minimal API sample (ASP.NET Core)
// Run: dotnet new web -n JwtRefreshDemo
// Replace Program.cs with the contents below. This sample uses an in-memory user store and refresh-token store for demo purposes.
// For production: store users and refresh tokens in a secure DB, hash refresh tokens, use HTTPS, strong keys and rotate keys.
// ----------------------------
// Program.cs
// ----------------------------
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// ----- CONFIG -----
var jwtSettings = new JwtSettings
{
Issuer = "JwtRefreshDemo",
Audience = "JwtRefreshDemoClient",
Secret = "VeryStrongSecretKey_For_Demo_PleaseChange_!1234567890",
AccessTokenExpiryMinutes = 15,
RefreshTokenExpiryDays = 7
};
//registering
builder.Services.AddSingleton(jwtSettings);
builder.Services.AddSingleton<IUserRepository, InMemoryUserRepository>();
builder.Services.AddSingleton<IRefreshTokenStore, InMemoryRefreshTokenStore>();
builder.Services.AddSingleton<ITokenService, TokenService>();
// JWT authentication
var key = Encoding.UTF8.GetBytes(jwtSettings.Secret);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(key),
ClockSkew = TimeSpan.Zero // strict expiry check
};
// Optional: if you want to read expired access token in refresh endpoint, set this:
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = ctx =>
{
// swallow for demo
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// -------------------------------------------------
// Minimal API endpoints: /auth/login, /auth/refresh, /me
// -------------------------------------------------
app.MapPost("/auth/login", (LoginRequest req, IUserRepository users, ITokenService tokenService, IRefreshTokenStore rtStore) =>
{
var user = users.ValidateCredentials(req.Username, req.Password);
if (user == null) return Results.Unauthorized();
var accessToken = tokenService.GenerateAccessToken(user);
var refreshToken = tokenService.GenerateRefreshToken();
// Save hashed refresh token with metadata in store
rtStore.SaveRefreshToken(user.Id, refreshToken, tokenService.GetRefreshTokenExpiry());
return Results.Ok(new TokenResponse { AccessToken = accessToken, RefreshToken = refreshToken });
});
app.MapPost("/auth/refresh", async (RefreshRequest req, IRefreshTokenStore rtStore, ITokenService tokenService, IUserRepository users) =>
{
// Validate request
if (string.IsNullOrWhiteSpace(req.RefreshToken)) return Results.BadRequest("Missing refresh token");
// Validate refresh token from store
var stored = await rtStore.GetByTokenAsync(req.RefreshToken);
if (stored == null) return Results.Unauthorized();
if (stored.IsExpired) return Results.Unauthorized();
if (stored.IsRevoked) return Results.Unauthorized();
// Optional: Check that the access token belongs to same user (if you send it). For demo, we use stored.UserId
var user = users.GetById(stored.UserId);
if (user == null) return Results.Unauthorized();
// Token rotation: revoke old refresh token and create a new one
await rtStore.RevokeAsync(stored.Token);
var newAccessToken = tokenService.GenerateAccessToken(user);
var newRefreshToken = tokenService.GenerateRefreshToken();
await rtStore.SaveRefreshTokenAsync(user.Id, newRefreshToken, tokenService.GetRefreshTokenExpiry());
return Results.Ok(new TokenResponse { AccessToken = newAccessToken, RefreshToken = newRefreshToken });
});
app.MapGet("/me", (ClaimsPrincipal userPrincipal) =>
{
var sub = userPrincipal.FindFirstValue(ClaimTypes.NameIdentifier);
return Results.Ok(new { Message = "Protected data", UserId = sub });
})
.RequireAuthorization();
app.Run();
// ----------------------------
// Supporting types and simple in-memory stores
// ----------------------------
public record LoginRequest(string Username, string Password);
public record RefreshRequest(string AccessToken, string RefreshToken);
public record TokenResponse { public string AccessToken { get; init; } public string RefreshToken { get; init; } }
public class JwtSettings
{
public string Issuer { get; init; }
public string Audience { get; init; }
public string Secret { get; init; }
public int AccessTokenExpiryMinutes { get; init; }
public int RefreshTokenExpiryDays { get; init; }
}
public class User
{
public Guid Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; } // For demo, plain text optional
}
public interface IUserRepository
{
User ValidateCredentials(string username, string password);
User GetById(Guid id);
}
public class InMemoryUserRepository : IUserRepository
{
private readonly List<User> _users = new()
{
new User { Id = Guid.Parse("11111111-1111-1111-1111-111111111111"), Username = "reena", PasswordHash = "password123" }
};
public User ValidateCredentials(string username, string password)
{
return _users.FirstOrDefault(u => u.Username == username && u.PasswordHash == password);
}
public User GetById(Guid id) => _users.FirstOrDefault(u => u.Id == id);
}
public class RefreshTokenEntry
{
public string Token { get; set; } // store as plain for demo; in production store hashed version
public Guid UserId { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsRevoked { get; set; }
public bool IsUsed { get; set; }
public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
}
public interface IRefreshTokenStore
{
void SaveRefreshToken(Guid userId, string token, DateTime expiresAt);
Task SaveRefreshTokenAsync(Guid userId, string token, DateTime expiresAt);
Task<RefreshTokenEntry> GetByTokenAsync(string token);
Task RevokeAsync(string token);
}
public class InMemoryRefreshTokenStore : IRefreshTokenStore
{
private readonly List<RefreshTokenEntry> _store = new();
public void SaveRefreshToken(Guid userId, string token, DateTime expiresAt)
{
_store.Add(new RefreshTokenEntry { Token = token, UserId = userId, ExpiresAt = expiresAt });
}
public Task SaveRefreshTokenAsync(Guid userId, string token, DateTime expiresAt)
{
SaveRefreshToken(userId, token, expiresAt);
return Task.CompletedTask;
}
public Task<RefreshTokenEntry> GetByTokenAsync(string token)
{
var entry = _store.FirstOrDefault(x => x.Token == token);
return Task.FromResult(entry);
}
public Task RevokeAsync(string token)
{
var entry = _store.FirstOrDefault(x => x.Token == token);
if (entry != null)
{
entry.IsRevoked = true;
entry.IsUsed = true;
}
return Task.CompletedTask;
}
}
public interface ITokenService
{
string GenerateAccessToken(User user);
string GenerateRefreshToken();
DateTime GetRefreshTokenExpiry();
}
public class TokenService : ITokenService
{
private readonly JwtSettings _settings;
private readonly byte[] _key;
public TokenService(JwtSettings settings)
{
_settings = settings;
_key = Encoding.UTF8.GetBytes(_settings.Secret);
}
public string GenerateAccessToken(User user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, user.Username)
};
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpiryMinutes),
Issuer = _settings.Issuer,
Audience = _settings.Audience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(_key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(descriptor);
return tokenHandler.WriteToken(token);
}
public string GenerateRefreshToken()
{
var random = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(random);
return Convert.ToBase64String(random);
}
public DateTime GetRefreshTokenExpiry()
{
return DateTime.UtcNow.AddDays(_settings.RefreshTokenExpiryDays);
}
}
// End of file