174 lines
6.1 KiB
C#
174 lines
6.1 KiB
C#
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;
|
|
}
|