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

@@ -1,8 +1,12 @@
@using System.ComponentModel.DataAnnotations
@using System.Net.Http.Json
@using System.Security.Claims
@implements IAsyncDisposable
@inject AuthenticationStateProvider AuthenticationStateProvider
@using ChessCubing.App.Models.Auth
@using Microsoft.AspNetCore.Components.Authorization
@implements IDisposable
@inject AppAuthenticationStateProvider AuthenticationStateProvider
@inject HttpClient Http
@inject NavigationManager Navigation
@inject IJSRuntime JS
<div class="site-menu-shell">
<header class="site-menu-bar">
@@ -22,7 +26,7 @@
</nav>
<div class="site-menu-account">
<span class="micro-label">Compte Keycloak</span>
<span class="micro-label">Compte joueur</span>
@if (IsAuthenticated)
{
<div class="site-menu-account-panel">
@@ -30,7 +34,7 @@
<strong>@DisplayName</strong>
<span>@DisplayMeta</span>
</div>
<a class="button ghost small" href="@LogoutHref">Se deconnecter</a>
<button class="button ghost small" type="button" @onclick="LogoutAsync" disabled="@IsSubmitting">Se deconnecter</button>
</div>
}
else
@@ -50,19 +54,97 @@
<div class="modal-card auth-modal-card">
<div class="modal-head">
<div>
<p class="eyebrow">Compte Keycloak</p>
<p class="eyebrow">Compte joueur</p>
<h2>@AuthModalTitle</h2>
</div>
<button class="button ghost small" type="button" @onclick="CloseAuthModal">Fermer</button>
<button class="button ghost small" type="button" @onclick="CloseAuthModal" disabled="@IsSubmitting">Fermer</button>
</div>
<p class="auth-modal-copy">
L'authentification se fait maintenant dans cette fenetre integree, sans quitter la page en cours.
L'authentification se fait maintenant directement dans l'application, sans redirection vers une page externe.
</p>
@if (!string.IsNullOrWhiteSpace(AuthModalSource))
<div class="auth-modal-switch">
<button class="@BuildModeButtonClass(AuthMode.Login)" type="button" @onclick="SwitchToLogin" disabled="@IsSubmitting">Se connecter</button>
<button class="@BuildModeButtonClass(AuthMode.Register)" type="button" @onclick="SwitchToRegister" disabled="@IsSubmitting">Creer un compte</button>
</div>
@if (!string.IsNullOrWhiteSpace(FormError))
{
<div class="auth-modal-frame-shell">
<iframe class="auth-modal-frame" src="@AuthModalSource" title="@AuthModalTitle"></iframe>
</div>
<p class="auth-form-error">@FormError</p>
}
@if (Mode == AuthMode.Login)
{
<EditForm Model="@LoginModel" OnValidSubmit="SubmitLoginAsync">
<DataAnnotationsValidator />
<div class="auth-form-grid">
<label class="field">
<span>Identifiant ou email</span>
<InputText @bind-Value="LoginModel.Username" autocomplete="username email" />
<ValidationMessage For="@(() => LoginModel.Username)" />
</label>
<label class="field">
<span>Mot de passe</span>
<InputText @bind-Value="LoginModel.Password" type="password" autocomplete="current-password" />
<ValidationMessage For="@(() => LoginModel.Password)" />
</label>
</div>
<div class="modal-actions">
<button class="button secondary" type="submit" disabled="@IsSubmitting">
@(IsSubmitting ? "Connexion..." : "Se connecter")
</button>
</div>
</EditForm>
}
else
{
<EditForm Model="@RegisterModel" OnValidSubmit="SubmitRegisterAsync">
<DataAnnotationsValidator />
<div class="auth-form-grid two-columns">
<label class="field">
<span>Prenom</span>
<InputText @bind-Value="RegisterModel.FirstName" autocomplete="given-name" />
</label>
<label class="field">
<span>Nom</span>
<InputText @bind-Value="RegisterModel.LastName" autocomplete="family-name" />
</label>
<label class="field">
<span>Nom d'utilisateur</span>
<InputText @bind-Value="RegisterModel.Username" autocomplete="username" />
<ValidationMessage For="@(() => RegisterModel.Username)" />
</label>
<label class="field">
<span>Email</span>
<InputText @bind-Value="RegisterModel.Email" autocomplete="email" />
<ValidationMessage For="@(() => RegisterModel.Email)" />
</label>
<label class="field">
<span>Mot de passe</span>
<InputText @bind-Value="RegisterModel.Password" type="password" autocomplete="new-password" />
<ValidationMessage For="@(() => RegisterModel.Password)" />
</label>
<label class="field">
<span>Confirmation du mot de passe</span>
<InputText @bind-Value="RegisterModel.ConfirmPassword" type="password" autocomplete="new-password" />
<ValidationMessage For="@(() => RegisterModel.ConfirmPassword)" />
</label>
</div>
<div class="modal-actions">
<button class="button secondary" type="submit" disabled="@IsSubmitting">
@(IsSubmitting ? "Creation..." : "Creer mon compte")
</button>
</div>
</EditForm>
}
</div>
</section>
@@ -72,22 +154,18 @@
private static readonly string[] ApplicationPaths = ["application", "application.html"];
private static readonly string[] RulesPaths = ["reglement", "reglement.html"];
private DotNetObjectReference<SiteMenu>? _dotNetReference;
private bool _listenerRegistered;
private bool _authStateSubscribed;
private string? AuthModalSource;
private string AuthModalTitle = "Authentification";
private bool ShowAuthModal;
private readonly LoginFormModel LoginModel = new();
private readonly RegisterFormModel RegisterModel = new();
private bool IsAuthenticated;
private bool ShowAuthModal;
private bool IsSubmitting;
private string? FormError;
private string AuthModalTitle = "Se connecter";
private AuthMode Mode = AuthMode.Login;
private string DisplayName = "Utilisateur connecte";
private string DisplayMeta = "Session active";
private string LoginHref => BuildAuthHref("login", EffectiveReturnUrl);
private string RegisterHref => BuildAuthHref("register", EffectiveReturnUrl);
private string LoginModalSrc => BuildAuthHref("login", EffectiveReturnUrl, embedded: true);
private string RegisterModalSrc => BuildAuthHref("register", EffectiveReturnUrl, embedded: true);
private string LogoutHref => BuildAuthHref("logout", "/");
private string CurrentPath
{
get
@@ -97,20 +175,10 @@
}
}
private string EffectiveReturnUrl
protected override async Task OnInitializedAsync()
{
get
{
var absolutePath = new Uri(Navigation.Uri).AbsolutePath;
if (absolutePath.StartsWith("/authentication/", StringComparison.OrdinalIgnoreCase))
{
return "/";
}
return string.IsNullOrWhiteSpace(absolutePath)
? "/"
: absolutePath;
}
AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
await RefreshAuthenticationStateAsync();
}
private string BuildNavLinkClass(string[] paths)
@@ -122,88 +190,138 @@
private bool IsCurrentPage(string[] paths)
=> paths.Any(path => string.Equals(CurrentPath, path, StringComparison.OrdinalIgnoreCase));
private static string BuildAuthHref(string action, string returnUrl, bool embedded = false)
private void OpenLoginModal()
{
var query = $"returnUrl={Uri.EscapeDataString(returnUrl)}";
if (embedded)
{
query += "&embedded=1";
}
return $"authentication/{action}?{query}";
}
private static string BuildDisplayName(ClaimsPrincipal user)
=> user.Identity?.Name
?? user.FindFirst("name")?.Value
?? user.FindFirst("preferred_username")?.Value
?? "Utilisateur connecte";
private static string BuildMeta(ClaimsPrincipal user)
=> user.FindFirst("email")?.Value
?? "Session active";
protected override async Task OnInitializedAsync()
{
AuthenticationStateProvider.AuthenticationStateChanged += HandleAuthenticationStateChanged;
_authStateSubscribed = true;
await RefreshAuthenticationStateAsync();
}
private async Task OpenLoginModal()
=> await OpenAuthModalAsync("Se connecter", LoginModalSrc);
private async Task OpenRegisterModal()
=> await OpenAuthModalAsync("Creer un compte", RegisterModalSrc);
private async Task OpenAuthModalAsync(string title, string source)
{
await EnsureAuthListenerAsync();
AuthModalTitle = title;
AuthModalSource = source;
ShowAuthModal = true;
SwitchToLogin();
}
private void OpenRegisterModal()
{
ShowAuthModal = true;
SwitchToRegister();
}
private void CloseAuthModal()
{
ShowAuthModal = false;
AuthModalSource = null;
}
private async Task EnsureAuthListenerAsync()
{
if (_listenerRegistered)
if (IsSubmitting)
{
return;
}
_dotNetReference ??= DotNetObjectReference.Create(this);
ShowAuthModal = false;
FormError = null;
}
private void SwitchToLogin()
{
Mode = AuthMode.Login;
AuthModalTitle = "Se connecter";
FormError = null;
}
private void SwitchToRegister()
{
Mode = AuthMode.Register;
AuthModalTitle = "Creer un compte";
FormError = null;
}
private string BuildModeButtonClass(AuthMode mode)
=> mode == Mode ? "button secondary small" : "button ghost small";
private async Task SubmitLoginAsync()
{
await SubmitAsync(
"api/auth/login",
new LoginRequest
{
Username = LoginModel.Username.Trim(),
Password = LoginModel.Password,
},
"Connexion impossible.");
}
private async Task SubmitRegisterAsync()
{
await SubmitAsync(
"api/auth/register",
new RegisterRequest
{
Username = RegisterModel.Username.Trim(),
Email = RegisterModel.Email.Trim(),
Password = RegisterModel.Password,
ConfirmPassword = RegisterModel.ConfirmPassword,
FirstName = RegisterModel.FirstName?.Trim(),
LastName = RegisterModel.LastName?.Trim(),
},
"Creation du compte impossible.");
}
private async Task SubmitAsync<TRequest>(string endpoint, TRequest payload, string fallbackMessage)
{
if (IsSubmitting)
{
return;
}
IsSubmitting = true;
FormError = null;
try
{
await JS.InvokeVoidAsync("chesscubingAuthModal.registerListener", _dotNetReference);
_listenerRegistered = true;
var response = await Http.PostAsJsonAsync(endpoint, payload);
if (!response.IsSuccessStatusCode)
{
FormError = await ReadErrorAsync(response, fallbackMessage);
return;
}
var session = await response.Content.ReadFromJsonAsync<AuthSessionResponse>();
if (session is not null && session.IsAuthenticated)
{
AuthenticationStateProvider.SetAuthenticated(session);
}
else
{
await AuthenticationStateProvider.RefreshAsync();
}
ShowAuthModal = false;
FormError = null;
ResetForms();
await RefreshAuthenticationStateAsync();
}
catch
{
// Si le navigateur garde encore un ancien script en cache,
// on laisse l'application fonctionner et la modal reste utilisable sans auto-fermeture.
FormError = fallbackMessage;
}
finally
{
IsSubmitting = false;
}
}
[JSInvokable]
public Task HandleAuthModalMessage(string status)
private async Task LogoutAsync()
{
if (!string.Equals(status, "login-succeeded", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(status, "logout-succeeded", StringComparison.OrdinalIgnoreCase))
if (IsSubmitting)
{
return Task.CompletedTask;
return;
}
CloseAuthModal();
StateHasChanged();
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
return Task.CompletedTask;
IsSubmitting = true;
FormError = null;
try
{
await Http.PostAsync("api/auth/logout", null);
AuthenticationStateProvider.SetAnonymous();
await RefreshAuthenticationStateAsync();
}
finally
{
IsSubmitting = false;
}
}
private void HandleAuthenticationStateChanged(Task<AuthenticationState> authenticationStateTask)
@@ -231,6 +349,8 @@
{
ResetAuthenticationDisplay();
}
StateHasChanged();
}
private void ResetAuthenticationDisplay()
@@ -240,27 +360,90 @@
DisplayMeta = "Session active";
}
public async ValueTask DisposeAsync()
private void ResetForms()
{
if (_authStateSubscribed)
{
AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
}
LoginModel.Username = string.Empty;
LoginModel.Password = string.Empty;
RegisterModel.FirstName = string.Empty;
RegisterModel.LastName = string.Empty;
RegisterModel.Username = string.Empty;
RegisterModel.Email = string.Empty;
RegisterModel.Password = string.Empty;
RegisterModel.ConfirmPassword = string.Empty;
}
if (_listenerRegistered)
private static string BuildDisplayName(ClaimsPrincipal user)
=> user.FindFirst("name")?.Value
?? user.FindFirst("preferred_username")?.Value
?? user.Identity?.Name
?? "Utilisateur connecte";
private static string BuildMeta(ClaimsPrincipal user)
=> user.FindFirst("email")?.Value
?? "Session active";
private static async Task<string> ReadErrorAsync(HttpResponseMessage response, string fallbackMessage)
{
try
{
try
{
await JS.InvokeVoidAsync("chesscubingAuthModal.unregisterListener");
}
catch
var error = await response.Content.ReadFromJsonAsync<ApiErrorMessage>();
if (!string.IsNullOrWhiteSpace(error?.Message))
{
return error.Message;
}
}
catch
{
}
_dotNetReference?.Dispose();
return fallbackMessage;
}
private static string BoolString(bool value)
=> value ? "true" : "false";
public void Dispose()
=> AuthenticationStateProvider.AuthenticationStateChanged -= HandleAuthenticationStateChanged;
private enum AuthMode
{
Login,
Register,
}
private sealed class ApiErrorMessage
{
public string? Message { get; set; }
}
private sealed class LoginFormModel
{
[Required(ErrorMessage = "L'identifiant est obligatoire.")]
public string Username { get; set; } = string.Empty;
[Required(ErrorMessage = "Le mot de passe est obligatoire.")]
public string Password { get; set; } = string.Empty;
}
private sealed class RegisterFormModel
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
[Required(ErrorMessage = "Le nom d'utilisateur est obligatoire.")]
public string Username { get; set; } = string.Empty;
[Required(ErrorMessage = "L'email est obligatoire.")]
[EmailAddress(ErrorMessage = "L'email n'est pas valide.")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Le mot de passe est obligatoire.")]
[MinLength(8, ErrorMessage = "Le mot de passe doit contenir au moins 8 caracteres.")]
public string Password { get; set; } = string.Empty;
[Required(ErrorMessage = "La confirmation est obligatoire.")]
[Compare(nameof(Password), ErrorMessage = "Les mots de passe ne correspondent pas.")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}

View File

@@ -25,8 +25,7 @@ else
|| string.Equals(currentPath, "cube", StringComparison.OrdinalIgnoreCase)
|| string.Equals(currentPath, "cube.html", StringComparison.OrdinalIgnoreCase)
|| string.Equals(currentPath, "application", StringComparison.OrdinalIgnoreCase)
|| string.Equals(currentPath, "application.html", StringComparison.OrdinalIgnoreCase)
|| currentPath.StartsWith("authentication/", StringComparison.OrdinalIgnoreCase);
|| string.Equals(currentPath, "application.html", StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,16 @@
namespace ChessCubing.App.Models.Auth;
public sealed class AuthSessionResponse
{
public bool IsAuthenticated { get; set; }
public string? Subject { get; set; }
public string? Username { get; set; }
public string? Name { get; set; }
public string? Email { get; set; }
public string[] Roles { get; set; } = [];
}

View File

@@ -0,0 +1,8 @@
namespace ChessCubing.App.Models.Auth;
public sealed class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,16 @@
namespace ChessCubing.App.Models.Auth;
public sealed class RegisterRequest
{
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string ConfirmPassword { get; set; } = string.Empty;
public string? FirstName { get; set; }
public string? LastName { get; set; }
}

View File

@@ -1,109 +1,16 @@
@page "/authentication/{action}"
@inject NavigationManager Navigation
@inject IJSRuntime JS
<main class="rules-shell">
<section class="panel panel-wide cta-panel" style="margin-top: 2rem;">
<p class="eyebrow">Authentification</p>
@if (StartsInteractiveFlow)
{
<div>
<strong>@(IsRegisterAction ? "Ouverture de la creation de compte..." : "Ouverture de la connexion...")</strong>
<p>
@(IsRegisterAction
? "Le formulaire Keycloak s'ouvre dans cette fenetre integree."
: "Le formulaire de connexion Keycloak s'ouvre dans cette fenetre integree.")
</p>
</div>
}
else
{
<RemoteAuthenticatorView Action="@Action" />
}
<div>
<strong>Le systeme d'authentification est maintenant integre a l'application.</strong>
<p>Utilise les boutons du menu en haut de page pour te connecter ou creer un compte sans quitter le site.</p>
</div>
</section>
</main>
@code {
[Parameter]
public string? Action { get; set; }
[SupplyParameterFromQuery(Name = "returnUrl")]
public string? ReturnUrl { get; set; }
[SupplyParameterFromQuery(Name = "embedded")]
public string? Embedded { get; set; }
private bool IsRegisterAction
=> string.Equals(Action, RemoteAuthenticationActions.Register, StringComparison.OrdinalIgnoreCase);
private bool IsLoginAction
=> string.Equals(Action, RemoteAuthenticationActions.LogIn, StringComparison.OrdinalIgnoreCase);
private bool IsEmbeddedFlow
=> string.Equals(Embedded, "1", StringComparison.OrdinalIgnoreCase)
|| string.Equals(Embedded, "true", StringComparison.OrdinalIgnoreCase);
private bool StartsInteractiveFlow
=> IsEmbeddedFlow && (IsLoginAction || IsRegisterAction);
protected override void OnParametersSet()
{
if (!StartsInteractiveFlow)
{
return;
}
var request = new InteractiveRequestOptions
{
Interaction = InteractionType.SignIn,
ReturnUrl = NormalizeReturnUrl(ReturnUrl)
};
if (IsRegisterAction)
{
request.TryAddAdditionalParameter("prompt", "create");
}
Navigation.NavigateToLogin("authentication/login", request);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
var status = Action switch
{
RemoteAuthenticationActions.LogInCallback => "login-succeeded",
RemoteAuthenticationActions.LogOutCallback => "logout-succeeded",
_ => null,
};
if (status is null || !IsEmbeddedFlow)
{
return;
}
try
{
await JS.InvokeVoidAsync("chesscubingAuthModal.notifyParent", status);
}
catch
{
}
}
private static string NormalizeReturnUrl(string? returnUrl)
{
if (string.IsNullOrWhiteSpace(returnUrl))
{
return "/";
}
return returnUrl.StartsWith("/", StringComparison.Ordinal)
? returnUrl
: $"/{returnUrl}";
}
}

View File

@@ -2,57 +2,18 @@ using ChessCubing.App;
using ChessCubing.App.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.Authorization;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
var keycloakAuthority = builder.Configuration["Keycloak:Authority"] ?? "/auth/realms/chesscubing";
var keycloakClientId = builder.Configuration["Keycloak:ClientId"] ?? "chesscubing-web";
var keycloakResponseType = builder.Configuration["Keycloak:ResponseType"] ?? "code";
var postLogoutRedirectUri = builder.Configuration["Keycloak:PostLogoutRedirectUri"] ?? "/";
var defaultScopes = builder.Configuration
.GetSection("Keycloak:DefaultScopes")
.GetChildren()
.Select(child => child.Value)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Cast<string>()
.ToArray();
builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services
.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = ResolveUri(builder.HostEnvironment.BaseAddress, keycloakAuthority);
options.ProviderOptions.ClientId = keycloakClientId;
options.ProviderOptions.ResponseType = keycloakResponseType;
options.ProviderOptions.RedirectUri = ResolveUri(builder.HostEnvironment.BaseAddress, "authentication/login-callback");
options.ProviderOptions.PostLogoutRedirectUri = ResolveUri(builder.HostEnvironment.BaseAddress, postLogoutRedirectUri);
options.ProviderOptions.DefaultScopes.Clear();
foreach (var scope in defaultScopes.Length == 0 ? ["openid", "profile", "email"] : defaultScopes)
{
options.ProviderOptions.DefaultScopes.Add(scope);
}
options.UserOptions.NameClaim = "preferred_username";
options.UserOptions.RoleClaim = "role";
})
.AddAccountClaimsPrincipalFactory<KeycloakAccountFactory>();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AppAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<AppAuthenticationStateProvider>());
builder.Services.AddScoped<BrowserBridge>();
builder.Services.AddScoped<UserSession>();
builder.Services.AddScoped<MatchStore>();
await builder.Build().RunAsync();
static string ResolveUri(string baseAddress, string value)
{
if (Uri.TryCreate(value, UriKind.Absolute, out var absoluteUri))
{
return absoluteUri.ToString();
}
return new Uri(new Uri(baseAddress), value).ToString();
}

