fix
This commit is contained in:
@@ -1,10 +1,27 @@
|
||||
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")}");
|
||||
@@ -23,13 +40,125 @@ var config = new AppConfig
|
||||
DbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "dexdemo",
|
||||
DbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "dexdemo",
|
||||
DbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "dexdemo",
|
||||
DexIssuer = Environment.GetEnvironmentVariable("DEX_ISSUER") ?? "https://dex.127.0.0.1.sslip.io/",
|
||||
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);
|
||||
builder.Services.AddSingleton<JwtValidator>();
|
||||
|
||||
// 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<ILogger<JwtBearerHandler>>();
|
||||
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<ILogger<JwtBearerHandler>>();
|
||||
|
||||
// Логируем все доступные 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<ILogger<JwtBearerHandler>>();
|
||||
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<ILogger<JwtBearerHandler>>();
|
||||
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<OpenIdConnectConfiguration>(
|
||||
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 =>
|
||||
@@ -50,92 +179,205 @@ builder.Services.AddCors(options =>
|
||||
var app = builder.Build();
|
||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
|
||||
// Create logger after app is built
|
||||
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
// 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 info endpoint
|
||||
app.MapGet("/api/user-info", async (HttpContext context, [FromServices] AppConfig cfg, [FromServices] JwtValidator jwtValidator) =>
|
||||
// User identity demo endpoint
|
||||
app.MapGet("/api/user-identity", [Authorize] (HttpContext context, [FromServices] ILogger<Program> logger) =>
|
||||
{
|
||||
var email = await GetUserEmail(context, cfg, jwtValidator);
|
||||
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);
|
||||
|
||||
await using var conn = new NpgsqlConnection(cfg.ConnectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
// Get user info
|
||||
var userResult = await conn.QuerySingleOrDefaultAsync<UserQueryResult>(@"
|
||||
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)
|
||||
{
|
||||
return Results.NotFound(new { detail = "User not found in database" });
|
||||
}
|
||||
|
||||
var organization = userResult.OrganizationId.HasValue && userResult.OrgName != null
|
||||
? new Organization(userResult.OrganizationId.Value, userResult.OrgName)
|
||||
: null;
|
||||
|
||||
// Get user roles
|
||||
var roles = (await conn.QueryAsync<Role>(@"
|
||||
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();
|
||||
|
||||
// Get available links
|
||||
var links = (await conn.QueryAsync<Link>(@"
|
||||
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();
|
||||
|
||||
return Results.Ok(new UserInfo(userResult.Email, userResult.FullName, organization, roles, links));
|
||||
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<Program> 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<UserQueryResult>(@"
|
||||
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<Role>(@"
|
||||
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<Link>(@"
|
||||
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 async Task<string> GetUserEmail(HttpContext context, AppConfig config, JwtValidator jwtValidator)
|
||||
static Task<string> GetUserEmail(HttpContext context, AppConfig config, ILogger logger)
|
||||
{
|
||||
// Dev mode
|
||||
if (config.InsecureDevMode)
|
||||
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)
|
||||
{
|
||||
Console.WriteLine($"INSECURE_DEV_MODE: Using email {config.InsecureDevEmail}");
|
||||
return config.InsecureDevEmail!;
|
||||
}
|
||||
|
||||
// Try Authorization header
|
||||
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
|
||||
{
|
||||
var headerValue = authHeader.ToString();
|
||||
if (headerValue.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
logger.LogDebug("[GetUserEmail] All user claims:");
|
||||
foreach (var claim in context.User.Claims)
|
||||
{
|
||||
var token = headerValue[7..];
|
||||
var payload = await jwtValidator.ValidateToken(token);
|
||||
|
||||
if (payload.TryGetValue("email", out var emailClaim))
|
||||
{
|
||||
return emailClaim.ToString()!;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Try OAuth2 proxy header
|
||||
if (context.Request.Headers.TryGetValue("X-Auth-Request-Email", out var emailHeader))
|
||||
// Dev mode
|
||||
if (config.InsecureDevMode)
|
||||
{
|
||||
return emailHeader.ToString();
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user