Skip to content

Commit

Permalink
fix: Added missing error codes for AuthException (#995)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
lawinski and dshukertjr authored Aug 12, 2024
1 parent c68d44d commit 4e0270a
Show file tree
Hide file tree
Showing 14 changed files with 412 additions and 35 deletions.
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;
}
}
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');
}

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

0 comments on commit 4e0270a

Please sign in to comment.