Refactor AppConfig and update configuration settings across environments. Changed properties to use set accessors, added AllowedOrigins, and improved error handling in Program.cs. Updated appsettings files for development, production, and added new structure for user data retrieval.
This commit is contained in:
@@ -3,383 +3,291 @@ 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();
|
||||
}
|
||||
// Configure AppConfig using IConfiguration
|
||||
builder.Services.Configure<AppConfig>(builder.Configuration.GetSection("AppConfig"));
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<AppConfig>>().Value);
|
||||
|
||||
// Logger will be created after app is built
|
||||
var config = builder.Configuration.GetSection("AppConfig").Get<AppConfig>() ?? new AppConfig();
|
||||
|
||||
// // 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")}");
|
||||
ValidateConfiguration(config);
|
||||
|
||||
// // 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<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
|
||||
}
|
||||
);
|
||||
});
|
||||
ConfigureJwtAuthentication(builder.Services, config);
|
||||
|
||||
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<ILogger<Program>>();
|
||||
// Get current config from DI (supports reload)
|
||||
var currentConfig = app.Services.GetRequiredService<AppConfig>();
|
||||
|
||||
// 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)
|
||||
app.UseCors(policy =>
|
||||
{
|
||||
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();
|
||||
policy.WithOrigins(currentConfig.AllowedOrigins)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
});
|
||||
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<Program> logger) =>
|
||||
// Global error handling middleware
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
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<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 next();
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
await context.Response.WriteAsJsonAsync(new { error = "Unauthorized" });
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/api/health", () => Results.Ok(new HealthResponse("ok")));
|
||||
|
||||
app.MapGet("/api/user-identity", [Authorize] (HttpContext context) =>
|
||||
{
|
||||
return Results.Ok(new UserIdentityResponse(
|
||||
context.User.Identity?.Name,
|
||||
context.User.Identity?.IsAuthenticated ?? false,
|
||||
context.User.Identity?.AuthenticationType,
|
||||
context.User.Claims.Select(c => new ClaimResponse(c.Type, c.Value)).ToList()
|
||||
));
|
||||
});
|
||||
|
||||
app.MapGet("/api/user-info", [Authorize] async (HttpContext context, [FromServices] AppConfig cfg) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var email = await GetUserEmail(context, cfg);
|
||||
|
||||
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
|
||||
var userData = await conn.QueryAsync<UserDataResult>(@"
|
||||
SELECT
|
||||
u.email, u.full_name,
|
||||
o.id as org_id, o.name as org_name,
|
||||
r.id as role_id, r.name as role_name, r.description as role_description,
|
||||
l.id as link_id, l.title as link_title, l.url as link_url, l.description as link_description
|
||||
FROM users u
|
||||
LEFT JOIN organizations o ON u.organization_id = o.id
|
||||
WHERE u.email = @email",
|
||||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
||||
LEFT JOIN roles r ON ur.role_id = r.id
|
||||
LEFT JOIN role_links rl ON r.id = rl.role_id
|
||||
LEFT JOIN links l ON rl.link_id = l.id
|
||||
WHERE u.email = @email
|
||||
ORDER BY l.id",
|
||||
new { email });
|
||||
|
||||
if (userResult is null)
|
||||
|
||||
if (!userData.Any())
|
||||
{
|
||||
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)
|
||||
var firstRecord = userData.First();
|
||||
var organization = firstRecord.OrgId.HasValue && firstRecord.OrgName != null
|
||||
? new Organization(firstRecord.OrgId.Value, firstRecord.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();
|
||||
var roles = userData
|
||||
.Where(x => x.RoleId.HasValue)
|
||||
.GroupBy(x => x.RoleId)
|
||||
.Select(g => g.First())
|
||||
.Select(x => new Role(x.RoleId!.Value, x.RoleName!, x.RoleDescription))
|
||||
.ToList();
|
||||
|
||||
userInfoLogger.LogInformation("[UserInfo] Found {RoleCount} roles for user", roles.Count);
|
||||
var links = userData
|
||||
.Where(x => x.LinkId.HasValue)
|
||||
.GroupBy(x => x.LinkId)
|
||||
.Select(g => g.First())
|
||||
.Select(x => new Link(x.LinkId!.Value, x.LinkTitle!, x.LinkUrl!, x.LinkDescription))
|
||||
.ToList();
|
||||
|
||||
// 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));
|
||||
return Results.Ok(new UserInfo(firstRecord.Email, firstRecord.FullName, organization, roles, links));
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (NpgsqlException ex) when (ex.IsTransient)
|
||||
{
|
||||
userInfoLogger.LogError(ex, "[UserInfo] Error occurred: {Message}", ex.Message);
|
||||
throw;
|
||||
return Results.Json(new { error = "Database temporarily unavailable" }, statusCode: 503);
|
||||
}
|
||||
catch (NpgsqlException)
|
||||
{
|
||||
return Results.Json(new { error = "Database error" }, statusCode: 500);
|
||||
}
|
||||
});
|
||||
|
||||
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<string> GetUserEmail(HttpContext context, AppConfig config, ILogger logger)
|
||||
static void ConfigureJwtAuthentication(IServiceCollection services, AppConfig config)
|
||||
{
|
||||
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)
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
options.Authority = config.Issuer;
|
||||
options.RequireHttpsMetadata = false;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = config.Issuer,
|
||||
ValidateAudience = false,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ClockSkew = OidcConfigConstants.ClockSkew,
|
||||
NameClaimType = OidcConfigConstants.NameClaimType,
|
||||
RoleClaimType = OidcConfigConstants.RoleClaimType
|
||||
};
|
||||
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
var nameClaim = context.Principal?.FindFirst(OidcConfigConstants.NameClaimType);
|
||||
var emailClaim = context.Principal?.FindFirst(OidcConfigConstants.EmailClaimType);
|
||||
|
||||
// Dev mode
|
||||
if (nameClaim == null && emailClaim != null)
|
||||
{
|
||||
var identity = context.Principal?.Identity as ClaimsIdentity;
|
||||
identity?.AddClaim(new Claim(OidcConfigConstants.NameClaimType, emailClaim.Value));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
|
||||
if (config.InsecureDevMode)
|
||||
{
|
||||
httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
|
||||
}
|
||||
|
||||
var discoveryUrl = $"{config.Issuer}.well-known/openid-configuration";
|
||||
|
||||
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
|
||||
discoveryUrl,
|
||||
new OpenIdConnectConfigurationRetriever(),
|
||||
new HttpDocumentRetriever(new HttpClient(httpClientHandler))
|
||||
{
|
||||
RequireHttps = !config.InsecureDevMode
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static void ValidateConfiguration(AppConfig config)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.DbHost))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbHost' is required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.DbPort))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbPort' is required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.DbName))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbName' is required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.DbUser))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbUser' is required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.DbPassword))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:DbPassword' is required");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.Issuer))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:Issuer' is required");
|
||||
|
||||
if (!Uri.TryCreate(config.Issuer, UriKind.Absolute, out _))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:Issuer' must be a valid URL");
|
||||
|
||||
if (config.InsecureDevMode && string.IsNullOrWhiteSpace(config.InsecureDevEmail))
|
||||
throw new InvalidOperationException("Configuration parameter 'AppConfig:InsecureDevEmail' is required when 'AppConfig:InsecureDevMode' is enabled");
|
||||
}
|
||||
|
||||
static Task<string> GetUserEmail(HttpContext context, AppConfig config)
|
||||
{
|
||||
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");
|
||||
var emailClaim = context.User.FindFirst(OidcConfigConstants.EmailClaimType);
|
||||
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))
|
||||
if (context.Request.Headers.TryGetValue(OidcConfigConstants.AuthRequestEmailHeader, 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);
|
||||
record UserDataResult(
|
||||
string Email,
|
||||
string FullName,
|
||||
int? OrgId,
|
||||
string? OrgName,
|
||||
int? RoleId,
|
||||
string? RoleName,
|
||||
string? RoleDescription,
|
||||
int? LinkId,
|
||||
string? LinkTitle,
|
||||
string? LinkUrl,
|
||||
string? LinkDescription
|
||||
);
|
||||
|
||||
// API Response Models
|
||||
record HealthResponse(string Status);
|
||||
|
||||
record ClaimResponse(string Type, string Value);
|
||||
|
||||
record UserIdentityResponse(
|
||||
string? Name,
|
||||
bool IsAuthenticated,
|
||||
string? AuthenticationType,
|
||||
List<ClaimResponse> Claims
|
||||
);
|
||||
|
||||
static class OidcConfigConstants
|
||||
{
|
||||
// Default environment variable values
|
||||
public const string DefaultDbHost = "postgres";
|
||||
public const string DefaultDbPort = "5440";
|
||||
public const string DefaultDbName = "dexdemo";
|
||||
public const string DefaultDbUser = "dexdemo";
|
||||
public const string DefaultDbPassword = "dexdemo";
|
||||
public const string DefaultDexIssuer = "https://dex.127.0.0.1.sslip.io/";
|
||||
|
||||
// JWT settings
|
||||
public const string NameClaimType = "name";
|
||||
public const string RoleClaimType = "role";
|
||||
public const string EmailClaimType = "email";
|
||||
public const string AuthRequestEmailHeader = "X-Auth-Request-Email";
|
||||
|
||||
// Time settings
|
||||
public static readonly TimeSpan ClockSkew = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user