@@ -3,150 +3,185 @@ 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.js on
// Configure AppConfig using IConfigurati on
// 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 . AddSingleto n ( sp = > sp . GetRequiredService < Microsoft . Extensions . Options . IOptions < AppConfig > > ( ) . Value ) ;
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 ;
// Get current config from DI (supports reload)
var currentConfig = app . Services . GetRequiredService < AppConfig > ( ) ;
app . UseCors ( policy = >
{
policy . WithOrigins ( currentConfig . AllowedOrigins )
. AllowAnyMethod ( )
. AllowAnyHeader ( )
. AllowCredentials ( ) ;
} ) ;
app . UseAuthentication ( ) ;
app . UseAuthorization ( ) ;
// Global error handling middleware
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 ( I ServiceCollection services , AppConfig config )
{
// Configure JWT Authentication
services . AddAuthentication ( JwtBearerDefaults . AuthenticationScheme )
builder . 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 = OidcConfigConstants . ClockSkew ,
NameClaimType = OidcConfigConstants . NameClaimType ,
// Настройка для заполнения User.Identity.Nam e
RoleClaimType = OidcConfigConstants . RoleClaimTyp e
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 ( OidcConfigConstants . NameClaimType ) ;
var emailClaim = context . Principal ? . FindFirst ( OidcConfigConstants . 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 ( OidcConfigConstants . 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 )
if ( config . InsecureDevMode )
{
{
// Logging will be done after logger is created
httpClientHandler . 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 +192,102 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
}
}
) ;
) ;
} ) ;
} ) ;
}
builder . Services . AddAuthorization ( ) ;
// Configure JSON serialization to use snake_case
static void ValidateConfiguration ( AppConfig config )
builder . Services . ConfigureHttpJsonOptions ( options = >
{
{
options . SerializerOptions . PropertyNamingPolicy = System . Text . Json . JsonNamingPolicy . SnakeCaseLower ;
if ( string . IsNullOrWhiteSpace ( config . DbHost ) )
} ) ;
throw new InvalidOperationException ( "Configuration parameter 'AppConfig:DbHost' is required" ) ;
builder . Servi ces . AddCors ( options = >
if ( string . IsNullOrWhiteSpa ce ( 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 )
{
{
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 )
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
? 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 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 )
{
logger . LogDebug ( "[GetUserEmail] All user claims:" ) ;
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 ! ) ;
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 ) ;
}