Integrer l'authentification Keycloak dans l'application
This commit is contained in:
59
ChessCubing.Server/Auth/AuthContracts.cs
Normal file
59
ChessCubing.Server/Auth/AuthContracts.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ChessCubing.Server.Auth;
|
||||
|
||||
public sealed record LoginRequest(string Username, string Password);
|
||||
|
||||
public sealed record RegisterRequest(
|
||||
string Username,
|
||||
string Email,
|
||||
string Password,
|
||||
string ConfirmPassword,
|
||||
string? FirstName,
|
||||
string? LastName);
|
||||
|
||||
public sealed record ApiErrorResponse(string Message);
|
||||
|
||||
public sealed class AuthSessionResponse
|
||||
{
|
||||
public bool IsAuthenticated { get; init; }
|
||||
|
||||
public string? Subject { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
public string? Name { get; init; }
|
||||
|
||||
public string? Email { get; init; }
|
||||
|
||||
public string[] Roles { get; init; } = [];
|
||||
|
||||
public static AuthSessionResponse FromUser(ClaimsPrincipal user)
|
||||
=> new()
|
||||
{
|
||||
IsAuthenticated = user.Identity?.IsAuthenticated == true,
|
||||
Subject = user.FindFirst("sub")?.Value ?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
|
||||
Username = user.FindFirst("preferred_username")?.Value ?? user.Identity?.Name,
|
||||
Name = user.FindFirst("name")?.Value ?? user.Identity?.Name,
|
||||
Email = user.FindFirst("email")?.Value,
|
||||
Roles = user.FindAll("role").Select(claim => claim.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class KeycloakUserInfo
|
||||
{
|
||||
[JsonPropertyName("sub")]
|
||||
public string? Subject { get; init; }
|
||||
|
||||
[JsonPropertyName("preferred_username")]
|
||||
public string? PreferredUsername { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string? Email { get; init; }
|
||||
|
||||
public string[] Roles { get; set; } = [];
|
||||
}
|
||||
18
ChessCubing.Server/Auth/KeycloakAuthOptions.cs
Normal file
18
ChessCubing.Server/Auth/KeycloakAuthOptions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ChessCubing.Server.Auth;
|
||||
|
||||
public sealed class KeycloakAuthOptions
|
||||
{
|
||||
public string BaseUrl { get; set; } = "http://keycloak:8080/auth";
|
||||
|
||||
public string Realm { get; set; } = "chesscubing";
|
||||
|
||||
public string ClientId { get; set; } = "chesscubing-web";
|
||||
|
||||
public string AdminRealm { get; set; } = "master";
|
||||
|
||||
public string AdminClientId { get; set; } = "admin-cli";
|
||||
|
||||
public string AdminUsername { get; set; } = "admin";
|
||||
|
||||
public string AdminPassword { get; set; } = "admin";
|
||||
}
|
||||
334
ChessCubing.Server/Auth/KeycloakAuthService.cs
Normal file
334
ChessCubing.Server/Auth/KeycloakAuthService.cs
Normal file
@@ -0,0 +1,334 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ChessCubing.Server.Auth;
|
||||
|
||||
public sealed class KeycloakAuthService(HttpClient httpClient, IOptions<KeycloakAuthOptions> options)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly HttpClient _httpClient = httpClient;
|
||||
private readonly KeycloakAuthOptions _options = options.Value;
|
||||
|
||||
public async Task<KeycloakUserInfo> LoginAsync(string username, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
var tokenResponse = await RequestPasswordTokenAsync(username, password, cancellationToken);
|
||||
return await GetUserInfoAsync(tokenResponse.AccessToken!, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<KeycloakUserInfo> RegisterAsync(RegisterRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var adminToken = await RequestAdminTokenAsync(cancellationToken);
|
||||
var userId = await CreateUserAsync(adminToken, request, cancellationToken);
|
||||
await SetPasswordAsync(adminToken, userId, request.Password, cancellationToken);
|
||||
await TryAssignPlayerRoleAsync(adminToken, userId, cancellationToken);
|
||||
return await LoginAsync(request.Username, request.Password, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<TokenSuccessResponse> RequestPasswordTokenAsync(string username, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["client_id"] = _options.ClientId,
|
||||
["grant_type"] = "password",
|
||||
["scope"] = "openid profile email",
|
||||
["username"] = username,
|
||||
["password"] = password,
|
||||
};
|
||||
|
||||
using var response = await _httpClient.PostAsync(
|
||||
$"{GetRealmBaseUrl()}/protocol/openid-connect/token",
|
||||
new FormUrlEncodedContent(formData),
|
||||
cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await ReadJsonAsync<TokenErrorResponse>(response, cancellationToken);
|
||||
var message = error?.ErrorDescription switch
|
||||
{
|
||||
not null when error.ErrorDescription.Contains("Account is not fully set up", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Le compte existe mais n'est pas encore actif dans Keycloak.",
|
||||
not null => "Identifiants invalides ou connexion Keycloak indisponible.",
|
||||
_ => "Connexion Keycloak impossible.",
|
||||
};
|
||||
|
||||
throw new KeycloakAuthException(message, (int)response.StatusCode);
|
||||
}
|
||||
|
||||
var token = await ReadJsonAsync<TokenSuccessResponse>(response, cancellationToken);
|
||||
if (token?.AccessToken is null)
|
||||
{
|
||||
throw new KeycloakAuthException("Keycloak n'a pas retourne de jeton utilisable.");
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private async Task<string> RequestAdminTokenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var formData = new Dictionary<string, string>
|
||||
{
|
||||
["client_id"] = _options.AdminClientId,
|
||||
["grant_type"] = "password",
|
||||
["username"] = _options.AdminUsername,
|
||||
["password"] = _options.AdminPassword,
|
||||
};
|
||||
|
||||
using var response = await _httpClient.PostAsync(
|
||||
$"{GetBaseUrl()}/realms/{Uri.EscapeDataString(_options.AdminRealm)}/protocol/openid-connect/token",
|
||||
new FormUrlEncodedContent(formData),
|
||||
cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new KeycloakAuthException("Impossible d'obtenir un acces admin Keycloak.", (int)response.StatusCode);
|
||||
}
|
||||
|
||||
var token = await ReadJsonAsync<TokenSuccessResponse>(response, cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(token?.AccessToken))
|
||||
{
|
||||
throw new KeycloakAuthException("Keycloak n'a pas retourne de jeton admin.");
|
||||
}
|
||||
|
||||
return token.AccessToken;
|
||||
}
|
||||
|
||||
private async Task<KeycloakUserInfo> GetUserInfoAsync(string accessToken, CancellationToken cancellationToken)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"{GetRealmBaseUrl()}/protocol/openid-connect/userinfo");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new KeycloakAuthException("Impossible de recuperer le profil utilisateur Keycloak.", (int)response.StatusCode);
|
||||
}
|
||||
|
||||
var userInfo = await ReadJsonAsync<KeycloakUserInfo>(response, cancellationToken);
|
||||
if (userInfo is null)
|
||||
{
|
||||
throw new KeycloakAuthException("Le profil Keycloak est invalide.");
|
||||
}
|
||||
|
||||
userInfo.Roles = ExtractRealmRoles(accessToken);
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
private async Task<string> CreateUserAsync(string adminToken, RegisterRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{GetAdminBaseUrl()}/users")
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
username = request.Username.Trim(),
|
||||
email = request.Email.Trim(),
|
||||
enabled = true,
|
||||
emailVerified = false,
|
||||
firstName = string.IsNullOrWhiteSpace(request.FirstName) ? null : request.FirstName.Trim(),
|
||||
lastName = string.IsNullOrWhiteSpace(request.LastName) ? null : request.LastName.Trim(),
|
||||
}, options: JsonOptions)
|
||||
};
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
throw new KeycloakAuthException("Ce nom d'utilisateur ou cet email existe deja.", StatusCodes.Status409Conflict);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new KeycloakAuthException("Creation du compte impossible dans Keycloak.", (int)response.StatusCode);
|
||||
}
|
||||
|
||||
var userId = response.Headers.Location?.Segments.LastOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return userId.Trim('/');
|
||||
}
|
||||
|
||||
var fallbackUserId = await FindUserIdByUsernameAsync(adminToken, request.Username, cancellationToken);
|
||||
if (!string.IsNullOrWhiteSpace(fallbackUserId))
|
||||
{
|
||||
return fallbackUserId;
|
||||
}
|
||||
|
||||
throw new KeycloakAuthException("Le compte a ete cree mais l'identifiant Keycloak est introuvable.");
|
||||
}
|
||||
|
||||
private async Task SetPasswordAsync(string adminToken, string userId, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"{GetAdminBaseUrl()}/users/{Uri.EscapeDataString(userId)}/reset-password")
|
||||
{
|
||||
Content = JsonContent.Create(new
|
||||
{
|
||||
type = "password",
|
||||
value = password,
|
||||
temporary = false,
|
||||
})
|
||||
};
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new KeycloakAuthException("Le compte a ete cree mais le mot de passe n'a pas pu etre defini.", (int)response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryAssignPlayerRoleAsync(string adminToken, string userId, CancellationToken cancellationToken)
|
||||
{
|
||||
using var roleRequest = new HttpRequestMessage(HttpMethod.Get, $"{GetAdminBaseUrl()}/roles/player");
|
||||
roleRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var roleResponse = await _httpClient.SendAsync(roleRequest, cancellationToken);
|
||||
if (!roleResponse.IsSuccessStatusCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var role = await ReadJsonAsync<RealmRoleRepresentation>(roleResponse, cancellationToken);
|
||||
if (role?.Name is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var assignRequest = new HttpRequestMessage(HttpMethod.Post, $"{GetAdminBaseUrl()}/users/{Uri.EscapeDataString(userId)}/role-mappings/realm")
|
||||
{
|
||||
Content = JsonContent.Create(new[]
|
||||
{
|
||||
new RealmRoleRepresentation
|
||||
{
|
||||
Id = role.Id,
|
||||
Name = role.Name,
|
||||
Description = role.Description,
|
||||
}
|
||||
})
|
||||
};
|
||||
assignRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var assignResponse = await _httpClient.SendAsync(assignRequest, cancellationToken);
|
||||
_ = assignResponse.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
private async Task<string?> FindUserIdByUsernameAsync(string adminToken, string username, CancellationToken cancellationToken)
|
||||
{
|
||||
using var httpRequest = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
$"{GetAdminBaseUrl()}/users?username={Uri.EscapeDataString(username)}&exact=true");
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var users = await ReadJsonAsync<List<UserRepresentation>>(response, cancellationToken);
|
||||
return users?.FirstOrDefault()?.Id;
|
||||
}
|
||||
|
||||
private string[] ExtractRealmRoles(string accessToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenParts = accessToken.Split('.');
|
||||
if (tokenParts.Length < 2)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var payloadBytes = DecodeBase64Url(tokenParts[1]);
|
||||
using var document = JsonDocument.Parse(payloadBytes);
|
||||
if (!document.RootElement.TryGetProperty("realm_access", out var realmAccess) ||
|
||||
!realmAccess.TryGetProperty("roles", out var rolesElement) ||
|
||||
rolesElement.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return rolesElement
|
||||
.EnumerateArray()
|
||||
.Select(role => role.GetString())
|
||||
.Where(role => !string.IsNullOrWhiteSpace(role))
|
||||
.Cast<string>()
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] DecodeBase64Url(string input)
|
||||
{
|
||||
var normalized = input.Replace('-', '+').Replace('_', '/');
|
||||
normalized = normalized.PadRight(normalized.Length + (4 - normalized.Length % 4) % 4, '=');
|
||||
return Convert.FromBase64String(normalized);
|
||||
}
|
||||
|
||||
private string GetBaseUrl()
|
||||
=> _options.BaseUrl.TrimEnd('/');
|
||||
|
||||
private string GetRealmBaseUrl()
|
||||
=> $"{GetBaseUrl()}/realms/{Uri.EscapeDataString(_options.Realm)}";
|
||||
|
||||
private string GetAdminBaseUrl()
|
||||
=> $"{GetBaseUrl()}/admin/realms/{Uri.EscapeDataString(_options.Realm)}";
|
||||
|
||||
private static async Task<T?> ReadJsonAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<T>(content, JsonOptions);
|
||||
}
|
||||
|
||||
private sealed class TokenSuccessResponse
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public string? AccessToken { get; init; }
|
||||
}
|
||||
|
||||
private sealed class TokenErrorResponse
|
||||
{
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; init; }
|
||||
|
||||
[JsonPropertyName("error_description")]
|
||||
public string? ErrorDescription { get; init; }
|
||||
}
|
||||
|
||||
private sealed class UserRepresentation
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RealmRoleRepresentation
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; init; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class KeycloakAuthException(string message, int statusCode = StatusCodes.Status400BadRequest) : Exception(message)
|
||||
{
|
||||
public int StatusCode { get; } = statusCode;
|
||||
}
|
||||
11
ChessCubing.Server/ChessCubing.Server.csproj
Normal file
11
ChessCubing.Server/ChessCubing.Server.csproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ChessCubing.Server</RootNamespace>
|
||||
<AssemblyName>ChessCubing.Server</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
173
ChessCubing.Server/Program.cs
Normal file
173
ChessCubing.Server/Program.cs
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user