using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace DramaLing.Api.Services; public interface IAuthService { Task GetUserIdFromTokenAsync(string? authorizationHeader); Task ValidateTokenAsync(string token); } public class AuthService : IAuthService { private readonly IConfiguration _configuration; private readonly ILogger _logger; public AuthService(IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; } public async Task GetUserIdFromTokenAsync(string? authorizationHeader) { try { if (string.IsNullOrEmpty(authorizationHeader)) return null; if (!authorizationHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) return null; var token = authorizationHeader["Bearer ".Length..].Trim(); var claimsPrincipal = await ValidateTokenAsync(token); if (claimsPrincipal == null) return null; var userIdString = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? claimsPrincipal.FindFirst("sub")?.Value; if (Guid.TryParse(userIdString, out var userId)) return userId; return null; } catch (Exception ex) { _logger.LogError(ex, "Error extracting user ID from token"); return null; } } public async Task ValidateTokenAsync(string token) { try { // 優先從環境變數讀取,再從配置讀取 var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_JWT_SECRET") ?? _configuration["Supabase:JwtSecret"]; if (string.IsNullOrEmpty(jwtSecret)) { _logger.LogError("Supabase JWT Secret not configured in environment variables or config"); return null; } var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.UTF8.GetBytes(jwtSecret); var validationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_URL") ?? _configuration["Supabase:Url"] ?? "https://localhost", ValidAudience = "authenticated", IssuerSigningKey = new SymmetricSecurityKey(key), ClockSkew = TimeSpan.FromMinutes(5) // 允許 5 分鐘時間偏差 }; var principal = tokenHandler.ValidateToken(token, validationParameters, out _); return principal; } catch (SecurityTokenException ex) { _logger.LogWarning("Invalid JWT token: {Message}", ex.Message); return null; } catch (Exception ex) { _logger.LogError(ex, "Error validating JWT token"); return null; } } }