Files
Finlytic/FinlyticBackend/Services/JwtTokenService.cs
T

99 lines
4.1 KiB
C#

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using FinlyticBackend.Entities;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
namespace FinlyticBackend.Services;
/// <summary>
/// Service for generating JWT tokens for user authentication.
/// </summary>
public interface IJwtTokenService
{
/// <summary>
/// Generates a JWT token for the specified user.
/// </summary>
/// <param name="user">The user entity.</param>
/// <returns>A tuple containing the generated token string and its expiration date.</returns>
(string token, DateTime expiresAt) GenerateToken(UserEntity user);
}
/// <inheritdoc />
public class JwtTokenService : IJwtTokenService
{
private readonly string _secretKey;
private readonly string _issuer;
private readonly string _audience;
private readonly int _expiryDays;
/// <summary>
/// Initializes a new instance of the <see cref="JwtTokenService"/> class.
/// </summary>
/// <param name="configuration">The application configuration.</param>
public JwtTokenService(IConfiguration configuration)
{
var configuredKey = configuration["JWT:SecretKey"] ?? configuration["JWT__SecretKey"];
// Guard: Mindestlänge für HMAC-SHA256 erzwingen (mindestens 32 Zeichen / 256 Bits).
// Fail-fast statt Fallback auf einen im Repo öffentlichen Literal (Rules.md §12): ein Gateway,
// das mit einem repo-öffentlichen Signing-Key läuft, ist schlimmer als eines, das nicht startet.
if (string.IsNullOrWhiteSpace(configuredKey) || configuredKey.Length < 32)
{
throw new InvalidOperationException(
"JWT:SecretKey (bzw. JWT__SecretKey) ist nicht konfiguriert oder kürzer als 32 Zeichen (256 Bit). " +
"JwtTokenService kann ohne einen ausreichend starken, explizit konfigurierten Signing-Key nicht initialisiert werden.");
}
_secretKey = configuredKey;
_issuer = configuration["JWT:Issuer"] ?? configuration["JWT__Issuer"] ?? "FinlyticBackend";
_audience = configuration["JWT:Audience"] ?? configuration["JWT__Audience"] ?? "FinlyticClients";
_expiryDays = int.TryParse(configuration["JWT:ExpiryDays"] ?? configuration["JWT__ExpiryDays"], out var days) ? days : 7;
}
/// <inheritdoc />
public (string token, DateTime expiresAt) GenerateToken(UserEntity user)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_secretKey);
var expiresAt = DateTime.UtcNow.AddDays(_expiryDays);
string userIdStr = user.Id.ToString();
string emailStr = user.Email ?? string.Empty;
string nameStr = user.FullName ?? string.Empty;
string roleStr = string.IsNullOrWhiteSpace(user.Role) ? "User" : user.Role;
// Bündelung von ASP.NET Core ClaimTypes UND OAuth2/OpenID Standard-Claims (sub, email, role, name)
var claims = new List<Claim>
{
// Standard ASP.NET Core Claims
new(ClaimTypes.NameIdentifier, userIdStr),
new(ClaimTypes.Email, emailStr),
new(ClaimTypes.Name, nameStr),
new(ClaimTypes.Role, roleStr),
// OpenID / OAuth2 Short Claims für Frontend/Mobile Clients (Flutter/Avalonia)
new(JwtRegisteredClaimNames.Sub, userIdStr),
new(JwtRegisteredClaimNames.Email, emailStr),
new(JwtRegisteredClaimNames.Name, nameStr),
new("role", roleStr),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N"))
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = expiresAt,
Issuer = _issuer,
Audience = _audience,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return (tokenHandler.WriteToken(token), expiresAt);
}
}