Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gotrue, supabase_flutter): New auth token refresh algorithm #879

Merged
merged 25 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6cbcbdd
align _callRefreshSession
dshukertjr Mar 10, 2024
a9d5228
complete fetch rework
dshukertjr Mar 11, 2024
2fa4139
Update the methods on gotrue-dart
dshukertjr Mar 22, 2024
64c7559
update the app lifecycle change listener
dshukertjr Mar 22, 2024
1cdf63e
Complete ticking behavior except tests
dshukertjr Apr 2, 2024
151e046
minor comment update
dshukertjr Apr 2, 2024
4d7cf9c
update the behavior when the SDK failed to refresh the token when mak…
dshukertjr Apr 2, 2024
0d3c00e
adjust gotrue tests
dshukertjr Apr 2, 2024
21f60cb
fix supabase test
dshukertjr Apr 2, 2024
b7b9199
fix supabase_flutter tests
dshukertjr Apr 2, 2024
23aa612
Fix supabase-flutter test
dshukertjr Apr 2, 2024
fe5c2ee
stop autorefresh timer within widget test
dshukertjr Apr 2, 2024
01e46d9
Update packages/gotrue/lib/src/constants.dart
dshukertjr Apr 3, 2024
dfd6458
Update packages/gotrue/lib/src/gotrue_client.dart
dshukertjr Apr 3, 2024
b89eef4
Update packages/gotrue/lib/src/gotrue_client.dart
dshukertjr Apr 3, 2024
be4e903
Update packages/gotrue/lib/src/gotrue_client.dart
dshukertjr Apr 3, 2024
ea404b1
Update packages/gotrue/lib/src/gotrue_client.dart
dshukertjr Apr 3, 2024
c49e232
Update packages/gotrue/lib/src/gotrue_client.dart
dshukertjr Apr 3, 2024
1930ac3
minor refactor within _autoRefreshTokenTick
dshukertjr Apr 3, 2024
668b4ed
rename errors to exceptions
dshukertjr Apr 3, 2024
0058223
style: remove redundant bracket
Vinzent03 Apr 5, 2024
711dfd7
Have supabase_flutter respect the autoRefreshToken option for refresh…
dshukertjr Apr 7, 2024
eece78a
remove outdated comment on _callRefreshToken
dshukertjr Apr 7, 2024
f7cbb0a
Add error to onAuthStateChange stream for call refresh token
dshukertjr Apr 7, 2024
b03e8a5
startAutoRefresh within gotrue
dshukertjr Apr 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/supabase_flutter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ jobs:
with:
java-version: '12.x'

- uses: subosito/flutter-action@v1
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ matrix.flutter-version }}
channel: 'stable'

- run: flutter --version

- name: Bootstrap workspace
run: |
cd ../../
Expand All @@ -64,6 +66,7 @@ jobs:
run: dart format lib test -l 80 --set-exit-if-changed

- name: analyzer
if: ${{ matrix.sdk == '3.x'}}
run: flutter analyze --fatal-warnings --fatal-infos .

- name: Run tests
Expand Down
12 changes: 9 additions & 3 deletions packages/gotrue/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ class Constants {

/// storage key prefix to store code verifiers
static const String defaultStorageKey = 'supabase.auth.token';
static const expiryMargin = Duration(seconds: 10);
static const int maxRetryCount = 10;
static const retryInterval = Duration(milliseconds: 200);

/// The margin to use when checking if a token is expired.
static const expiryMargin = Duration(seconds: 30);

/// Current session will be checked for refresh at this interval.
static const autoRefreshTickDuration = Duration(seconds: 10);

/// A token refresh will be attempted this many ticks before the current session expires.
static const autoRefreshTickThreshold = 3;
}

