diff --git a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart index dbe4b429..cfcfba68 100644 --- a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart +++ b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions.dart @@ -256,3 +256,24 @@ abstract class GetIdTokenFailureInterface GetIdTokenFailureInterface.fromCode(super.code) : super.fromCode(); } + +abstract class ReauthenticateFailureInterface + extends AuthenticationFailureInterface { + ReauthenticateFailureInterface(super.code, super.msg); + + ReauthenticateFailureInterface.fromCode(super.code) : super.fromCode(); +} + +abstract class UpdateEmailFailureInterface + extends AuthenticationFailureInterface { + UpdateEmailFailureInterface(super.code, super.msg); + + UpdateEmailFailureInterface.fromCode(super.code) : super.fromCode(); +} + +abstract class UpdatePasswordFailureInterface + extends AuthenticationFailureInterface { + UpdatePasswordFailureInterface(super.code, super.msg); + + UpdatePasswordFailureInterface.fromCode(super.code) : super.fromCode(); +} diff --git a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart index 7a6e37b4..c36dcbd0 100644 --- a/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart +++ b/packages/wyatt_authentication_bloc/lib/src/core/exceptions/exceptions_firebase.dart @@ -277,3 +277,75 @@ class GetIdTokenFailureFirebase extends GetIdTokenFailureInterface { GetIdTokenFailureFirebase.fromCode(super.code) : super.fromCode(); } + +class ReauthenticateFailureFirebase extends ReauthenticateFailureInterface { + ReauthenticateFailureFirebase([String? code, String? msg]) + : super(code ?? 'unknown', msg ?? 'An unknown error occurred.'); + ReauthenticateFailureFirebase.fromCode(String code) : super.fromCode(code) { + switch (code) { + case 'user-mismatch': + msg = 'Given credential does not correspond to the user.'; + break; + case 'user-not-found': + msg = 'User is not found, please create an account.'; + break; + case 'invalid-credential': + msg = 'The credential received is malformed or has expired.'; + break; + case 'invalid-email': + msg = 'Email is not valid or badly formatted.'; + break; + case 'wrong-password': + msg = 'Incorrect password, please try again.'; + break; + case 'invalid-verification-code': + msg = 'The credential verification code received is invalid.'; + break; + case 'invalid-verification-id': + msg = 'The credential verification ID received is invalid.'; + break; + default: + this.code = 'unknown'; + msg = 'An unknown error occurred.'; + } + } +} + +class UpdateEmailFailureFirebase extends UpdateEmailFailureInterface { + UpdateEmailFailureFirebase([String? code, String? msg]) + : super(code ?? 'unknown', msg ?? 'An unknown error occurred.'); + UpdateEmailFailureFirebase.fromCode(String code) : super.fromCode(code) { + switch (code) { + case 'invalid-email': + msg = 'Email is not valid or badly formatted.'; + break; + case 'email-already-in-use': + msg = 'An account already exists for that email.'; + break; + case 'requires-recent-login': + msg = "User's last sign-in time does not meet the security threshold."; + break; + default: + this.code = 'unknown'; + msg = 'An unknown error occurred.'; + } + } +} + +class UpdatePasswordFailureFirebase extends UpdatePasswordFailureInterface { + UpdatePasswordFailureFirebase([String? code, String? msg]) + : super(code ?? 'unknown', msg ?? 'An unknown error occurred.'); + UpdatePasswordFailureFirebase.fromCode(String code) : super.fromCode(code) { + switch (code) { + case 'weak-password': + msg = 'Please enter a stronger password.'; + break; + case 'requires-recent-login': + msg = "User's last sign-in time does not meet the security threshold."; + break; + default: + this.code = 'unknown'; + msg = 'An unknown error occurred.'; + } + } +} diff --git a/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart b/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart index 565b76cc..131e160a 100644 --- a/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart +++ b/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_firebase_data_source_impl.dart @@ -21,6 +21,7 @@ import 'package:wyatt_type_utils/wyatt_type_utils.dart'; class AuthenticationFirebaseDataSourceImpl extends AuthenticationRemoteDataSource { final FirebaseAuth _firebaseAuth; + UserCredential? _latestCreds; AuthenticationFirebaseDataSourceImpl({FirebaseAuth? firebaseAuth}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance; @@ -51,6 +52,7 @@ class AuthenticationFirebaseDataSourceImpl email: email, password: password, ); + _latestCreds = userCredential; final user = userCredential.user; if (user.isNotNull) { return _mapper(user!); @@ -76,6 +78,7 @@ class AuthenticationFirebaseDataSourceImpl email: email, password: password, ); + _latestCreds = userCredential; final user = userCredential.user; if (user.isNotNull) { return _mapper(user!); @@ -92,6 +95,7 @@ class AuthenticationFirebaseDataSourceImpl @override Future signOut() async { try { + _latestCreds = null; await _firebaseAuth.signOut(); } catch (_) { throw SignOutFailureFirebase(); @@ -164,6 +168,7 @@ class AuthenticationFirebaseDataSourceImpl Future signInAnonymously() async { try { final userCredential = await _firebaseAuth.signInAnonymously(); + _latestCreds = userCredential; final user = userCredential.user; if (user.isNotNull) { return _mapper(user!); @@ -199,4 +204,60 @@ class AuthenticationFirebaseDataSourceImpl throw RefreshFailureFirebase(); } } + + @override + Future reauthenticateWithCredential() async { + try { + if (_latestCreds?.credential != null) { + await _firebaseAuth.currentUser + ?.reauthenticateWithCredential(_latestCreds!.credential!); + } else { + throw Exception(); // Get caught just after. + } + final user = _firebaseAuth.currentUser; + if (user.isNotNull) { + return _mapper(user!); + } else { + throw Exception(); // Get caught just after. + } + } on FirebaseAuthException catch (e) { + throw ReauthenticateFailureFirebase.fromCode(e.code); + } catch (_) { + throw ReauthenticateFailureFirebase(); + } + } + + @override + Future updateEmail({required String email}) async { + try { + await _firebaseAuth.currentUser!.updateEmail(email); + final user = _firebaseAuth.currentUser; + if (user.isNotNull) { + return _mapper(user!); + } else { + throw Exception(); // Get caught just after. + } + } on FirebaseAuthException catch (e) { + throw UpdateEmailFailureFirebase.fromCode(e.code); + } catch (_) { + throw UpdateEmailFailureFirebase(); + } + } + + @override + Future updatePassword({required String password}) async { + try { + await _firebaseAuth.currentUser!.updatePassword(password); + final user = _firebaseAuth.currentUser; + if (user.isNotNull) { + return _mapper(user!); + } else { + throw Exception(); // Get caught just after. + } + } on FirebaseAuthException catch (e) { + throw UpdatePasswordFailureFirebase.fromCode(e.code); + } catch (_) { + throw UpdatePasswordFailureFirebase(); + } + } } diff --git a/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_mock_data_source_impl.dart b/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_mock_data_source_impl.dart index 1a7fa1d5..1d750629 100644 --- a/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_mock_data_source_impl.dart +++ b/packages/wyatt_authentication_bloc/lib/src/data/data_sources/remote/authentication_mock_data_source_impl.dart @@ -23,6 +23,7 @@ import 'package:wyatt_type_utils/wyatt_type_utils.dart'; class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource { Pair? _connectedMock; Pair? _registeredMock; + DateTime _lastSignInTime = DateTime.now(); final StreamController _streamAccount = StreamController() ..add(null); @@ -118,6 +119,7 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource { ); _streamAccount.add(mock); _connectedMock = _connectedMock?.copyWith(left: mock); + _lastSignInTime = DateTime.now(); return Future.value(mock); } @@ -149,6 +151,7 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource { } _streamAccount.add(_registeredMock!.left); _connectedMock = _registeredMock!.copyWith(); + _lastSignInTime = DateTime.now(); return _registeredMock!.left!; } throw SignInWithCredentialFailureFirebase(); @@ -193,6 +196,7 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource { ); _streamAccount.add(mock); _registeredMock = Pair(mock, password); + _lastSignInTime = DateTime.now(); return Future.value(mock); } @@ -204,4 +208,45 @@ class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource { await _randomDelay(); return true; } + + @override + Future reauthenticateWithCredential() async { + await _randomDelay(); + if (_connectedMock.isNull) { + throw ReauthenticateFailureFirebase(); + } + await refresh(); + _lastSignInTime = DateTime.now(); + return Future.value(_connectedMock?.left); + } + + @override + Future updateEmail({required String email}) { + final before = DateTime.now().subtract(const Duration(seconds: 10)); + if (_lastSignInTime.isBefore(before)) { + throw UpdateEmailFailureFirebase('requires-recent-login'); + } + final refresh = DateTime.now(); + final mock = (_connectedMock?.left as AccountModel?) + ?.copyWith(lastSignInTime: refresh, email: email); + _connectedMock = _connectedMock?.copyWith(left: mock); + _registeredMock = _registeredMock?.copyWith(left: mock); + _streamAccount.add(mock); + return Future.value(_connectedMock?.left); + } + + @override + Future updatePassword({required String password}) { + final before = DateTime.now().subtract(const Duration(seconds: 10)); + if (_lastSignInTime.isBefore(before)) { + throw UpdatePasswordFailureFirebase('requires-recent-login'); + } + final refresh = DateTime.now(); + final mock = (_connectedMock?.left as AccountModel?) + ?.copyWith(lastSignInTime: refresh); + _connectedMock = _connectedMock?.copyWith(left: mock, right: password); + _registeredMock = _registeredMock?.copyWith(left: mock, right: password); + _streamAccount.add(mock); + return Future.value(_connectedMock?.left); + } } diff --git a/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart b/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart index f5653592..c0f2c8e1 100644 --- a/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart +++ b/packages/wyatt_authentication_bloc/lib/src/data/repositories/authentication_repository_impl.dart @@ -314,4 +314,38 @@ class AuthenticationRepositoryImpl }, (error) => error, ); + + @override + FutureOrResult reauthenticateWithCredential() => + Result.tryCatchAsync( + () async { + final account = await _authenticationRemoteDataSource + .reauthenticateWithCredential(); + return account; + }, + (error) => error, + ); + + @override + FutureOrResult updateEmail({required String email}) => + Result.tryCatchAsync( + () async { + final account = + await _authenticationRemoteDataSource.updateEmail(email: email); + return account; + }, + (error) => error, + ); + + @override + FutureOrResult updatePassword({required String password}) => + Result.tryCatchAsync( + () async { + final account = await _authenticationRemoteDataSource.updatePassword( + password: password, + ); + return account; + }, + (error) => error, + ); } diff --git a/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart b/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart index 755aa157..b379de92 100644 --- a/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart +++ b/packages/wyatt_authentication_bloc/lib/src/domain/data_sources/remote/authentication_remote_data_source.dart @@ -48,4 +48,10 @@ abstract class AuthenticationRemoteDataSource extends BaseRemoteDataSource { Future verifyPasswordResetCode({required String code}); Future signInAnonymously(); + + Future updateEmail({required String email}); + + Future updatePassword({required String password}); + + Future reauthenticateWithCredential(); } diff --git a/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart b/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart index b88de024..d4b2c23b 100644 --- a/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart +++ b/packages/wyatt_authentication_bloc/lib/src/domain/repositories/authentication_repository.dart @@ -84,12 +84,45 @@ abstract class AuthenticationRepository extends BaseRepository { required String password, }); + /// {@template update_email} + /// Update or add [email]. + /// + /// Throws a UpdateEmailFailureInterface if + /// an exception occurs. + /// {@endtemplate} + FutureOrResult updateEmail({ + required String email, + }); + + /// {@template update_password} + /// Update or add [password]. + /// + /// Throws a UpdatePasswordFailureInterface if + /// an exception occurs. + /// {@endtemplate} + FutureOrResult updatePassword({ + required String password, + }); + + /// {@template reauthenticate} + /// Some security-sensitive actions—such as deleting an account, + /// setting a primary email address, and changing a password—require that + /// the user has recently signed in. + /// + /// Throws a ReauthenticateFailureInterface if + /// an exception occurs. + /// {@endtemplate} + FutureOrResult reauthenticateWithCredential(); + /// {@template signout} /// Signs out the current user. /// It also clears the cache and the associated data. /// {@endtemplate} FutureOrResult signOut(); + /// {@template refresh} + /// Refreshes the current user, if signed in. + /// {@endtemplate} FutureOrResult refresh(); /// {@template stream_account}