Compare commits

...

2 Commits

7 changed files with 306 additions and 329 deletions

View File

@@ -2,16 +2,26 @@ 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"];
// JWT configuration
public string NameClaimType { get; set; } = "name";
public string RoleClaimType { get; set; } = "role";
public string EmailClaimType { get; set; } = "email";
public string AuthRequestEmailHeader { get; set; } = "X-Auth-Request-Email";
public TimeSpan ClockSkew { get; set; } = TimeSpan.FromMinutes(5);
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

@@ -0,0 +1,36 @@
using DexDemoBackend;
using Microsoft.AspNetCore.Authentication;
using System.Security.Claims;
using Microsoft.Extensions.Options;
using System.Text.Encodings.Web;
public class InsecureDevAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly AppConfig _config;
public InsecureDevAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
AppConfig config) : base(options, logger, encoder)
{
_config = config;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var email = _config.InsecureDevEmail ?? "dev@example.com";
var claims = new[]
{
new Claim(ClaimTypes.Name, email),
new Claim(_config.EmailClaimType, email)
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}

View File

@@ -1,152 +1,192 @@
using Dapper; using Dapper;
using DexDemoBackend; using DexDemoBackend;
using Microsoft.AspNetCore.Authentication;
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;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Logging configuration is now handled in appsettings.json builder.Services.Configure<AppConfig>(builder.Configuration.GetSection("AppConfig"));
// Only add console logging if not already configured builder.Services.AddSingleton(sp => sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<AppConfig>>().Value);
if (!builder.Logging.Services.Any(s => s.ServiceType == typeof(ILoggerProvider)))
var config = builder.Configuration.GetSection("AppConfig").Get<AppConfig>() ?? new AppConfig();
ValidateConfiguration(config);
ConfigureJwtAuthentication(builder.Services, config);
builder.Services.AddAuthorization();
builder.Services.ConfigureHttpJsonOptions(options =>
{ {
builder.Logging.AddConsole(); options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
});
var app = builder.Build();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
var currentConfig = app.Services.GetRequiredService<AppConfig>();
app.UseCors(policy =>
{
policy.WithOrigins(currentConfig.AllowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
try
{
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();
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
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 (!userData.Any())
{
return Results.NotFound(new { detail = "User not found in database" });
} }
// Logger will be created after app is built var firstRecord = userData.First();
var organization = firstRecord.OrgId.HasValue && firstRecord.OrgName != null
? new Organization(firstRecord.OrgId.Value, firstRecord.OrgName)
: null;
// // Force binding to all interfaces in Kubernetes var roles = userData
// Console.WriteLine($"ASPNETCORE_URLS env var: {Environment.GetEnvironmentVariable("ASPNETCORE_URLS")}"); .Where(x => x.RoleId.HasValue)
// Console.WriteLine($"ASPNETCORE_HTTP_PORTS env var: {Environment.GetEnvironmentVariable("ASPNETCORE_HTTP_PORTS")}"); .GroupBy(x => x.RoleId)
.Select(g => g.First())
.Select(x => new Role(x.RoleId!.Value, x.RoleName!, x.RoleDescription))
.ToList();
// // Clear any existing configuration and force our URL var links = userData
// builder.WebHost.ConfigureKestrel(options => .Where(x => x.LinkId.HasValue)
// { .GroupBy(x => x.LinkId)
// options.ListenAnyIP(8000); .Select(g => g.First())
// }); .Select(x => new Link(x.LinkId!.Value, x.LinkTitle!, x.LinkUrl!, x.LinkDescription))
.ToList();
// Configuration return Results.Ok(new UserInfo(firstRecord.Email, firstRecord.FullName, organization, roles, links));
var config = new AppConfig }
catch (NpgsqlException ex) when (ex.IsTransient)
{ {
DbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "postgres", return Results.Json(new { error = "Database temporarily unavailable" }, statusCode: 503);
DbPort = Environment.GetEnvironmentVariable("DB_PORT") ?? "5440", }
DbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "dexdemo", catch (NpgsqlException)
DbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "dexdemo", {
DbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "dexdemo", return Results.Json(new { error = "Database error" }, statusCode: 500);
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 app.Run();
builder.Services.AddSingleton(config); static void ConfigureJwtAuthentication(IServiceCollection services, AppConfig config)
{
if (config.InsecureDevMode)
{
services.AddAuthentication("InsecureDev")
.AddScheme<AuthenticationSchemeOptions, InsecureDevAuthenticationHandler>("InsecureDev", _ => { });
// Configure JWT Authentication return;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) }
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => .AddJwtBearer(options =>
{ {
options.Authority = config.Issuer; options.Authority = config.Issuer;
options.RequireHttpsMetadata = false; // For development options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidateIssuer = true, ValidateIssuer = true,
ValidIssuer = config.Issuer, ValidIssuer = config.Issuer,
ValidateAudience = false, // Можно включить, если DEX выдает audience ValidateAudience = false,
ValidateLifetime = true, ValidateLifetime = true,
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(5), // Допуск 5 минут на разницу времени ClockSkew = config.ClockSkew,
NameClaimType = config.NameClaimType,
// Настройка для заполнения User.Identity.Name RoleClaimType = config.RoleClaimType
NameClaimType = "name", // Используем claim "name" для User.Identity.Name
RoleClaimType = "role" // Используем claim "role" для ролей
}; };
// Add detailed event logging for JWT authentication
options.Events = new JwtBearerEvents 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 => OnTokenValidated = context =>
{ {
var jwtLogger = context.HttpContext.RequestServices.GetRequiredService<ILogger<JwtBearerHandler>>(); var nameClaim = context.Principal?.FindFirst(config.NameClaimType);
var emailClaim = context.Principal?.FindFirst(config.EmailClaimType);
// Логируем все доступные 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) if (nameClaim == null && emailClaim != null)
{ {
// Если нет name claim, но есть email - добавляем его как name claim
var identity = context.Principal?.Identity as ClaimsIdentity; var identity = context.Principal?.Identity as ClaimsIdentity;
identity?.AddClaim(new Claim("name", emailClaim.Value)); identity?.AddClaim(new Claim(config.NameClaimType, 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; return Task.CompletedTask;
} }
}; };
// Configure OpenID Connect discovery var httpClientHandler = new HttpClientHandler
var httpClientHandler = new HttpClientHandler();
// В режиме разработки отключаем проверку SSL
// В продакшене это должно быть настроено правильно!
if (config.InsecureDevMode)
{ {
// Logging will be done after logger is created ServerCertificateCustomValidationCallback = (_, _, _, _) => true
httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; };
}
var discoveryUrl = $"{config.Issuer}.well-known/openid-configuration"; var discoveryUrl = $"{config.Issuer}.well-known/openid-configuration";
// Logging will be done after logger is created
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>( options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
discoveryUrl, discoveryUrl,
@@ -157,229 +197,77 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
} }
); );
}); });
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>>();
// 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<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
{
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 static void ValidateConfiguration(AppConfig config)
? 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); if (string.IsNullOrWhiteSpace(config.DbHost))
throw; 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");
} }
});
logger.LogInformation("[App] Application starting..."); static Task<string> GetUserEmail(HttpContext context, AppConfig config)
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)
{ {
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) if (context.User.Identity?.IsAuthenticated == true)
{ {
logger.LogDebug("[GetUserEmail] All user claims:"); var emailClaim = context.User.FindFirst(config.EmailClaimType);
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) 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(config.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
);

View File

@@ -13,9 +13,9 @@ dotnet run
```bash ```bash
docker build -t dex-demo-backend-dotnet:latest . docker build -t dex-demo-backend-dotnet:latest .
docker run -p 8000:8000 \ docker run -p 8000:8000 \
-e DB_HOST=postgres \ -e AppConfig__DbHost=postgres \
-e DB_PORT=5440 \ -e AppConfig__DbPort=5440 \
-e DEX_ISSUER=https://dex.127.0.0.1.sslip.io/ \ -e AppConfig__Issuer=https://dex.127.0.0.1.sslip.io/ \
dex-demo-backend-dotnet:latest dex-demo-backend-dotnet:latest
``` ```
@@ -34,8 +34,20 @@ docker run -p 8000:8000 \
## Переменные окружения ## Переменные окружения
Все переменные идентичны Python версии: ### База данных
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` - `AppConfig__DbHost` - хост PostgreSQL (по умолчанию: postgres)
- `DEX_ISSUER` - `AppConfig__DbPort` - порт PostgreSQL (по умолчанию: 5440)
- `INSECURE_DEV_MODE`, `INSECURE_DEV_EMAIL` - `AppConfig__DbName` - имя базы данных (по умолчанию: dexdemo)
- `AppConfig__DbUser` - пользователь базы данных (по умолчанию: dexdemo)
- `AppConfig__DbPassword` - пароль базы данных (по умолчанию: dexdemo)
### Аутентификация
- `AppConfig__Issuer` - URL Dex сервера (по умолчанию: https://dex.127.0.0.1.sslip.io/)
### Режим разработки
- `AppConfig__InsecureDevMode` - включить небезопасный режим разработки (true/false)
- `AppConfig__InsecureDevEmail` - email для тестирования в режиме разработки
### CORS
- `AppConfig__AllowedOrigins` - разрешенные origins для CORS (JSON массив)

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"]
}
} }