enum AuthChangeEvent {
Expand Down
154 changes: 103 additions & 51 deletions packages/gotrue/lib/src/fetch.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:gotrue/src/types/auth_exception.dart';
import 'package:gotrue/src/types/fetch_options.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';

enum RequestMethodType { get, post, put, delete }
Expand All @@ -16,23 +16,56 @@ class GotrueFetch {
return code >= 200 && code <= 299;
}

AuthException _handleError(http.Response error) {
late AuthException errorRes;
String _getErrorMessage(dynamic error) {
if (error is Map) {
return error['msg'] ??
error['message'] ??
error['error_description'] ??
error['error']?.toString() ??
error.toString();
}

return error.toString();
}

AuthException _handleError(dynamic error) {
if (error is! Response) {
throw AuthRetryableFetchException();
}

// If the status is 500 or above, it's likely a server error,
// and can be retried.
if (error.statusCode >= 500) {
throw AuthRetryableFetchException();
}

final dynamic data;
try {
final parsedJson = json.decode(error.body) as Map<String, dynamic>;
final String message = (parsedJson['msg'] ??
parsedJson['message'] ??
parsedJson['error_description'] ??
parsedJson['error'] ??
error.body)
.toString();
errorRes = AuthException(message, statusCode: '${error.statusCode}');
} catch (_) {
errorRes = AuthException(error.body, statusCode: '${error.statusCode}');
data = jsonDecode(error.body);
} catch (error) {
throw AuthUnknownException(
message: error.toString(), originalError: error);
}

// Check if weak password reasons only contain strings
if (data is Map &&
data['weak_password'] is Map &&
data['weak_password']['reasons'] is List &&
(data['weak_password']['reasons'] as List).isNotEmpty &&
(data['weak_password']['reasons'] as List)
.whereNot((element) => element is String)
.isEmpty) {
throw AuthWeakPasswordException(
message: _getErrorMessage(data),
statusCode: error.statusCode.toString(),
reasons: data['weak_password']['reasons'],
);
}

return errorRes;
throw AuthApiException(
_getErrorMessage(data),
statusCode: error.statusCode.toString(),
);
}

Future<dynamic> request(
Expand All @@ -51,52 +84,71 @@ class GotrueFetch {
}
Uri uri = Uri.parse(url);
uri = uri.replace(queryParameters: {...uri.queryParameters, ...qs});
late final http.Response response;

return await _handleRequest(
method: method, uri: uri, options: options, headers: headers);
}

Future<dynamic> _handleRequest({
required RequestMethodType method,
required Uri uri,
required GotrueRequestOptions? options,
required Map<String, String> headers,
}) async {
final bodyStr = json.encode(options?.body ?? {});

if (method != RequestMethodType.get) {
headers['Content-Type'] = 'application/json';
}
switch (method) {
case RequestMethodType.get:
response = await (httpClient?.get ?? http.get)(
uri,
headers: headers,
);

break;
case RequestMethodType.post:
response = await (httpClient?.post ?? http.post)(
uri,
headers: headers,
body: bodyStr,
);
break;
case RequestMethodType.put:
response = await (httpClient?.put ?? http.put)(
uri,
headers: headers,
body: bodyStr,
);
break;
case RequestMethodType.delete:
response = await (httpClient?.delete ?? http.delete)(
uri,
headers: headers,
body: bodyStr,
);
break;
}
Response response;
try {
switch (method) {
case RequestMethodType.get:
response = await (httpClient?.get ?? get)(
uri,
headers: headers,
);

if (isSuccessStatusCode(response.statusCode)) {
if (options?.noResolveJson == true) {
return response.body;
} else {
return json.decode(utf8.decode(response.bodyBytes));
break;
case RequestMethodType.post:
response = await (httpClient?.post ?? post)(
uri,
headers: headers,
body: bodyStr,
);
break;
case RequestMethodType.put:
response = await (httpClient?.put ?? put)(
uri,
headers: headers,
body: bodyStr,
);
break;
case RequestMethodType.delete:
response = await (httpClient?.delete ?? delete)(
uri,
headers: headers,
body: bodyStr,
);
break;
}
} else {
} catch (e) {
// fetch failed, likely due to a network or CORS error
throw AuthRetryableFetchException();
}

if (!isSuccessStatusCode(response.statusCode)) {
throw _handleError(response);
}

if (options?.noResolveJson == true) {
return response.body;
}

try {
return json.decode(utf8.decode(response.bodyBytes));
} catch (error) {
throw _handleError(error);
}
}
}
Loading
Loading