using Dapper; using DexDemoBackend; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; using Npgsql; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); // Logging configuration is now handled in appsettings.json // Only add console logging if not already configured if (!builder.Logging.Services.Any(s => s.ServiceType == typeof(ILoggerProvider))) { builder.Logging.AddConsole(); } // Logger will be created after app is built // // Force binding to all interfaces in Kubernetes // Console.WriteLine($"ASPNETCORE_URLS env var: {Environment.GetEnvironmentVariable("ASPNETCORE_URLS")}"); // Console.WriteLine($"ASPNETCORE_HTTP_PORTS env var: {Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS")}"); // // Clear any existing configuration and force our URL // builder.WebHost.ConfigureKestrel(options => // { // options.ListenAnyIP(8000); // }); // Configuration var config = new AppConfig { DbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "postgres", DbPort = Environment.GetEnvironmentVariable("DB_PORT") ?? "5440", DbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "dexdemo", DbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "dexdemo", DbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "dexdemo", Issuer = Environment.GetEnvironmentVariable("DEX_ISSUER") ?? "https://dex.127.0.0.1.sslip.io/", InsecureDevMode = Environment.GetEnvironmentVariable("INSECURE_DEV_MODE")?.ToLower() == "true", InsecureDevEmail = Environment.GetEnvironmentVariable("INSECURE_DEV_EMAIL") }; // Configuration will be logged after logger is created builder.Services.AddSingleton(config); // Configure JWT Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = config.Issuer; options.RequireHttpsMetadata = false; // For development options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = config.Issuer, ValidateAudience = false, // Можно включить, если DEX выдает audience ValidateLifetime = true, ValidateIssuerSigningKey = true, ClockSkew = TimeSpan.FromMinutes(5), // Допуск 5 минут на разницу времени // Настройка для заполнения User.Identity.Name NameClaimType = "name", // Используем claim "name" для User.Identity.Name RoleClaimType = "role" // Используем claim "role" для ролей }; // Add detailed event logging for JWT authentication options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { var jwtLogger = context.HttpContext.RequestServices.GetRequiredService>(); jwtLogger.LogError(context.Exception, "[JWT Auth Failed] Exception: {Message}", context.Exception?.Message); jwtLogger.LogDebug("[JWT Auth Failed] StackTrace: {StackTrace}", context.Exception?.StackTrace); return Task.CompletedTask; }, OnTokenValidated = context => { var jwtLogger = context.HttpContext.RequestServices.GetRequiredService>(); // Логируем все доступные claims для отладки var claims = string.Join(", ", context.Principal?.Claims?.Select(c => $"{c.Type}={c.Value}") ?? new string[0]); jwtLogger.LogDebug("[JWT Token Validated] All claims: {Claims}", claims); // Проверяем, есть ли name claim, если нет - используем email как fallback var nameClaim = context.Principal?.FindFirst("name"); var emailClaim = context.Principal?.FindFirst("email"); var subClaim = context.Principal?.FindFirst("sub"); if (nameClaim == null && emailClaim != null) { // Если нет name claim, но есть email - добавляем его как name claim var identity = context.Principal?.Identity as ClaimsIdentity; identity?.AddClaim(new Claim("name", emailClaim.Value)); jwtLogger.LogDebug("[JWT Token Validated] Added email as name claim: {Email}", emailClaim.Value); } jwtLogger.LogInformation("[JWT Token Validated] User.Identity.Name: {UserName}", context.Principal?.Identity?.Name); jwtLogger.LogInformation("[JWT Token Validated] Email claim: {Email}", emailClaim?.Value ?? "Not found"); jwtLogger.LogInformation("[JWT Token Validated] Sub claim: {Sub}", subClaim?.Value ?? "Not found"); return Task.CompletedTask; }, OnMessageReceived = context => { var jwtLogger = context.HttpContext.RequestServices.GetRequiredService>(); jwtLogger.LogDebug("[JWT Message Received] Scheme: {Scheme}, Path: {Path}, Method: {Method}", context.Scheme.Name, context.Request.Path, context.Request.Method); // Log Authorization header (without sensitive token data) if (context.Request.Headers.TryGetValue("Authorization", out var authHeader)) { var authValue = authHeader.ToString(); if (authValue.StartsWith("Bearer ")) { var tokenPreview = authValue.Substring(7, Math.Min(20, authValue.Length - 7)) + "..."; jwtLogger.LogDebug("[JWT Message Received] Token preview: {TokenPreview}", tokenPreview); } } return Task.CompletedTask; }, OnChallenge = context => { var jwtLogger = context.HttpContext.RequestServices.GetRequiredService>(); jwtLogger.LogWarning("[JWT Challenge] Error: {Error}, ErrorDescription: {ErrorDescription}, ErrorUri: {ErrorUri}", context.Error, context.ErrorDescription, context.ErrorUri); return Task.CompletedTask; } }; // Configure OpenID Connect discovery var httpClientHandler = new HttpClientHandler(); // В режиме разработки отключаем проверку SSL // В продакшене это должно быть настроено правильно! if (config.InsecureDevMode) { // Logging will be done after logger is created httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; } var discoveryUrl = $"{config.Issuer}.well-known/openid-configuration"; // Logging will be done after logger is created options.ConfigurationManager = new ConfigurationManager( discoveryUrl, new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever(new HttpClient(httpClientHandler)) { RequireHttps = !config.InsecureDevMode } ); }); builder.Services.AddAuthorization(); // Configure JSON serialization to use snake_case builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower; }); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; // Create logger after app is built var logger = app.Services.GetRequiredService>(); // Log configuration (without sensitive data) logger.LogInformation("[Config] Application configuration:"); logger.LogInformation("[Config] DB_HOST: {DbHost}", config.DbHost); logger.LogInformation("[Config] DB_PORT: {DbPort}", config.DbPort); logger.LogInformation("[Config] DB_NAME: {DbName}", config.DbName); logger.LogInformation("[Config] DB_USER: {DbUser}", config.DbUser); logger.LogInformation("[Config] DB_PASSWORD: [HIDDEN]"); logger.LogInformation("[Config] DEX_ISSUER: {Issuer}", config.Issuer); logger.LogInformation("[Config] INSECURE_DEV_MODE: {InsecureDevMode}", config.InsecureDevMode); logger.LogInformation("[Config] INSECURE_DEV_EMAIL: {InsecureDevEmail}", config.InsecureDevEmail ?? "[NOT SET]"); // Log JWT configuration if (config.InsecureDevMode) { logger.LogWarning("[JWT Config] InsecureDevMode enabled - disabling SSL certificate validation"); } logger.LogInformation("[JWT Config] OpenID Connect discovery URL: {DiscoveryUrl}", $"{config.Issuer}.well-known/openid-configuration"); logger.LogInformation("[JWT Config] RequireHttps: {RequireHttps}", !config.InsecureDevMode); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); logger.LogInformation("[App] Application middleware configured:"); logger.LogInformation("[App] CORS: Enabled"); logger.LogInformation("[App] Authentication: JWT Bearer"); logger.LogInformation("[App] Authorization: Enabled"); // Health check endpoint app.MapGet("/api/health", () => Results.Ok(new { status = "ok" })); // User identity demo endpoint app.MapGet("/api/user-identity", [Authorize] (HttpContext context, [FromServices] ILogger logger) => { logger.LogInformation("[UserIdentity] User.Identity.Name: {Name}", context.User.Identity?.Name ?? "NULL"); logger.LogInformation("[UserIdentity] User.Identity.IsAuthenticated: {IsAuthenticated}", context.User.Identity?.IsAuthenticated); logger.LogInformation("[UserIdentity] User.Identity.AuthenticationType: {AuthType}", context.User.Identity?.AuthenticationType); return Results.Ok(new { name = context.User.Identity?.Name, isAuthenticated = context.User.Identity?.IsAuthenticated, authenticationType = context.User.Identity?.AuthenticationType, claims = context.User.Claims.Select(c => new { type = c.Type, value = c.Value }).ToList() }); }); // User info endpoint app.MapGet("/api/user-info", [Authorize] async (HttpContext context, [FromServices] AppConfig cfg, [FromServices] ILogger userInfoLogger) => { userInfoLogger.LogInformation("[UserInfo] Starting user info request for path: {Path}", context.Request.Path); userInfoLogger.LogInformation("[UserInfo] User authenticated: {IsAuthenticated}", context.User.Identity?.IsAuthenticated); try { var email = await GetUserEmail(context, cfg, userInfoLogger); userInfoLogger.LogInformation("[UserInfo] Retrieved email: {Email}", email); await using var conn = new NpgsqlConnection(cfg.ConnectionString); await conn.OpenAsync(); userInfoLogger.LogDebug("[UserInfo] Database connection opened successfully"); // Get user info userInfoLogger.LogDebug("[UserInfo] Querying user info for email: {Email}", email); var userResult = await conn.QuerySingleOrDefaultAsync(@" SELECT u.email, u.full_name, u.organization_id, o.name as org_name FROM users u LEFT JOIN organizations o ON u.organization_id = o.id WHERE u.email = @email", new { email }); if (userResult is null) { userInfoLogger.LogWarning("[UserInfo] User not found in database for email: {Email}", email); return Results.NotFound(new { detail = "User not found in database" }); } userInfoLogger.LogInformation("[UserInfo] User found: {Email}, FullName: {FullName}, OrgId: {OrgId}", userResult.Email, userResult.FullName, userResult.OrganizationId); var organization = userResult.OrganizationId.HasValue && userResult.OrgName != null ? new Organization(userResult.OrganizationId.Value, userResult.OrgName) : null; // Get user roles userInfoLogger.LogDebug("[UserInfo] Querying user roles for email: {Email}", email); var roles = (await conn.QueryAsync(@" SELECT r.id, r.name, r.description FROM roles r JOIN user_roles ur ON r.id = ur.role_id JOIN users u ON ur.user_id = u.id WHERE u.email = @email", new { email })).ToList(); userInfoLogger.LogInformation("[UserInfo] Found {RoleCount} roles for user", roles.Count); // Get available links userInfoLogger.LogDebug("[UserInfo] Querying available links for email: {Email}", email); var links = (await conn.QueryAsync(@" SELECT DISTINCT l.id, l.title, l.url, l.description FROM links l JOIN role_links rl ON l.id = rl.link_id JOIN user_roles ur ON rl.role_id = ur.role_id JOIN users u ON ur.user_id = u.id WHERE u.email = @email ORDER BY l.id", new { email })).ToList(); userInfoLogger.LogInformation("[UserInfo] Found {LinkCount} links for user", links.Count); userInfoLogger.LogInformation("[UserInfo] Returning user info successfully"); return Results.Ok(new UserInfo(userResult.Email, userResult.FullName, organization, roles, links)); } catch (Exception ex) { userInfoLogger.LogError(ex, "[UserInfo] Error occurred: {Message}", ex.Message); throw; } }); logger.LogInformation("[App] Application starting..."); logger.LogInformation("[App] Available endpoints:"); logger.LogInformation("[App] GET /api/health - Health check"); logger.LogInformation("[App] GET /api/user-identity - User identity information (requires authentication)"); logger.LogInformation("[App] GET /api/user-info - User information (requires authentication)"); app.Run(); static Task GetUserEmail(HttpContext context, AppConfig config, ILogger logger) { logger.LogDebug("[GetUserEmail] Starting authentication check for path: {Path}", context.Request.Path); logger.LogDebug("[GetUserEmail] Request method: {Method}", context.Request.Method); logger.LogDebug("[GetUserEmail] User.Identity.IsAuthenticated: {IsAuthenticated}", context.User.Identity?.IsAuthenticated); logger.LogInformation("[GetUserEmail] User.Identity.Name: {Name}", context.User.Identity?.Name ?? "NULL"); logger.LogDebug("[GetUserEmail] User.Identity.AuthenticationType: {AuthType}", context.User.Identity?.AuthenticationType); // Log all claims if (context.User.Identity?.IsAuthenticated == true) { logger.LogDebug("[GetUserEmail] All user claims:"); foreach (var claim in context.User.Claims) { logger.LogDebug("[GetUserEmail] {ClaimType} = {ClaimValue}", claim.Type, claim.Value); } } // Log all headers logger.LogDebug("[GetUserEmail] Request headers:"); foreach (var header in context.Request.Headers) { if (header.Key.ToLower().Contains("auth") || header.Key.ToLower().Contains("email")) { logger.LogDebug("[GetUserEmail] {HeaderKey} = {HeaderValue}", header.Key, header.Value); } } // Dev mode if (config.InsecureDevMode) { logger.LogInformation("[GetUserEmail] INSECURE_DEV_MODE: Using email {Email}", config.InsecureDevEmail); return Task.FromResult(config.InsecureDevEmail!); } // Try JWT token from authenticated user if (context.User.Identity?.IsAuthenticated == true) { logger.LogDebug("[GetUserEmail] Checking JWT token claims for email"); var emailClaim = context.User.FindFirst("email"); if (emailClaim != null) { logger.LogInformation("[GetUserEmail] Found email in JWT claim: {Email}", emailClaim.Value); return Task.FromResult(emailClaim.Value); } else { logger.LogWarning("[GetUserEmail] No email claim found in JWT token"); } } else { logger.LogDebug("[GetUserEmail] User is not authenticated via JWT"); } // Try OAuth2 proxy header logger.LogDebug("[GetUserEmail] Checking OAuth2 proxy headers"); if (context.Request.Headers.TryGetValue("X-Auth-Request-Email", out var emailHeader)) { logger.LogInformation("[GetUserEmail] Found email in OAuth2 proxy header: {Email}", emailHeader.ToString()); return Task.FromResult(emailHeader.ToString()); } else { logger.LogDebug("[GetUserEmail] No X-Auth-Request-Email header found"); } logger.LogError("[GetUserEmail] No authentication information found - throwing UnauthorizedAccessException"); throw new UnauthorizedAccessException("No authentication information found"); } // Internal query result model record UserQueryResult(string Email, string FullName, int? OrganizationId, string? OrgName);