using System.Security.Claims; using ChessCubing.Server.Auth; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOptions() .Configure((options, configuration) => { options.BaseUrl = configuration["KEYCLOAK_BASE_URL"] ?? options.BaseUrl; options.Realm = configuration["KEYCLOAK_REALM"] ?? options.Realm; options.ClientId = configuration["KEYCLOAK_CLIENT_ID"] ?? options.ClientId; options.AdminRealm = configuration["KEYCLOAK_ADMIN_REALM"] ?? options.AdminRealm; options.AdminClientId = configuration["KEYCLOAK_ADMIN_CLIENT_ID"] ?? options.AdminClientId; options.AdminUsername = configuration["KEYCLOAK_ADMIN_USERNAME"] ?? options.AdminUsername; options.AdminPassword = configuration["KEYCLOAK_ADMIN_PASSWORD"] ?? options.AdminPassword; }); builder.Services .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.Name = "chesscubing.auth"; options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.SlidingExpiration = true; options.ExpireTimeSpan = TimeSpan.FromDays(7); options.Events = new CookieAuthenticationEvents { OnRedirectToLogin = context => { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; }, OnRedirectToAccessDenied = context => { context.Response.StatusCode = StatusCodes.Status403Forbidden; return Task.CompletedTask; }, }; }); builder.Services.AddAuthorization(); builder.Services.AddHttpClient(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapGet("/api/health", () => TypedResults.Ok(new { status = "ok" })); app.MapGet("/api/auth/session", (ClaimsPrincipal user) => TypedResults.Ok(AuthSessionResponse.FromUser(user))); app.MapPost("/api/auth/login", async Task ( LoginRequest request, HttpContext httpContext, KeycloakAuthService keycloak, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password)) { return TypedResults.BadRequest(new ApiErrorResponse("Nom d'utilisateur et mot de passe obligatoires.")); } try { var userInfo = await keycloak.LoginAsync(request.Username.Trim(), request.Password, cancellationToken); await SignInAsync(httpContext, userInfo); return TypedResults.Ok(AuthSessionResponse.FromUser(httpContext.User)); } catch (KeycloakAuthException exception) { return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode); } }); app.MapPost("/api/auth/register", async Task ( RegisterRequest request, HttpContext httpContext, KeycloakAuthService keycloak, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password)) { return TypedResults.BadRequest(new ApiErrorResponse("Nom d'utilisateur, email et mot de passe obligatoires.")); } if (!string.Equals(request.Password, request.ConfirmPassword, StringComparison.Ordinal)) { return TypedResults.BadRequest(new ApiErrorResponse("Les mots de passe ne correspondent pas.")); } try { var userInfo = await keycloak.RegisterAsync(request with { Username = request.Username.Trim(), Email = request.Email.Trim(), FirstName = request.FirstName?.Trim(), LastName = request.LastName?.Trim(), }, cancellationToken); await SignInAsync(httpContext, userInfo); return TypedResults.Ok(AuthSessionResponse.FromUser(httpContext.User)); } catch (KeycloakAuthException exception) { return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode); } }); app.MapPost("/api/auth/logout", async Task (HttpContext httpContext) => { await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return TypedResults.Ok(AuthSessionResponse.FromUser(new ClaimsPrincipal(new ClaimsIdentity()))); }); app.Run(); static async Task SignInAsync(HttpContext httpContext, KeycloakUserInfo userInfo) { var claims = new List(); if (!string.IsNullOrWhiteSpace(userInfo.Subject)) { claims.Add(new Claim("sub", userInfo.Subject)); claims.Add(new Claim(ClaimTypes.NameIdentifier, userInfo.Subject)); } if (!string.IsNullOrWhiteSpace(userInfo.PreferredUsername)) { claims.Add(new Claim("preferred_username", userInfo.PreferredUsername)); claims.Add(new Claim(ClaimTypes.Name, userInfo.PreferredUsername)); } if (!string.IsNullOrWhiteSpace(userInfo.Name)) { claims.Add(new Claim("name", userInfo.Name)); } if (!string.IsNullOrWhiteSpace(userInfo.Email)) { claims.Add(new Claim("email", userInfo.Email)); claims.Add(new Claim(ClaimTypes.Email, userInfo.Email)); } foreach (var role in userInfo.Roles.Where(role => !string.IsNullOrWhiteSpace(role))) { claims.Add(new Claim("role", role)); claims.Add(new Claim(ClaimTypes.Role, role)); } var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var principal = new ClaimsPrincipal(identity); await httpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = true, AllowRefresh = true, ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7), }); httpContext.User = principal; }