Skip to content

Commit bfee048

Browse files
authored
Merge pull request #113 from azegallo/master
Fix token refresh logic
2 parents e136a55 + 3fbd3bd commit bfee048

File tree

5 files changed

+88
-104
lines changed

5 files changed

+88
-104
lines changed

Gotrue/Client.cs

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ public async Task<ResetPasswordForEmailState> ResetPasswordForEmail(ResetPasswor
488488

489489
await RefreshToken();
490490

491-
var user = await _api.GetUser(CurrentSession.AccessToken!);
491+
var user = await _api.GetUser(CurrentSession.AccessToken);
492492
CurrentSession.User = user;
493493

494494
return CurrentSession;
@@ -518,14 +518,18 @@ public async Task<Session> SetSession(string accessToken, string refreshToken, b
518518
NotifyAuthStateChange(SignedIn);
519519
return CurrentSession;
520520
}
521-
521+
522+
var iat = payload.IssuedAt;
523+
var exp = payload.ValidTo;
524+
var expiresIn = (long)(exp - iat).TotalSeconds;
525+
522526
CurrentSession = new Session
523527
{
524528
AccessToken = accessToken,
525529
RefreshToken = refreshToken,
526530
TokenType = "bearer",
527-
ExpiresIn = payload.Expiration!.Value,
528-
User = await _api.GetUser(accessToken)
531+
ExpiresIn = expiresIn,
532+
User = await _api.GetUser(accessToken),
529533
};
530534

531535
NotifyAuthStateChange(SignedIn);
@@ -574,7 +578,7 @@ public async Task<Session> SetSession(string accessToken, string refreshToken, b
574578
ExpiresIn = long.Parse(expiresIn),
575579
RefreshToken = refreshToken,
576580
TokenType = tokenType,
577-
User = user
581+
User = user,
578582
};
579583

580584
if (storeSession)
@@ -595,14 +599,6 @@ public async Task<Session> SetSession(string accessToken, string refreshToken, b
595599
if (CurrentSession == null)
596600
return null;
597601

598-
// Check to see if the session has expired. If so go ahead and destroy it.
599-
if (CurrentSession != null && CurrentSession.Expired())
600-
{
601-
_debugNotification?.Log($"Loaded session has expired");
602-
DestroySession();
603-
return null;
604-
}
605-
606602
// If we aren't online, we can't refresh the token
607603
if (!Online)
608604
{
@@ -691,16 +687,28 @@ private void DestroySession()
691687
/// <inheritdoc />
692688
public async Task RefreshToken(string accessToken, string refreshToken)
693689
{
690+
if (!Online)
691+
throw new GotrueException("Only supported when online", Offline);
692+
694693
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
695694
throw new GotrueException("No token provided", NoSessionFound);
696695

697-
var result = await _api.RefreshAccessToken(accessToken, refreshToken);
698-
699-
if (result == null || string.IsNullOrEmpty(result.AccessToken))
700-
throw new GotrueException("Could not refresh token from provided session.", NoSessionFound);
696+
try
697+
{
698+
var result = await _api.RefreshAccessToken(accessToken, refreshToken);
701699

702-
CurrentSession = result;
703-
NotifyAuthStateChange(TokenRefreshed);
700+
if (result == null || string.IsNullOrEmpty(result.AccessToken))
701+
throw new GotrueException("Could not refresh token from provided session.", NoSessionFound);
702+
703+
CurrentSession = result;
704+
NotifyAuthStateChange(TokenRefreshed);
705+
}
706+
catch (GotrueException ex) when (ex.Reason is InvalidRefreshToken)
707+
{
708+
DestroySession();
709+
NotifyAuthStateChange(SignedOut);
710+
throw;
711+
}
704712
}
705713

706714
/// <inheritdoc />
@@ -712,17 +720,22 @@ public async Task RefreshToken()
712720
if (CurrentSession == null || string.IsNullOrEmpty(CurrentSession?.AccessToken) || string.IsNullOrEmpty(CurrentSession?.RefreshToken))
713721
throw new GotrueException("No current session.", NoSessionFound);
714722

715-
if (CurrentSession!.Expired())
716-
throw new GotrueException("Session expired", ExpiredRefreshToken);
717-
718-
var result = await _api.RefreshAccessToken(CurrentSession.AccessToken!, CurrentSession.RefreshToken!);
719-
720-
if (result == null || string.IsNullOrEmpty(result.AccessToken))
721-
throw new GotrueException("Could not refresh token from provided session.", NoSessionFound);
723+
try
724+
{
725+
var result = await _api.RefreshAccessToken(CurrentSession.AccessToken!, CurrentSession.RefreshToken!);
726+
if (result == null || string.IsNullOrEmpty(result.AccessToken))
727+
throw new GotrueException("Could not refresh token from provided session.", NoSessionFound);
722728

723-
CurrentSession = result;
729+
CurrentSession = result;
724730

725-
NotifyAuthStateChange(TokenRefreshed);
731+
NotifyAuthStateChange(TokenRefreshed);
732+
}
733+
catch (GotrueException ex) when (ex.Reason is InvalidRefreshToken)
734+
{
735+
DestroySession();
736+
NotifyAuthStateChange(SignedOut);
737+
throw;
738+
}
726739
}
727740

728741

@@ -791,7 +804,7 @@ public void Shutdown()
791804

792805
if (result == null || string.IsNullOrEmpty(result.AccessToken))
793806
throw new GotrueException("Could not verify MFA.", MfaChallengeUnverified);
794-
807+
795808
var session = new Session
796809
{
797810
AccessToken = result.AccessToken,
@@ -835,14 +848,14 @@ public void Shutdown()
835848

836849
if (result == null || string.IsNullOrEmpty(result.AccessToken))
837850
throw new GotrueException("Could not verify MFA.", MfaChallengeUnverified);
838-
851+
839852
var session = new Session
840853
{
841854
AccessToken = result.AccessToken,
842855
RefreshToken = result.RefreshToken,
843856
TokenType = "bearer",
844857
ExpiresIn = result.ExpiresIn,
845-
User = result.User
858+
User = result.User,
846859
};
847860

848861
UpdateSession(session);

Gotrue/Session.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class Session
1717
public string? AccessToken { get; set; }
1818

1919
/// <summary>
20-
/// The number of seconds until the token expires (since it was issued). Returned when a login is confirmed.
20+
/// The number of seconds until the access token expires (since it was issued). Returned when a login is confirmed.
2121
/// </summary>
2222
[JsonProperty("expires_in")]
2323
public long ExpiresIn { get; set; }
@@ -49,17 +49,5 @@ public class Session
4949

5050
[JsonProperty("created_at")]
5151
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
52-
53-
/// <summary>
54-
/// The expiration date of this session, in UTC time.
55-
/// </summary>
56-
/// <returns></returns>
57-
public DateTime ExpiresAt() => new DateTimeOffset(CreatedAt).AddSeconds(ExpiresIn).ToUniversalTime().DateTime;
58-
59-
/// <summary>
60-
/// Returns true if the session has expired
61-
/// </summary>
62-
/// <returns></returns>
63-
public bool Expired() => ExpiresAt() < DateTime.UtcNow;
6452
}
6553
}

Gotrue/TokenRefresh.cs

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
using System.IdentityModel.Tokens.Jwt;
23
using System.Threading;
4+
using Supabase.Gotrue.Exceptions;
35
using Supabase.Gotrue.Interfaces;
46
using static Supabase.Gotrue.Constants.AuthState;
57

@@ -46,10 +48,11 @@ public void ManageAutoRefresh(IGotrueClient<User, Session> sender, Constants.Aut
4648
case SignedIn:
4749
if (Debug)
4850
_client.Debug("Refresh Timer started");
49-
InitRefreshTimer();
51+
CreateNewTimer();
5052
// Turn on auto-refresh timer
5153
break;
5254
case SignedOut:
55+
case Shutdown:
5356
if (Debug)
5457
_client.Debug("Refresh Timer stopped");
5558
_refreshTimer?.Dispose();
@@ -58,32 +61,17 @@ public void ManageAutoRefresh(IGotrueClient<User, Session> sender, Constants.Aut
5861
case UserUpdated:
5962
if (Debug)
6063
_client.Debug("Refresh Timer restarted");
61-
InitRefreshTimer();
64+
CreateNewTimer();
6265
break;
6366
case PasswordRecovery:
64-
// Doesn't affect auto refresh
65-
break;
6667
case TokenRefreshed:
68+
case MfaChallengeVerified:
6769
// Doesn't affect auto refresh
6870
break;
69-
case Shutdown:
70-
if (Debug)
71-
_client.Debug("Refresh Timer stopped");
72-
_refreshTimer?.Dispose();
73-
// Turn off auto-refresh timer
74-
break;
7571
default: throw new ArgumentOutOfRangeException(nameof(stateChanged), stateChanged, null);
7672
}
7773
}
7874

79-
/// <summary>
80-
/// Sets up the auto-refresh timer
81-
/// </summary>
82-
private void InitRefreshTimer()
83-
{
84-
CreateNewTimer();
85-
}
86-
8775
/// <summary>
8876
/// The timer calls this method at the configured interval to refresh the token.
8977
///
@@ -119,29 +107,21 @@ private async void HandleRefreshTimerTick(object _)
119107
/// </summary>
120108
private void CreateNewTimer()
121109
{
122-
if (_client.CurrentSession == null || _client.CurrentSession.ExpiresIn == default)
110+
if (_client.CurrentSession == null)
123111
{
124112
if (Debug)
125113
_client.Debug($"No session, refresh timer not started");
126114
return;
127115
}
128116

129-
if (_client.CurrentSession.Expired())
130-
{
131-
if (Debug)
132-
_client.Debug($"Token expired, signing out");
133-
_client.NotifyAuthStateChange(SignedOut);
134-
return;
135-
}
136-
137117
try
138118
{
139-
TimeSpan interval = GetInterval();
119+
TimeSpan refreshDueTime = GetSecondsUntilNextRefresh();
140120
_refreshTimer?.Dispose();
141-
_refreshTimer = new Timer(HandleRefreshTimerTick, null, interval, Timeout.InfiniteTimeSpan);
121+
_refreshTimer = new Timer(HandleRefreshTimerTick, null, refreshDueTime, Timeout.InfiniteTimeSpan);
142122

143123
if (Debug)
144-
_client.Debug($"Refresh timer scheduled {interval.TotalMinutes} minutes");
124+
_client.Debug($"Refresh timer scheduled {refreshDueTime.TotalMinutes} minutes");
145125
}
146126
catch (Exception e)
147127
{
@@ -151,23 +131,35 @@ private void CreateNewTimer()
151131
}
152132

153133
/// <summary>
154-
/// Interval should be t - (1/5(n)) (i.e. if session time (t) 3600s, attempt refresh at 2880s or 720s (1/5) seconds before expiration)
134+
/// Returns remaining seconds until the access token should be refreshed.
135+
/// Interval is calculated as:<code>t - (1/5(n))</code> (i.e. if session time (t) 3600s, attempt refresh at 2880s or 720s (1/5) seconds before expiration).
136+
/// <remarks>
137+
/// - The maximum refresh wait time is clamped to <see cref="ClientOptions.MaximumRefreshWaitTime"/>
138+
/// </remarks>
139+
/// <remarks>
140+
/// - If the access token is expired it will refresh immediately.
141+
/// </remarks>
155142
/// </summary>
156-
private TimeSpan GetInterval()
143+
/// <returns>The remaining seconds until the token should be refreshed</returns>
144+
private TimeSpan GetSecondsUntilNextRefresh()
157145
{
158-
if (_client.CurrentSession == null || _client.CurrentSession.ExpiresIn == default)
146+
if (_client.CurrentSession is null || _client.CurrentSession.AccessToken == null)
159147
{
160148
return TimeSpan.Zero;
161149
}
162150

163-
var interval = (long)Math.Floor(_client.CurrentSession.ExpiresIn * 4.0f / 5.0f);
164-
165-
var timeoutSeconds = Convert.ToInt64((_client.CurrentSession.CreatedAt.AddSeconds(interval) - DateTime.UtcNow).TotalSeconds);
151+
var interval = (long)Math.Floor(_client.CurrentSession.ExpiresIn * 4.0 / 5.0);
152+
var refreshAt = _client.CurrentSession.CreatedAt.AddSeconds(interval);
166153

167-
if (timeoutSeconds > _client.Options.MaximumRefreshWaitTime)
168-
timeoutSeconds = _client.Options.MaximumRefreshWaitTime;
169-
170-
return TimeSpan.FromSeconds(timeoutSeconds);
154+
var secondsUntilNextRefresh = Convert.ToInt64((refreshAt - DateTime.UtcNow).TotalSeconds);
155+
156+
if (secondsUntilNextRefresh < 0)
157+
return TimeSpan.Zero;
158+
159+
if (secondsUntilNextRefresh > _client.Options.MaximumRefreshWaitTime)
160+
secondsUntilNextRefresh = _client.Options.MaximumRefreshWaitTime;
161+
162+
return TimeSpan.FromSeconds(secondsUntilNextRefresh);
171163
}
172164
}
173165
}

