Skip to content

Commit 4e0270a

Browse files
lawinskidshukertjr
andauthored
fix: Added missing error codes for AuthException (#995)
* Add auth error codes enum * Add error code parameter for the auth exception * Handle error_code parameter * Add error_code for the getSessionFromUrl exception * Add basic asserts for error_codes * Replace AuthException with AuthSessionMissingException * Adjust logic to JS implementation. - Introduce api versioning - Change AuthErrorCode to ErrorCode * more alignment with the JS SDK * fix: use docker compose v2 * update auth server version and fix failing tests * fix failing test --------- Co-authored-by: dshukertjr <dshukertjr@gmail.com>
1 parent c68d44d commit 4e0270a

File tree

14 files changed

+412
-35
lines changed

14 files changed

+412
-35
lines changed

.github/workflows/gotrue.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ jobs:
5353
- name: Build Docker image
5454
run: |
5555
cd ../../infra/gotrue
56-
docker-compose down
57-
docker-compose up -d
56+
docker compose down
57+
docker compose up -d
5858
5959
- name: Sleep for 5 seconds
6060
uses: jakejarvis/wait-action@master

.github/workflows/postgrest.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ jobs:
5555
- name: Build Docker image
5656
run: |
5757
cd ../../infra/postgrest
58-
docker-compose down
59-
docker-compose up -d
58+
docker compose down
59+
docker compose up -d
6060
6161
- name: Sleep for 5 seconds
6262
uses: jakejarvis/wait-action@master

.github/workflows/storage_client.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ jobs:
5252
- name: Build Docker image
5353
run: |
5454
cd ../../infra/storage_client
55-
docker-compose down
56-
docker-compose up -d
55+
docker compose down
56+
docker compose up -d
5757
5858
- name: Sleep for 5 seconds
5959
uses: jakejarvis/wait-action@master

infra/gotrue/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
version: '3'
33
services:
44
gotrue: # Signup enabled, autoconfirm on
5-
image: supabase/gotrue:v2.146.0
5+
image: supabase/auth:v2.151.0
66
ports:
77
- '9998:9998'
88
environment:

packages/gotrue/lib/src/constants.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:gotrue/src/types/api_version.dart';
12
import 'package:gotrue/src/types/auth_response.dart';
23
import 'package:gotrue/src/version.dart';
34

@@ -20,6 +21,16 @@ class Constants {
2021

2122
/// A token refresh will be attempted this many ticks before the current session expires.
2223
static const autoRefreshTickThreshold = 3;
24+
25+
/// The name of the header that contains API version.
26+
static const apiVersionHeaderName = 'x-supabase-api-version';
27+
}
28+
29+
class ApiVersions {
30+
static final v20240101 = ApiVersion(
31+
name: '2024-01-01',
32+
timestamp: DateTime.parse('2024-01-01T00:00:00.0Z'),
33+
);
2334
}
2435

2536
enum AuthChangeEvent {

packages/gotrue/lib/src/fetch.dart

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import 'dart:convert';
22

33
import 'package:collection/collection.dart';
4+
import 'package:gotrue/src/constants.dart';
5+
import 'package:gotrue/src/types/api_version.dart';
46
import 'package:gotrue/src/types/auth_exception.dart';
7+
import 'package:gotrue/src/types/error_code.dart';
58
import 'package:gotrue/src/types/fetch_options.dart';
69
import 'package:http/http.dart';
710

@@ -28,6 +31,16 @@ class GotrueFetch {
2831
return error.toString();
2932
}
3033

34+
String? _getErrorCode(dynamic error, String key) {
35+
if (error is Map) {
36+
final dynamic errorCode = error[key];
37+
if (errorCode is String) {
38+
return errorCode;
39+
}
40+
}
41+
return null;
42+
}
43+
3144
AuthException _handleError(dynamic error) {
3245
if (error is! Response) {
3346
throw AuthRetryableFetchException(message: error.toString());
@@ -50,24 +63,44 @@ class GotrueFetch {
5063
message: error.toString(), originalError: error);
5164
}
5265

53-
// Check if weak password reasons only contain strings
54-
if (data is Map &&
55-
data['weak_password'] is Map &&
56-
data['weak_password']['reasons'] is List &&
57-
(data['weak_password']['reasons'] as List).isNotEmpty &&
58-
(data['weak_password']['reasons'] as List)
59-
.whereNot((element) => element is String)
60-
.isEmpty) {
66+
String? errorCode;
67+
68+
final responseApiVersion = ApiVersion.fromResponse(error);
69+
70+
if (responseApiVersion?.isSameOrAfter(ApiVersions.v20240101) ?? false) {
71+
errorCode = _getErrorCode(data, 'code');
72+
} else {
73+
errorCode = _getErrorCode(data, 'error_code');
74+
}
75+
76+
if (errorCode == null) {
77+
// Legacy support for weak password errors, when there were no error codes
78+
// Check if weak password reasons only contain strings
79+
if (data is Map &&
80+
data['weak_password'] is Map &&
81+
data['weak_password']['reasons'] is List &&
82+
(data['weak_password']['reasons'] as List).isNotEmpty &&
83+
(data['weak_password']['reasons'] as List)
84+
.whereNot((element) => element is String)
85+
.isEmpty) {
86+
throw AuthWeakPasswordException(
87+
message: _getErrorMessage(data),
88+
statusCode: error.statusCode.toString(),
89+
reasons: List<String>.from(data['weak_password']['reasons']),
90+
);
91+
}
92+
} else if (errorCode == ErrorCode.weakPassword.code) {
6193
throw AuthWeakPasswordException(
6294
message: _getErrorMessage(data),
6395
statusCode: error.statusCode.toString(),
64-
reasons: List<String>.from(data['weak_password']['reasons']),
96+
reasons: List<String>.from(data['weak_password']?['reasons'] ?? []),
6597
);
6698
}
6799

68100
throw AuthApiException(
69101
_getErrorMessage(data),
70102
statusCode: error.statusCode.toString(),
103+
code: errorCode,
71104
);
72105
}
73106

@@ -77,6 +110,12 @@ class GotrueFetch {
77110
GotrueRequestOptions? options,
78111
}) async {
79112
final headers = options?.headers ?? {};
113+
114+
// Set the API version header if not already set
115+
if (!headers.containsKey(Constants.apiVersionHeaderName)) {
116+
headers[Constants.apiVersionHeaderName] = ApiVersions.v20240101.name;
117+
}
118+
80119
if (options?.jwt != null) {
81120
headers['Authorization'] = 'Bearer ${options!.jwt}';
82121
}

packages/gotrue/lib/src/gotrue_client.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ class GoTrueClient {
607607
/// If the current session's refresh token is invalid, an error will be thrown.
608608
Future<AuthResponse> refreshSession([String? refreshToken]) async {
609609
if (currentSession?.accessToken == null) {
610-
throw AuthException('Not logged in.');
610+
throw AuthSessionMissingException();
611611
}
612612

613613
final currentSessionRefreshToken =
@@ -626,7 +626,7 @@ class GoTrueClient {
626626
Future<void> reauthenticate() async {
627627
final session = currentSession;
628628
if (session == null) {
629-
throw AuthException('Not logged in.');
629+
throw AuthSessionMissingException();
630630
}
631631

632632
final options =
@@ -691,7 +691,7 @@ class GoTrueClient {
691691
/// Gets the current user details from current session or custom [jwt]
692692
Future<UserResponse> getUser([String? jwt]) async {
693693
if (jwt == null && currentSession?.accessToken == null) {
694-
throw AuthException('Cannot get user: no current session.');
694+
throw AuthSessionMissingException();
695695
}
696696
final options = GotrueRequestOptions(
697697
headers: _headers,
@@ -712,7 +712,7 @@ class GoTrueClient {
712712
}) async {
713713
final accessToken = currentSession?.accessToken;
714714
if (accessToken == null) {
715-
throw AuthException('Not logged in.');
715+
throw AuthSessionMissingException();
716716
}
717717

718718
final body = attributes.toJson();
@@ -736,7 +736,7 @@ class GoTrueClient {
736736
/// Sets the session data from refresh_token and returns the current session.
737737
Future<AuthResponse> setSession(String refreshToken) async {
738738
if (refreshToken.isEmpty) {
739-
throw AuthException('No current session.');
739+
throw AuthSessionMissingException('Refresh token cannot be empty');
740740
}
741741
return await _callRefreshToken(refreshToken);
742742
}
@@ -757,8 +757,13 @@ class GoTrueClient {
757757

758758
final errorDescription = url.queryParameters['error_description'];
759759
final errorCode = url.queryParameters['error_code'];
760+
final error = url.queryParameters['error'];
760761
if (errorDescription != null) {
761-
throw AuthException(errorDescription, statusCode: errorCode);
762+
throw AuthException(
763+
errorDescription,
764+
statusCode: errorCode,
765+
code: error,
766+
);
762767
}
763768

764769
if (_flowType == AuthFlowType.pkce) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'package:gotrue/src/constants.dart';
2+
import 'package:http/http.dart';
3+
4+
// Parses the API version which is 2YYY-MM-DD. */
5+
const String _apiVersionRegex =
6+
r'^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])';
7+
8+
/// Represents the API versions supported by the package.
9+
10+
/// Represents the API version specified by a [name] in the format YYYY-MM-DD.
11+
class ApiVersion {
12+
const ApiVersion({
13+
required this.name,
14+
required this.timestamp,
15+
});
16+
17+
final String name;
18+
final DateTime timestamp;
19+
20+
/// Parses the API version from the string date.
21+
static ApiVersion? fromString(String version) {
22+
if (!RegExp(_apiVersionRegex).hasMatch(version)) {
23+
return null;
24+
}
25+
26+
final DateTime? timestamp = DateTime.tryParse('${version}T00:00:00.0Z');
27+
if (timestamp == null) return null;
28+
return ApiVersion(name: version, timestamp: timestamp);
29+
}
30+
31+
/// Parses the API version from the response headers.
32+
static ApiVersion? fromResponse(Response response) {
33+
final version = response.headers[Constants.apiVersionHeaderName];
34+
return version != null ? fromString(version) : null;
35+
}
36+
37+
/// Returns true if this version is the same or after [other].
38+
bool isSameOrAfter(ApiVersion other) {
39+
return timestamp.isAfter(other.timestamp) || name == other.name;
40+
}
41+
}

packages/gotrue/lib/src/types/auth_exception.dart

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,51 @@
1+
import 'package:gotrue/src/types/error_code.dart';
2+
13
class AuthException implements Exception {
4+
/// Human readable error message associated with the error.
25
final String message;
6+
7+
/// HTTP status code that caused the error.
38
final String? statusCode;
49

5-
const AuthException(this.message, {this.statusCode});
10+
/// Error code associated with the error. Most errors coming from
11+
/// HTTP responses will have a code, though some errors that occur
12+
/// before a response is received will not have one present.
13+
/// In that case [statusCode] will also be null.
14+
///
15+
/// Find the full list of error codes in our documentation.
16+
/// https://supabase.com/docs/reference/dart/auth-error-codes
17+
final String? code;
18+
19+
const AuthException(this.message, {this.statusCode, this.code});
620

721
@override
822
String toString() =>
9-
'AuthException(message: $message, statusCode: $statusCode)';
23+
'AuthException(message: $message, statusCode: $statusCode, errorCode: $code)';
1024

1125
@override
1226
bool operator ==(Object other) {
1327
if (identical(this, other)) return true;
1428

1529
return other is AuthException &&
1630
other.message == message &&
17-
other.statusCode == statusCode;
31+
other.statusCode == statusCode &&
32+
other.code == code;
1833
}
1934

2035
@override
21-
int get hashCode => message.hashCode ^ statusCode.hashCode;
36+
int get hashCode => message.hashCode ^ statusCode.hashCode ^ code.hashCode;
2237
}
2338

2439
class AuthPKCEGrantCodeExchangeError extends AuthException {
2540
AuthPKCEGrantCodeExchangeError(super.message);
2641
}
2742

2843
class AuthSessionMissingException extends AuthException {
29-
AuthSessionMissingException()
30-
: super('Auth session missing!', statusCode: '400');
44+
AuthSessionMissingException([String? message])
45+
: super(
46+
message ?? 'Auth session missing!',
47+
statusCode: '400',
48+
);
3149
}
3250

3351
class AuthRetryableFetchException extends AuthException {
@@ -38,7 +56,7 @@ class AuthRetryableFetchException extends AuthException {
3856
}
3957

4058
class AuthApiException extends AuthException {
41-
AuthApiException(super.message, {super.statusCode});
59+
AuthApiException(super.message, {super.statusCode, super.code});
4260
}
4361

4462
class AuthUnknownException extends AuthException {
@@ -53,7 +71,7 @@ class AuthWeakPasswordException extends AuthException {
5371

5472
AuthWeakPasswordException({
5573
required String message,
56-
required String statusCode,
74+
required super.statusCode,
5775
required this.reasons,
58-
}) : super(message, statusCode: statusCode);
76+
}) : super(message, code: ErrorCode.weakPassword.code);
5977
}

0 commit comments

Comments
 (0)