Skip to content

Commit 5743bfc

Browse files
committed
Fix intermittent login loop caused by timezone and race condition bugs
- Fix DateTime parsing to preserve UTC timezone from ISO 8601 'Z' suffix using DateTimeOffset.TryParse with DateTimeStyles.RoundtripKind (TokenStorageService.cs, GitHubCallback.razor) - Remove forceLoad: true from Login.razor to prevent race condition - Fix _isInitialized one-shot failure in LrmAuthStateProvider - only mark as initialized if user fetch succeeds, allowing retry on failure
1 parent 586c9e9 commit 5743bfc

4 files changed

Lines changed: 20 additions & 10 deletions

File tree

cloud/src/LrmCloud.Web/Pages/Auth/GitHubCallback.razor

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@page "/auth/github/callback"
22
@layout AuthLayout
3+
@using System.Globalization
34
@inject AuthService AuthService
45
@inject NavigationManager Navigation
56
@inject IJSRuntime JS
@@ -62,21 +63,21 @@
6263
return;
6364
}
6465

65-
// Parse dates
66-
if (!DateTime.TryParse(Uri.UnescapeDataString(expiresAtStr), out var expiresAt) ||
67-
!DateTime.TryParse(Uri.UnescapeDataString(refreshTokenExpiresAtStr), out var refreshTokenExpiresAt))
66+
// Parse dates - use RoundtripKind to preserve UTC timezone from ISO 8601 "Z" suffix
67+
if (!DateTimeOffset.TryParse(Uri.UnescapeDataString(expiresAtStr), null, DateTimeStyles.RoundtripKind, out var expiresAtOffset) ||
68+
!DateTimeOffset.TryParse(Uri.UnescapeDataString(refreshTokenExpiresAtStr), null, DateTimeStyles.RoundtripKind, out var refreshTokenExpiresAtOffset))
6869
{
6970
_errorMessage = "Invalid token expiration data. Please try again.";
7071
StateHasChanged();
7172
return;
7273
}
7374

74-
// Process the tokens
75+
// Process the tokens (convert to UTC DateTime)
7576
var result = await AuthService.ProcessGitHubCallbackAsync(
7677
Uri.UnescapeDataString(token),
77-
expiresAt,
78+
expiresAtOffset.UtcDateTime,
7879
Uri.UnescapeDataString(refreshToken),
79-
refreshTokenExpiresAt);
80+
refreshTokenExpiresAtOffset.UtcDateTime);
8081

8182
if (result.IsSuccess)
8283
{

cloud/src/LrmCloud.Web/Pages/Auth/Login.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@
103103

104104
if (result.IsSuccess)
105105
{
106-
Navigation.NavigateTo(ReturnUrl ?? "", forceLoad: true);
106+
// Don't use forceLoad: true - causes race condition with localStorage persistence
107+
Navigation.NavigateTo(ReturnUrl ?? "");
107108
}
108109
else
109110
{

cloud/src/LrmCloud.Web/Services/LrmAuthStateProvider.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ public override async Task<AuthenticationState> GetAuthenticationStateAsync()
7171
// Try to get user info from cache or fetch from API
7272
if (_cachedUser == null && !_isInitialized)
7373
{
74-
_isInitialized = true;
7574
_cachedUser = await FetchCurrentUserAsync();
75+
// Only mark as initialized if fetch succeeded - allows retry on failure
76+
_isInitialized = _cachedUser != null;
7677
}
7778

7879
if (_cachedUser == null)

cloud/src/LrmCloud.Web/Services/TokenStorageService.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Globalization;
12
using Blazored.LocalStorage;
23

34
namespace LrmCloud.Web.Services;
@@ -41,14 +42,20 @@ public async Task StoreTokensAsync(string accessToken, string refreshToken, Date
4142
{
4243
var expiry = await _localStorage.GetItemAsStringAsync(TokenExpiryKey);
4344
if (string.IsNullOrEmpty(expiry)) return null;
44-
return DateTime.TryParse(expiry, out var dt) ? dt : null;
45+
// Use RoundtripKind to preserve UTC timezone from ISO 8601 "Z" suffix
46+
return DateTimeOffset.TryParse(expiry, null, DateTimeStyles.RoundtripKind, out var dt)
47+
? dt.UtcDateTime
48+
: null;
4549
}
4650

4751
public async Task<DateTime?> GetRefreshExpiryAsync()
4852
{
4953
var expiry = await _localStorage.GetItemAsStringAsync(RefreshExpiryKey);
5054
if (string.IsNullOrEmpty(expiry)) return null;
51-
return DateTime.TryParse(expiry, out var dt) ? dt : null;
55+
// Use RoundtripKind to preserve UTC timezone from ISO 8601 "Z" suffix
56+
return DateTimeOffset.TryParse(expiry, null, DateTimeStyles.RoundtripKind, out var dt)
57+
? dt.UtcDateTime
58+
: null;
5259
}
5360

5461
public async Task<bool> IsTokenExpiredAsync()

0 commit comments

Comments
 (0)