@@ -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" ,
Dex Issuer = 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 inf o endpoint
app . MapGet ( "/api/user-info" , async ( HttpContext context , [ FromServices ] AppConfig cfg , [ F romServices ] JwtValidator jwtValidato r) = >
// User identity dem o endpoint
app . MapGet ( "/api/user-identity" , [ Authorize ] ( HttpContext context , [ FromServices ] ILogger < P rogram > logge r) = >
{
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 jwtValidato r)
static Task < string > GetUserEmail ( HttpContext context , AppConfig config , ILogger logge r)
{
// Dev mode
if ( config . InsecureDevM ode )
logger . LogDebug ( "[GetUserEmail] Starting authentication check for path: {Path}" , context . Request . Path ) ;
logger . LogDebug ( "[GetUserEmail] Request method: {Method}" , context . Request . Meth od) ;
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} ") ;
retur n 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 i n 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 hea der
if ( context . Request . Headers . TryGetValue ( "X-Auth-Request-Email" , out var emailHeader ) )
// Dev mo de
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" ) ;
}