JWT- und Session-Validierung verbessert, UI aktualisiert

- JWT-Authentifizierung prüft nun, ob User aktiv und existent ist
- Session-Validierung beim App-Start via ValidateSessionAsync
- Ungültige Sessions werden entfernt, SignalR-Verbindung getrennt
- ApiService und IApiService um ValidateSessionAsync erweitert
- LoginViewModel nutzt Session-Check und behandelt SignalR-Fehler
- UI für eigene Phrasen auf <Border> mit Stil-Anpassung umgestellt
This commit is contained in:
Tim Krampitz
2026-08-08 21:30:18 +02:00
parent 6f3c0eb716
commit 3296ecdfb3
6 changed files with 74 additions and 10 deletions

View File

@@ -4,6 +4,7 @@ using Microsoft.IdentityModel.Tokens;
using SlipItIn.Server.Data; using SlipItIn.Server.Data;
using SlipItIn.Server.Hubs; using SlipItIn.Server.Hubs;
using SlipItIn.Server.Services; using SlipItIn.Server.Services;
using System.Security.Claims;
using System.Text; using System.Text;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -50,6 +51,24 @@ builder.Services
context.Token = accessToken; context.Token = accessToken;
} }
return Task.CompletedTask; return Task.CompletedTask;
},
OnTokenValidated = async context =>
{
var userIdClaim = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!int.TryParse(userIdClaim, out var userId))
{
context.Fail("Invalid user claim.");
return;
}
var dbFactory = context.HttpContext.RequestServices.GetRequiredService<IDbContextFactory<SlipItInDbContext>>();
await using var db = await dbFactory.CreateDbContextAsync();
var userExists = await db.Users.AnyAsync(u => u.Id == userId && u.IsActive);
if (!userExists)
{
context.Fail("User no longer exists or is inactive.");
}
} }
}; };
}); });

View File

@@ -35,17 +35,27 @@ public partial class App : Application
var authSession = ServiceHelper.GetRequiredService<IAuthSessionService>(); var authSession = ServiceHelper.GetRequiredService<IAuthSessionService>();
var signalR = ServiceHelper.GetRequiredService<ISignalRService>(); var signalR = ServiceHelper.GetRequiredService<ISignalRService>();
var gameStateService = ServiceHelper.GetRequiredService<IGameStateService>(); var gameStateService = ServiceHelper.GetRequiredService<IGameStateService>();
var apiService = ServiceHelper.GetRequiredService<IApiService>();
await authSession.InitializeAsync(); await authSession.InitializeAsync();
await gameStateService.InitializeAsync(); await gameStateService.InitializeAsync();
if (!string.IsNullOrWhiteSpace(authSession.AccessToken)) if (string.IsNullOrWhiteSpace(authSession.AccessToken))
return;
var isSessionValid = await apiService.ValidateSessionAsync();
if (!isSessionValid)
{ {
await authSession.ClearSessionAsync();
await gameStateService.ClearLocalGameDataAsync();
await signalR.DisconnectAsync();
return;
}
var connected = await signalR.ConnectAsync(authSession.AccessToken); var connected = await signalR.ConnectAsync(authSession.AccessToken);
if (connected) if (connected)
await gameStateService.ResyncAsync(); await gameStateService.ResyncAsync();
} }
}
catch catch
{ {
// Start darf nicht abstürzen, wenn Session/Netzwerk fehlschlägt. // Start darf nicht abstürzen, wenn Session/Netzwerk fehlschlägt.

View File

@@ -28,6 +28,23 @@ public class ApiService : IApiService
public Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default) public Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default)
=> PostAuthAsync("api/auth/login", request, cancellationToken); => PostAuthAsync("api/auth/login", request, cancellationToken);
public async Task<bool> ValidateSessionAsync(CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(_authSession.AccessToken))
return false;
try
{
AttachAuthHeader();
var response = await _httpClient.GetAsync("api/auth/me", cancellationToken);
return response.IsSuccessStatusCode;
}
catch
{
return false;
}
}
public Task LogoutAsync() => _authSession.ClearSessionAsync(); public Task LogoutAsync() => _authSession.ClearSessionAsync();
private async Task<AuthResponseDto> PostAuthAsync<TRequest>(string endpoint, TRequest request, CancellationToken cancellationToken) private async Task<AuthResponseDto> PostAuthAsync<TRequest>(string endpoint, TRequest request, CancellationToken cancellationToken)

