();
+ builder.Services.AddAuthentication(opt =>
+ {
+ opt.DefaultScheme =
+ opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ }).AddJwtBearer(opt =>
+ {
+ opt.TokenValidationParameters = new TokenValidationParameters {
+ ValidateIssuer = true,
+ ValidateAudience = false,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = issuer,
+ ValidateLifetime = true,
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
+ ClockSkew = TimeSpan.Zero,
+ RoleClaimType = ClaimTypes.Role,
+ NameClaimType = ClaimTypes.Name,
+ };
+
+ opt.Events = new JwtBearerEvents {
+ OnAuthenticationFailed = context =>
+ {
+ if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) {
+ context.Response.Headers.Append("Token-Expired", "true");
+ }
+
+ return Task.CompletedTask;
+ },
+ OnTokenValidated = async context =>
+ {
+ if (context.Principal != null)
+ {
+ var p = context.Principal!;
-// Configure identity control
-app.UseAuthentication();
-app.UseAuthorization();
+ var id = p.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+ if (id == null) throw new SecurityTokenExpiredException("User is not defined in the token!");
-// Configure actual controllers
-app.MapControllers();
+ var stamp = p.FindFirst(FlawlessClaimsType.SecurityStamp)?.Value;
+ if (stamp == null) throw new SecurityTokenExpiredException("No valid SecurityStamp found.");
-// Configure fallback endpoints
-if (app.Environment.IsDevelopment())
-{
- app.MapOpenApi();
- app.UseSwagger();
- app.UseSwaggerUI();
- app.MapGet("/", () => Results.Redirect("/swagger/index.html"));
-}
-else
-{
- app.MapGet("/", () => "Please use client app to open this server.
");
-}
+ var db = context.HttpContext.RequestServices.GetRequiredService>();
-app.Run();
+ // Start validate user is existed and not expired.
+ var u = await db.FindByIdAsync(id!);
+ if (u == null) throw new SecurityTokenExpiredException("User is not existed in db.");
+ if (u.SecurityStamp != stamp) throw new SecurityTokenExpiredException("SecurityStamp is mismatched.");
+
+ context.HttpContext.User = p;
+ context.Success();
+ }
+ }
+ };
+ });
+ // builder.Services.AddAuthorization(opt =>
+ // {
+ // opt.DefaultPolicy =
+ // })
+ }
+
+ private static void SetupWebApplication(WebApplication app)
+ {
+ app.UseMiddleware();
+ app.UseRouting();
+
+ // Config WebSocket support.
+ app.UseWebSockets(new WebSocketOptions
+ {
+ KeepAliveInterval = TimeSpan.FromSeconds(60),
+ KeepAliveTimeout = TimeSpan.FromSeconds(300),
+ });
+ app.UseMiddleware();
+
+ // Configure identity control
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ // Configure actual controllers
+ app.MapControllers();
+
+ // Configure fallback endpoints
+ if (app.Environment.IsDevelopment())
+ {
+ app.MapOpenApi();
+ app.UseSwagger();
+ app.UseSwaggerUI();
+ app.MapGet("/", () => Results.Redirect("/swagger/index.html"));
+ }
+ else
+ {
+ app.MapGet("/", () => "Please use client app to open this server.
");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Server/Services/AppDbContext.cs b/Flawless.Server/Services/AppDbContext.cs
new file mode 100644
index 0000000..1fe39a5
--- /dev/null
+++ b/Flawless.Server/Services/AppDbContext.cs
@@ -0,0 +1,12 @@
+using Flawless.Server.Models;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+
+namespace Flawless.Server.Services;
+
+public class AppDbContext(DbContextOptions options)
+ : IdentityDbContext, Guid>(options)
+{
+ public DbSet RefreshTokens { get; set; }
+}
\ No newline at end of file
diff --git a/Flawless.Server/Models/RepositoryContext.cs b/Flawless.Server/Services/RepositoryContext.cs
similarity index 59%
rename from Flawless.Server/Models/RepositoryContext.cs
rename to Flawless.Server/Services/RepositoryContext.cs
index b12fc46..8917267 100644
--- a/Flawless.Server/Models/RepositoryContext.cs
+++ b/Flawless.Server/Services/RepositoryContext.cs
@@ -1,15 +1,14 @@
using Microsoft.EntityFrameworkCore;
-namespace Flawless.Server.Models;
+namespace Flawless.Server.Services;
public class RepositoryContext : DbContext
{
private readonly string RepositoryPath;
- public RepositoryContext(IConfiguration config, DbContextOptions options) : base(options)
+ public RepositoryContext(IConfiguration config, DbContextOptions options) : base(options)
{
- var settings = config.GetSection("Settings").Get();
- RepositoryPath = settings?.DataStoragePath ?? "./Data";
+ RepositoryPath = Path.Combine(config["LocalStoragePath"] ?? "./Data", "Repository");
if (!Directory.Exists(RepositoryPath))
Directory.CreateDirectory(RepositoryPath);
diff --git a/Flawless.Server/Models/RepositoryContextFactory.cs b/Flawless.Server/Services/RepositoryContextFactory.cs
similarity index 91%
rename from Flawless.Server/Models/RepositoryContextFactory.cs
rename to Flawless.Server/Services/RepositoryContextFactory.cs
index 208876a..54e0edf 100644
--- a/Flawless.Server/Models/RepositoryContextFactory.cs
+++ b/Flawless.Server/Services/RepositoryContextFactory.cs
@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
-namespace Flawless.Server.Models;
+namespace Flawless.Server.Services;
public class RepositoryContextFactory : DbContextFactory
{
diff --git a/Flawless.Server/Services/TokenGenerationService.cs b/Flawless.Server/Services/TokenGenerationService.cs
new file mode 100644
index 0000000..c325dd2
--- /dev/null
+++ b/Flawless.Server/Services/TokenGenerationService.cs
@@ -0,0 +1,93 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Flawless.Server.Services;
+
+public class TokenGenerationService
+{
+ private readonly SymmetricSecurityKey _key;
+
+ private readonly string _issuer;
+
+ private readonly double _expiresIn;
+
+ private readonly double _refreshTokenLifeTime;
+
+ public TokenGenerationService(IConfiguration c)
+ {
+ var rawKey = Encoding.UTF8.GetBytes(c["Jwt:SecretKey"] ?? throw new Exception("No Jwt:SecretKey"));
+ _key = new SymmetricSecurityKey(rawKey);
+ _issuer = c["Jwt:Issuer"] ?? throw new Exception("No Jwt:Issuer");
+ _expiresIn = double.Parse(c["Jwt:ExpiresIn"] ?? throw new Exception("No Jwt:ExpiresIn"));
+ _refreshTokenLifeTime = double.Parse(c["Jwt:RefreshTokenLifeTime"] ?? throw new Exception("No Jwt:RefreshTokenLifeTime"));
+ }
+
+ public double RefreshTokenLifeTime => _refreshTokenLifeTime;
+
+ public string GenerateToken(IEnumerable claims)
+ {
+ var now = DateTime.UtcNow;
+ var jwt = new JwtSecurityToken(
+ issuer: _issuer,
+ claims: claims,
+ notBefore: now,
+ expires: now.AddMinutes(_expiresIn),
+ signingCredentials: new SigningCredentials(_key, SecurityAlgorithms.HmacSha256)
+ );
+
+ return new JwtSecurityTokenHandler().WriteToken(jwt);
+ }
+
+ public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
+ {
+ var tokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateAudience = false,
+ ValidateIssuer = true,
+ ValidateLifetime = false,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = _issuer,
+ IssuerSigningKey = _key,
+ };
+
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
+ var jwtSecurityToken = securityToken as JwtSecurityToken;
+ if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
+ throw new SecurityTokenException("Invalid token");
+
+ return principal;
+ }
+
+ public ClaimsPrincipal GetPrincipal(string token)
+ {
+ var tokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateAudience = false,
+ ValidateIssuer = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = _issuer,
+ IssuerSigningKey = _key,
+ };
+
+ var tokenHandler = new JwtSecurityTokenHandler();
+ var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
+ var jwtSecurityToken = securityToken as JwtSecurityToken;
+ if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
+ throw new SecurityTokenException("Invalid token");
+
+ return principal;
+ }
+
+ public string GenerateRefreshToken()
+ {
+ Span randomNumber = stackalloc byte[32];
+ using var rng = RandomNumberGenerator.Create();
+ rng.GetBytes(randomNumber);
+ return Convert.ToBase64String(randomNumber);
+ }
+}
\ No newline at end of file
diff --git a/Flawless.Server/Settings.cs b/Flawless.Server/Settings.cs
deleted file mode 100644
index e0f02e3..0000000
--- a/Flawless.Server/Settings.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Flawless.Server;
-
-public sealed class Settings
-{
- // Local settings
- public required string DataStoragePath { get; set; } = "./Data";
-
-
-}
\ No newline at end of file
diff --git a/Flawless.Server/appsettings.Development.json b/Flawless.Server/appsettings.Development.json
index 0c208ae..d4ee3dc 100644
--- a/Flawless.Server/appsettings.Development.json
+++ b/Flawless.Server/appsettings.Development.json
@@ -4,5 +4,19 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "CoreDb": "Server=localhost;Port=5432;User Id=postgres;Password=postgres;Database=flawless"
+ },
+ "LocalStoragePath": "./data/development",
+ "User": {
+ "PublicRegister": false
+ },
+ "Jwt": {
+ "SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",
+ "Issuer": "test",
+ "ExpiresIn": 30,
+ "RefreshTokenLifeTime": 7
}
}
diff --git a/Flawless.Server/appsettings.json b/Flawless.Server/appsettings.json
index add7934..96b602d 100644
--- a/Flawless.Server/appsettings.json
+++ b/Flawless.Server/appsettings.json
@@ -5,8 +5,17 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*",
- "Settings": {
- "DataStoragePath": "/Users/hcm-b0485/Desktop/test"
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "CoreDb": "Server=localhost;Port=5432;User Id=postgres;Password=postgres;Database=flawless"
+ },
+ "LocalStoragePath": "./data/production",
+ "User": {
+ "PublicRegister": false
+ },
+ "Jwt": {
+ "SecretKey": "your_256bit_security_key_at_here_otherwise_not_bootable",
+ "Issuer": "test",
+ "ExpiresIn": 30
}
}