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

fix: Added missing error codes for AuthException #995

Merged
merged 11 commits into from
Aug 12, 2024
4 changes: 2 additions & 2 deletions .github/workflows/gotrue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ jobs:
- name: Build Docker image
run: |
cd ../../infra/gotrue
docker-compose down
docker-compose up -d
docker compose down
docker compose up -d

- name: Sleep for 5 seconds
uses: jakejarvis/wait-action@master
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/postgrest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ jobs:
- name: Build Docker image
run: |
cd ../../infra/postgrest
docker-compose down
docker-compose up -d
docker compose down
docker compose up -d

- name: Sleep for 5 seconds
uses: jakejarvis/wait-action@master
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/storage_client.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ jobs:
- name: Build Docker image
run: |
cd ../../infra/storage_client
docker-compose down
docker-compose up -d
docker compose down
docker compose up -d

- name: Sleep for 5 seconds
uses: jakejarvis/wait-action@master
Expand Down
2 changes: 1 addition & 1 deletion infra/gotrue/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
version: '3'
services:
gotrue: # Signup enabled, autoconfirm on
image: supabase/gotrue:v2.146.0
image: supabase/auth:v2.151.0
ports:
- '9998:9998'
environment:
Expand Down
11 changes: 11 additions & 0 deletions packages/gotrue/lib/src/constants.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:gotrue/src/types/api_version.dart';
import 'package:gotrue/src/types/auth_response.dart';
import 'package:gotrue/src/version.dart';

Expand All @@ -20,6 +21,16 @@ class Constants {

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

/// The name of the header that contains API version.
static const apiVersionHeaderName = 'x-supabase-api-version';
}

class ApiVersions {
static final v20240101 = ApiVersion(
name: '2024-01-01',
timestamp: DateTime.parse('2024-01-01T00:00:00.0Z'),
);
}

