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:
2026-01-20 15:21:58 +05:00
parent 641e1fc14d
commit 4655b4111f
5 changed files with 259 additions and 319 deletions

View File

@@ -2,16 +2,17 @@ namespace DexDemoBackend;
public class AppConfig public class AppConfig
{ {
public string DbHost { get; init; } = default!; public string DbHost { get; set; } = "postgres";
public string DbPort { get; init; } = default!; public string DbPort { get; set; } = "5440";
public string DbName { get; init; } = default!; public string DbName { get; set; } = "dexdemo";
public string DbUser { get; init; } = default!; public string DbUser { get; set; } = "dexdemo";
public string DbPassword { get; init; } = default!; public string DbPassword { get; set; } = "dexdemo";
public string Issuer { get; init; } = default!; public string Issuer { get; set; } = "https://dex.127.0.0.1.sslip.io/";
public bool InsecureDevMode { get; init; } public bool InsecureDevMode { get; set; }
public string? InsecureDevEmail { get; init; } public string? InsecureDevEmail { get; set; }
public string[] AllowedOrigins { get; set; } = ["http://localhost:3000", "https://localhost:3000"];
public string ConnectionString =>
public string ConnectionString =>
$"Host={DbHost};Port={DbPort};Database={DbName};Username={DbUser};Password={DbPassword}"; $"Host={DbHost};Port={DbPort};Database={DbName};Username={DbUser};Password={DbPassword}";
} }

View File