View File

@@ -6,5 +6,6 @@ public interface IApiService
{ {
Task<AuthResponseDto> RegisterAsync(RegisterRequestDto request, CancellationToken cancellationToken = default); Task<AuthResponseDto> RegisterAsync(RegisterRequestDto request, CancellationToken cancellationToken = default);
Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default); Task<AuthResponseDto> LoginAsync(LoginRequestDto request, CancellationToken cancellationToken = default);
Task<bool> ValidateSessionAsync(CancellationToken cancellationToken = default);
Task LogoutAsync(); Task LogoutAsync();
} }

View File

@@ -42,7 +42,11 @@ public partial class LoginViewModel : BaseViewModel
}); });
await _authSession.SetSessionAsync(response); await _authSession.SetSessionAsync(response);
await _signalR.ConnectAsync(response.Token);
var connected = await _signalR.ConnectAsync(response.Token);
if (!connected)
throw new InvalidOperationException("Verbindung zum Spielserver fehlgeschlagen.");
await _gameStateService.InitializeAsync(); await _gameStateService.InitializeAsync();
await Shell.Current.GoToAsync(nameof(LobbyPage)); await Shell.Current.GoToAsync(nameof(LobbyPage));
}, "Anmeldung läuft..."); }, "Anmeldung läuft...");
@@ -51,11 +55,24 @@ public partial class LoginViewModel : BaseViewModel
[RelayCommand] [RelayCommand]
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
bool isAuthenticated = _authSession.IsAuthenticated; if (!_authSession.IsAuthenticated)
return;
if (isAuthenticated) var isSessionValid = await _apiService.ValidateSessionAsync();
if (!isSessionValid)
{ {
await _authSession.ClearSessionAsync();
await _gameStateService.ClearLocalGameDataAsync();
await _signalR.DisconnectAsync();
return;
}
var token = _authSession.AccessToken;
if (string.IsNullOrWhiteSpace(token))
return;
var connected = await _signalR.ConnectAsync(token);
if (connected)
await Shell.Current.GoToAsync(nameof(LobbyPage)); await Shell.Current.GoToAsync(nameof(LobbyPage));
} }
} }
}

View File

@@ -20,7 +20,7 @@
<CollectionView ItemsSource="{Binding MyCards}"> <CollectionView ItemsSource="{Binding MyCards}">
<CollectionView.ItemTemplate> <CollectionView.ItemTemplate>
<DataTemplate> <DataTemplate>
<Frame Margin="0,4" Padding="10"> <Border Margin="0,4" Padding="10" Stroke="LightGray" StrokeThickness="1" StrokeShape="RoundRectangle 10" BackgroundColor="White">
<VerticalStackLayout> <VerticalStackLayout>
<Label Text="{Binding Text}" /> <Label Text="{Binding Text}" />
<HorizontalStackLayout> <HorizontalStackLayout>
@@ -28,7 +28,7 @@
<Button Text="Beschuldigen" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ChallengeCardCommand}" CommandParameter="{Binding .}" /> <Button Text="Beschuldigen" Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.ChallengeCardCommand}" CommandParameter="{Binding .}" />
</HorizontalStackLayout> </HorizontalStackLayout>
</VerticalStackLayout> </VerticalStackLayout>
</Frame> </Border>
</DataTemplate> </DataTemplate>
</CollectionView.ItemTemplate> </CollectionView.ItemTemplate>
</CollectionView> </CollectionView>