enum AuthChangeEvent {
Expand Down
57 changes: 48 additions & 9 deletions packages/gotrue/lib/src/fetch.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:gotrue/src/constants.dart';
import 'package:gotrue/src/types/api_version.dart';
import 'package:gotrue/src/types/auth_exception.dart';
import 'package:gotrue/src/types/error_code.dart';
import 'package:gotrue/src/types/fetch_options.dart';
import 'package:http/http.dart';

Expand All @@ -28,6 +31,16 @@ class GotrueFetch {
return error.toString();
}

String? _getErrorCode(dynamic error, String key) {
if (error is Map) {
final dynamic errorCode = error[key];
if (errorCode is String) {
return errorCode;
}
bdlukaa marked this conversation as resolved.
Show resolved Hide resolved
}
return null;
}

AuthException _handleError(dynamic error) {
if (error is! Response) {
throw AuthRetryableFetchException(message: error.toString());
Expand All @@ -50,24 +63,44 @@ class GotrueFetch {
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) {
String? errorCode;

final responseApiVersion = ApiVersion.fromResponse(error);

if (responseApiVersion?.isSameOrAfter(ApiVersions.v20240101) ?? false) {
errorCode = _getErrorCode(data, 'code');
} else {
errorCode = _getErrorCode(data, 'error_code');
}
dshukertjr marked this conversation as resolved.
Show resolved Hide resolved

if (errorCode == null) {
// Legacy support for weak password errors, when there were no error codes
// 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: List<String>.from(data['weak_password']['reasons']),
);
}
} else if (errorCode == ErrorCode.weakPassword.code) {
throw AuthWeakPasswordException(
message: _getErrorMessage(data),
statusCode: error.statusCode.toString(),
reasons: List<String>.from(data['weak_password']['reasons']),
reasons: List<String>.from(data['weak_password']?['reasons'] ?? []),
);
}

throw AuthApiException(
_getErrorMessage(data),
statusCode: error.statusCode.toString(),
code: errorCode,
);
}

Expand All @@ -77,6 +110,12 @@ class GotrueFetch {
GotrueRequestOptions? options,
}) async {
final headers = options?.headers ?? {};

// Set the API version header if not already set
if (!headers.containsKey(Constants.apiVersionHeaderName)) {
headers[Constants.apiVersionHeaderName] = ApiVersions.v20240101.name;
}

if (options?.jwt != null) {
headers['Authorization'] = 'Bearer ${options!.jwt}';
}
Expand Down
17 changes: 11 additions & 6 deletions packages/gotrue/lib/src/gotrue_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ class GoTrueClient {
/// If the current session's refresh token is invalid, an error will be thrown.
Future<AuthResponse> refreshSession([String? refreshToken]) async {
if (currentSession?.accessToken == null) {
throw AuthException('Not logged in.');
throw AuthSessionMissingException();
}

final currentSessionRefreshToken =
Expand All @@ -626,7 +626,7 @@ class GoTrueClient {
Future<void> reauthenticate() async {
final session = currentSession;
if (session == null) {
throw AuthException('Not logged in.');
throw AuthSessionMissingException();
}

final options =
Expand Down Expand Up @@ -691,7 +691,7 @@ class GoTrueClient {
/// Gets the current user details from current session or custom [jwt]
Future<UserResponse> getUser([String? jwt]) async {
if (jwt == null && currentSession?.accessToken == null) {
throw AuthException('Cannot get user: no current session.');
throw AuthSessionMissingException();
}
final options = GotrueRequestOptions(
headers: _headers,
Expand All @@ -712,7 +712,7 @@ class GoTrueClient {
}) async {
final accessToken = currentSession?.accessToken;
if (accessToken == null) {
throw AuthException('Not logged in.');
throw AuthSessionMissingException();
}

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

final errorDescription = url.queryParameters['error_description'];
final errorCode = url.queryParameters['error_code'];
final error = url.queryParameters['error'];
if (errorDescription != null) {
throw AuthException(errorDescription, statusCode: errorCode);
throw AuthException(
errorDescription,
statusCode: errorCode,
code: error,
);
}

if (_flowType == AuthFlowType.pkce) {
Expand Down
41 changes: 41 additions & 0 deletions packages/gotrue/lib/src/types/api_version.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:gotrue/src/constants.dart';
import 'package:http/http.dart';

// Parses the API version which is 2YYY-MM-DD. */
const String _apiVersionRegex =
r'^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])';

/// Represents the API versions supported by the package.

/// Represents the API version specified by a [name] in the format YYYY-MM-DD.
class ApiVersion {
const ApiVersion({
required this.name,
required this.timestamp,
});

final String name;
final DateTime timestamp;

/// Parses the API version from the string date.
static ApiVersion? fromString(String version) {
if (!RegExp(_apiVersionRegex).hasMatch(version)) {
return null;
}

final DateTime? timestamp = DateTime.tryParse('${version}T00:00:00.0Z');
if (timestamp == null) return null;
return ApiVersion(name: version, timestamp: timestamp);
}

/// Parses the API version from the response headers.
static ApiVersion? fromResponse(Response response) {
final version = response.headers[Constants.apiVersionHeaderName];
return version != null ? fromString(version) : null;
}

/// Returns true if this version is the same or after [other].
bool isSameOrAfter(ApiVersion other) {
return timestamp.isAfter(other.timestamp) || name == other.name;
}
}
36 changes: 27 additions & 9 deletions packages/gotrue/lib/src/types/auth_exception.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
import 'package:gotrue/src/types/error_code.dart';

class AuthException implements Exception {
/// Human readable error message associated with the error.
final String message;

/// HTTP status code that caused the error.
final String? statusCode;

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

const AuthException(this.message, {this.statusCode, this.code});

@override
String toString() =>
'AuthException(message: $message, statusCode: $statusCode)';
'AuthException(message: $message, statusCode: $statusCode, errorCode: $code)';

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

return other is AuthException &&
other.message == message &&
other.statusCode == statusCode;
other.statusCode == statusCode &&
other.code == code;
}

@override
int get hashCode => message.hashCode ^ statusCode.hashCode;
int get hashCode => message.hashCode ^ statusCode.hashCode ^ code.hashCode;
}

class AuthPKCEGrantCodeExchangeError extends AuthException {
AuthPKCEGrantCodeExchangeError(super.message);
}

class AuthSessionMissingException extends AuthException {
AuthSessionMissingException()
: super('Auth session missing!', statusCode: '400');
AuthSessionMissingException([String? message])
: super(
message ?? 'Auth session missing!',
statusCode: '400',
);
}

class AuthRetryableFetchException extends AuthException {
Expand All @@ -38,7 +56,7 @@ class AuthRetryableFetchException extends AuthException {
}

class AuthApiException extends AuthException {
AuthApiException(super.message, {super.statusCode});
AuthApiException(super.message, {super.statusCode, super.code});
}

class AuthUnknownException extends AuthException {
Expand All @@ -53,7 +71,7 @@ class AuthWeakPasswordException extends AuthException {

AuthWeakPasswordException({
required String message,
required String statusCode,
required super.statusCode,
required this.reasons,
}) : super(message, statusCode: statusCode);
}) : super(message, code: ErrorCode.weakPassword.code);
}
Loading
Loading