Files
python-oauth2-proxy-k8s/backend-dotnet/Program.cs
2026-01-20 15:56:52 +05:00

277 lines
9.3 KiB
C#

using Dapper;
using DexDemoBackend;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Npgsql;
using System.Security.Claims;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<AppConfig>(builder.Configuration.GetSection("AppConfig"));
builder.Services.AddSingleton(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.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(config.AllowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
builder.Services.AddAuthorization();
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower;
});
var app = builder.Build();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
app.UseCors();
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" });
}
var firstRecord = userData.First();
var organization = firstRecord.OrgId.HasValue && firstRecord.OrgName != null
? new Organization(firstRecord.OrgId.Value, firstRecord.OrgName)
: null;
var roles = userData
.Where(x => x.RoleId.HasValue)
.GroupBy(x => x.RoleId)
.Select(g => g.First())
.Select(x => new Role(x.RoleId!.Value, x.RoleName!, x.RoleDescription))
.ToList();
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();
return Results.Ok(new UserInfo(firstRecord.Email, firstRecord.FullName, organization, roles, links));
}
catch (NpgsqlException ex) when (ex.IsTransient)
{
return Results.Json(new { error = "Database temporarily unavailable" }, statusCode: 503);
}
catch (NpgsqlException)
{
return Results.Json(new { error = "Database error" }, statusCode: 500);
}
});
app.Run();
static void ConfigureJwtAuthentication(IServiceCollection services, AppConfig config)
{
if (config.InsecureDevMode)
{
services.AddAuthentication("InsecureDev")
.AddScheme<AuthenticationSchemeOptions, InsecureDevAuthenticationHandler>("InsecureDev", _ => { });
return;
}
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = config.Issuer;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = config.ClockSkew,
NameClaimType = config.NameClaimType,
RoleClaimType = config.RoleClaimType
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var nameClaim = context.Principal?.FindFirst(config.NameClaimType);
var emailClaim = context.Principal?.FindFirst(config.EmailClaimType);
if (nameClaim == null && emailClaim != null)
{
var identity = context.Principal?.Identity as ClaimsIdentity;
identity?.AddClaim(new Claim(config.NameClaimType, emailClaim.Value));
}
return Task.CompletedTask;
}
};
var httpClientHandler = new 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 (context.User.Identity?.IsAuthenticated == true)
{
var emailClaim = context.User.FindFirst(config.EmailClaimType);
if (emailClaim != null)
{
return Task.FromResult(emailClaim.Value);
}
}
if (context.Request.Headers.TryGetValue(config.AuthRequestEmailHeader, out var emailHeader))
{
return Task.FromResult(emailHeader.ToString());
}
throw new UnauthorizedAccessException("No authentication information found");
}
record UserDataResult(
string Email,
string FullName,
int? OrgId,
string? OrgName,
int? RoleId,
string? RoleName,
string? RoleDescription,
int? LinkId,
string? LinkTitle,
string? LinkUrl,
string? LinkDescription
);
// API Response Models
record HealthResponse(string Status);
record ClaimResponse(string Type, string Value);
record UserIdentityResponse(
string? Name,
bool IsAuthenticated,
string? AuthenticationType,
List<ClaimResponse> Claims
);