Integrer l'authentification Keycloak dans l'application

This commit is contained in:
2026-04-13 23:59:20 +02:00
parent 53f0af761e
commit 9b739b02f6
20 changed files with 1201 additions and 276 deletions

View File

@@ -0,0 +1,173 @@
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<KeycloakAuthOptions>()
.Configure<IConfiguration>((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<KeycloakAuthService>();
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<IResult> (
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<IResult> (
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<IResult> (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<Claim>();
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;
}