Files
PlaylistShared/PlaylistShared.Pwa/Services/AuthStateProvider.cs

180 lines
6.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.AspNetCore.Components.Authorization;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
namespace PlaylistShared.Pwa.Services;
public class AuthStateProvider : AuthenticationStateProvider, IDisposable
{
private readonly TokenStorage _tokenStorage;
private readonly ApiClient _apiClient;
private readonly HttpClient _http;
private Timer? _refreshTimer;
private ClaimsPrincipal _currentUser = new(new ClaimsIdentity());
private string? _currentToken;
private string? _currentRefreshToken;
public AuthStateProvider(TokenStorage tokenStorage, ApiClient apiClient, HttpClient http)
{
_tokenStorage = tokenStorage;
_apiClient = apiClient;
_http = http;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var (token, refreshToken) = await _tokenStorage.GetTokensAsync();
if (string.IsNullOrEmpty(token))
return new AuthenticationState(_currentUser);
var (isValid, principal) = await ValidateTokenAsync(token);
if (isValid && principal != null)
{
_currentUser = principal;
_currentToken = token;
_currentRefreshToken = refreshToken;
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
ScheduleTokenRefresh(token, refreshToken);
return new AuthenticationState(principal);
}
// Токен невалиден пробуем обновить
if (!string.IsNullOrEmpty(refreshToken))
{
var newTokenResponse = await _apiClient.RefreshTokenAsync(refreshToken);
if (newTokenResponse != null && !string.IsNullOrEmpty(newTokenResponse.Token))
{
await MarkUserAsAuthenticated(newTokenResponse.Token, newTokenResponse.RefreshToken);
// После MarkUserAsAuthenticated состояние обновится через NotifyAuthenticationStateChanged,
// но текущий вызов всё равно должен вернуть нового пользователя
var (newIsValid, newPrincipal) = await ValidateTokenAsync(newTokenResponse.Token);
if (newIsValid && newPrincipal != null)
return new AuthenticationState(newPrincipal);
}
}
// Всё плохо — логаут
await MarkUserAsLoggedOut();
return new AuthenticationState(_currentUser);
}
// Вспомогательный метод проверки валидности токена (включая срок)
private async Task<(bool IsValid, ClaimsPrincipal? Principal)> ValidateTokenAsync(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
// Проверяем, не истёк ли токен
if (jwt.ValidTo < DateTime.UtcNow)
{
return (false, null);
}
var identity = new ClaimsIdentity(jwt.Claims, "jwt");
var principal = new ClaimsPrincipal(identity);
return (true, principal);
}
catch
{
return (false, null);
}
}
public async Task MarkUserAsAuthenticated(string token, string refreshToken)
{
await _tokenStorage.SetTokensAsync(token, refreshToken);
var principal = ParseToken(token);
_currentUser = principal ?? new ClaimsPrincipal(new ClaimsIdentity());
_currentToken = token;
_currentRefreshToken = refreshToken;
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task MarkUserAsLoggedOut()
{
await _tokenStorage.ClearTokensAsync();
_currentUser = new ClaimsPrincipal(new ClaimsIdentity());
_currentToken = null;
_currentRefreshToken = null;
_http.DefaultRequestHeaders.Authorization = null;
_refreshTimer?.Dispose();
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task<string?> TryRefreshTokenAsync()
{
if (string.IsNullOrEmpty(_currentRefreshToken))
return null;
try
{
var newToken = await _apiClient.RefreshTokenAsync(_currentRefreshToken);
if (newToken != null && !string.IsNullOrEmpty(newToken.Token))
{
await MarkUserAsAuthenticated(newToken.Token, newToken.RefreshToken);
return newToken.Token;
}
else
{
await MarkUserAsLoggedOut();
return null;
}
}
catch
{
await MarkUserAsLoggedOut();
return null;
}
}
public bool IsTokenExpiringSoon()
{
if (string.IsNullOrEmpty(_currentToken))
return false;
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(_currentToken);
var expiresAt = jwt.ValidTo;
var timeToExpiry = expiresAt - DateTime.UtcNow;
return timeToExpiry < TimeSpan.FromMinutes(1);
}
private ClaimsPrincipal? ParseToken(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
var identity = new ClaimsIdentity(jwt.Claims, "jwt");
return new ClaimsPrincipal(identity);
}
catch
{
return null;
}
}
private void ScheduleTokenRefresh(string token, string? refreshToken)
{
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
var expiresAt = jwt.ValidTo;
var timeToExpiry = expiresAt - DateTime.UtcNow;
var refreshTime = timeToExpiry - TimeSpan.FromMinutes(5);
if (refreshTime > TimeSpan.Zero && !string.IsNullOrEmpty(refreshToken))
{
_refreshTimer?.Dispose();
_refreshTimer = new Timer(async _ =>
{
await TryRefreshTokenAsync();
}, null, (int)refreshTime.TotalMilliseconds, Timeout.Infinite);
}
}
public void Dispose() => _refreshTimer?.Dispose();
}