View File

@@ -0,0 +1,101 @@
using System.Net.Http.Json;
using System.Security.Claims;
using ChessCubing.App.Models.Auth;
using Microsoft.AspNetCore.Components.Authorization;
namespace ChessCubing.App.Services;
public sealed class AppAuthenticationStateProvider(HttpClient httpClient) : AuthenticationStateProvider
{
private static readonly AuthenticationState AnonymousState = new(new ClaimsPrincipal(new ClaimsIdentity()));
private AuthenticationState? _cachedState;
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (_cachedState is not null)
{
return _cachedState;
}
_cachedState = await LoadStateAsync();
return _cachedState;
}
public async Task RefreshAsync()
{
_cachedState = await LoadStateAsync();
NotifyAuthenticationStateChanged(Task.FromResult(_cachedState));
}
public void SetAuthenticated(AuthSessionResponse session)
{
_cachedState = new AuthenticationState(BuildPrincipal(session));
NotifyAuthenticationStateChanged(Task.FromResult(_cachedState));
}
public void SetAnonymous()
{
_cachedState = AnonymousState;
NotifyAuthenticationStateChanged(Task.FromResult(_cachedState));
}
private async Task<AuthenticationState> LoadStateAsync()
{
try
{
var session = await httpClient.GetFromJsonAsync<AuthSessionResponse>("api/auth/session");
if (session is null || !session.IsAuthenticated)
{
return AnonymousState;
}
return new AuthenticationState(BuildPrincipal(session));
}
catch
{
return AnonymousState;
}
}
private static ClaimsPrincipal BuildPrincipal(AuthSessionResponse session)
{
if (!session.IsAuthenticated)
{
return AnonymousState.User;
}
var claims = new List<Claim>();
if (!string.IsNullOrWhiteSpace(session.Subject))
{
claims.Add(new Claim("sub", session.Subject));
claims.Add(new Claim(ClaimTypes.NameIdentifier, session.Subject));
}
if (!string.IsNullOrWhiteSpace(session.Username))
{
claims.Add(new Claim("preferred_username", session.Username));
claims.Add(new Claim(ClaimTypes.Name, session.Username));
}
if (!string.IsNullOrWhiteSpace(session.Name))
{
claims.Add(new Claim("name", session.Name));
}
if (!string.IsNullOrWhiteSpace(session.Email))
{
claims.Add(new Claim("email", session.Email));
claims.Add(new Claim(ClaimTypes.Email, session.Email));
}
foreach (var role in session.Roles.Where(role => !string.IsNullOrWhiteSpace(role)))
{
claims.Add(new Claim("role", role));
claims.Add(new Claim(ClaimTypes.Role, role));
}
return new ClaimsPrincipal(new ClaimsIdentity(claims, "ChessCubingCookie"));
}
}