@@ -3,383 +3,291 @@ using DexDemoBackend;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Npgsql; using Npgsql;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Logging configuration is now handled in appsettings.json // Configure AppConfig using IConfiguration
// Only add console logging if not already configured builder.Services.Configure<AppConfig>(builder.Configuration.GetSection("AppConfig"));
if (!builder.Logging.Services.Any(s => s.ServiceType == typeof(ILoggerProvider))) builder.Services.AddSingleton(sp => sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<AppConfig>>().Value);
{
builder.Logging.AddConsole();
}
// 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 ValidateConfiguration(config);
// 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 ConfigureJwtAuthentication(builder.Services, config);
// 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
}
);
});
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
// Configure JSON serialization to use snake_case
builder.Services.ConfigureHttpJsonOptions(options => builder.Services.ConfigureHttpJsonOptions(options =>
{ {
options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower; options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
}); });
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build(); var app = builder.Build();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
// Create logger after app is built // Get current config from DI (supports reload)
var logger = app.Services.GetRequiredService<ILogger<Program>>(); var currentConfig = app.Services.GetRequiredService<AppConfig>();
// Log configuration (without sensitive data) app.UseCors(policy =>
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"); policy.WithOrigins(currentConfig.AllowedOrigins)
} .AllowAnyMethod()
logger.LogInformation("[JWT Config] OpenID Connect discovery URL: {DiscoveryUrl}", $"{config.Issuer}.well-known/openid-configuration"); .AllowAnyHeader()
logger.LogInformation("[JWT Config] RequireHttps: {RequireHttps}", !config.InsecureDevMode); .AllowCredentials();
});
app.UseCors();
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
logger.LogInformation("[App] Application middleware configured:"); // Global error handling middleware
logger.LogInformation("[App] CORS: Enabled"); app.Use(async (context, next) =>
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) =>
{ {
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 try
{ {
var email = await GetUserEmail(context, cfg, userInfoLogger); await next();
userInfoLogger.LogInformation("[UserInfo] Retrieved email: {Email}", email); }
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 using var conn = new NpgsqlConnection(cfg.ConnectionString);
await conn.OpenAsync(); await conn.OpenAsync();
userInfoLogger.LogDebug("[UserInfo] Database connection opened successfully");
// Get user info var userData = await conn.QueryAsync<UserDataResult>(@"
userInfoLogger.LogDebug("[UserInfo] Querying user info for email: {Email}", email); SELECT
var userResult = await conn.QuerySingleOrDefaultAsync<UserQueryResult>(@" u.email, u.full_name,
SELECT u.email, u.full_name, u.organization_id, o.name as org_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 FROM users u
LEFT JOIN organizations o ON u.organization_id = o.id 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 }); 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" }); return Results.NotFound(new { detail = "User not found in database" });
} }
userInfoLogger.LogInformation("[UserInfo] User found: {Email}, FullName: {FullName}, OrgId: {OrgId}", var firstRecord = userData.First();
userResult.Email, userResult.FullName, userResult.OrganizationId); var organization = firstRecord.OrgId.HasValue && firstRecord.OrgName != null
? new Organization(firstRecord.OrgId.Value, firstRecord.OrgName)
var organization = userResult.OrganizationId.HasValue && userResult.OrgName != null
? new Organization(userResult.OrganizationId.Value, userResult.OrgName)
: null; : null;
// Get user roles var roles = userData
userInfoLogger.LogDebug("[UserInfo] Querying user roles for email: {Email}", email); .Where(x => x.RoleId.HasValue)
var roles = (await conn.QueryAsync<Role>(@" .GroupBy(x => x.RoleId)
SELECT r.id, r.name, r.description .Select(g => g.First())
FROM roles r .Select(x => new Role(x.RoleId!.Value, x.RoleName!, x.RoleDescription))
JOIN user_roles ur ON r.id = ur.role_id .ToList();
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); 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 return Results.Ok(new UserInfo(firstRecord.Email, firstRecord.FullName, organization, roles, 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) catch (NpgsqlException ex) when (ex.IsTransient)
{ {
userInfoLogger.LogError(ex, "[UserInfo] Error occurred: {Message}", ex.Message); return Results.Json(new { error = "Database temporarily unavailable" }, statusCode: 503);
throw; }
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(); 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); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
logger.LogDebug("[GetUserEmail] Request method: {Method}", context.Request.Method); .AddJwtBearer(options =>
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); options.Authority = config.Issuer;
} options.RequireHttpsMetadata = false;
} options.TokenValidationParameters = new TokenValidationParameters
{
// Log all headers ValidateIssuer = true,
logger.LogDebug("[GetUserEmail] Request headers:"); ValidIssuer = config.Issuer,
foreach (var header in context.Request.Headers) ValidateAudience = false,
{ ValidateLifetime = true,
if (header.Key.ToLower().Contains("auth") || header.Key.ToLower().Contains("email")) ValidateIssuerSigningKey = true,
{ ClockSkew = OidcConfigConstants.ClockSkew,
logger.LogDebug("[GetUserEmail] {HeaderKey} = {HeaderValue}", header.Key, header.Value); 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) if (config.InsecureDevMode)
{ {
logger.LogInformation("[GetUserEmail] INSECURE_DEV_MODE: Using email {Email}", config.InsecureDevEmail);
return Task.FromResult(config.InsecureDevEmail!); return Task.FromResult(config.InsecureDevEmail!);
} }
// Try JWT token from authenticated user
if (context.User.Identity?.IsAuthenticated == true) if (context.User.Identity?.IsAuthenticated == true)
{ {
logger.LogDebug("[GetUserEmail] Checking JWT token claims for email"); var emailClaim = context.User.FindFirst(OidcConfigConstants.EmailClaimType);
var emailClaim = context.User.FindFirst("email");
if (emailClaim != null) if (emailClaim != null)
{ {
logger.LogInformation("[GetUserEmail] Found email in JWT claim: {Email}", emailClaim.Value);
return Task.FromResult(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 if (context.Request.Headers.TryGetValue(OidcConfigConstants.AuthRequestEmailHeader, out var emailHeader))
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()); 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"); throw new UnauthorizedAccessException("No authentication information found");
} }
// Internal query result model record UserDataResult(
record UserQueryResult(string Email, string FullName, int? OrganizationId, string? OrgName); 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);
}

View File

@@ -14,6 +14,17 @@
"Microsoft.AspNetCore": "Information" "Microsoft.AspNetCore": "Information"
} }
}, },
"Urls": "http://localhost:8000" "Urls": "http://localhost:8000",
"AppConfig": {
"DbHost": "localhost",
"DbPort": "5432",
"DbName": "dexdemo",
"DbUser": "dexdemo",
"DbPassword": "dexdemo",
"Issuer": "https://dex.127.0.0.1.sslip.io/",
"InsecureDevMode": true,
"InsecureDevEmail": "test@example.com",
"AllowedOrigins": ["http://localhost:3000", "https://localhost:3000", "http://localhost:5173", "https://localhost:5173"]
}
} }

View File

@@ -14,7 +14,17 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"AppConfig": {
"DbHost": "postgres",
"DbPort": "5432",
"DbName": "dexdemo",
"DbUser": "dexdemo",
"DbPassword": "dexdemo",
"Issuer": "https://dex.127.0.0.1.sslip.io/",
"InsecureDevMode": false,
"AllowedOrigins": ["https://yourdomain.com"]
}
} }

View File

@@ -13,6 +13,16 @@
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"AppConfig": {
"DbHost": "postgres",
"DbPort": "5440",
"DbName": "dexdemo",
"DbUser": "dexdemo",
"DbPassword": "dexdemo",
"Issuer": "https://dex.127.0.0.1.sslip.io/",
"InsecureDevMode": false,
"AllowedOrigins": ["http://localhost:3000", "https://localhost:3000"]
}
} }