GotrueTests/AnonKeyClientFailureTests.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ public async Task ClientTriggersTokenRefreshedEvent()
125125
{
126126
await _client.RefreshSession();
127127
});
128+
128129
AreEqual(InvalidRefreshToken, x.Reason);
130+
IsNull(_client.CurrentSession);
129131
}
130132

131133
[TestMethod("Client: expired token")]
@@ -138,18 +140,17 @@ public async Task ExpiredTokenTest()
138140
IsNotNull(emailSession.RefreshToken);
139141
IsNotNull(emailSession.User);
140142

143+
// Set CreatedAt to an old date - this should NOT prevent refresh from working
144+
// Session "expiration" based on CreatedAt is about access token lifetime, not refresh token validity
145+
_client.CurrentSession.CreatedAt = DateTime.UtcNow.AddDays(-10);
146+
147+
// Refresh should still succeed with a valid refresh token
141148
await _client.RefreshSession();
142-
143-
IsNotNull(emailSession.AccessToken);
144-
IsNotNull(emailSession.RefreshToken);
145-
IsNotNull(emailSession.User);
146149

147-
_client.CurrentSession.CreatedAt = DateTime.UtcNow.AddDays(-10);
148-
var x = await ThrowsExceptionAsync<GotrueException>(async () =>
149-
{
150-
await _client.RefreshSession();
151-
});
152-
AreEqual(ExpiredRefreshToken, x.Reason);
150+
IsNotNull(_client.CurrentSession);
151+
IsNotNull(_client.CurrentSession.AccessToken);
152+
IsNotNull(_client.CurrentSession.RefreshToken);
153+
IsNotNull(_client.CurrentSession.User);
153154
}
154155

155156
[TestMethod("Client: Send Reset Password Email for unknown email")]

GotrueTests/AnonKeyClientTests.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -481,15 +481,5 @@ public async Task ClientCanSetSession()
481481
// As this is being forced to regenerate, the original should be different than the cached.
482482
AreNotEqual(refreshToken, _client.CurrentSession.RefreshToken);
483483
}
484-
485-
[TestMethod("Session: `ExpiresAt` is Calculated Correctly.")]
486-
public async Task SessionCalculatesExpiresAtCorrectly()
487-
{
488-
var email = $"{RandomString(12)}@supabase.io";
489-
var session = await _client.SignUp(email, PASSWORD);
490-
491-
IsFalse(session.Expired());
492-
AreEqual(session.ExpiresAt().Ticks, session.CreatedAt.ToUniversalTime().AddSeconds(session.ExpiresIn).Ticks);
493-
}
494484
}
495485
}

0 commit comments

Comments
 (0)