feat(authentication): add reauthenticate, updateEmail and updatePassword

This commit is contained in:
Hugo Pointcheval 2022-12-03 20:12:08 -05:00
parent d792f4cbe9
commit 5298bd99ed
Signed by: hugo
GPG Key ID: A9E8E9615379254F
7 changed files with 272 additions and 0 deletions

View File

@ -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();
}

View File

@ -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.';
}
}
}

View File

@ -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<void> signOut() async {
try {
_latestCreds = null;
await _firebaseAuth.signOut();
} catch (_) {
throw SignOutFailureFirebase();
@ -164,6 +168,7 @@ class AuthenticationFirebaseDataSourceImpl
Future<Account> 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<Account> 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<Account> 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<Account> 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();
}
}
}

View File

@ -23,6 +23,7 @@ import 'package:wyatt_type_utils/wyatt_type_utils.dart';
class AuthenticationMockDataSourceImpl extends AuthenticationRemoteDataSource {
Pair<Account, String>? _connectedMock;
Pair<Account, String>? _registeredMock;
DateTime _lastSignInTime = DateTime.now();
final StreamController<Account?> _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<Account> reauthenticateWithCredential() async {
await _randomDelay();
if (_connectedMock.isNull) {
throw ReauthenticateFailureFirebase();
}
await refresh();
_lastSignInTime = DateTime.now();
return Future.value(_connectedMock?.left);
}
@override
Future<Account> 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<Account> 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);
}
}

View File

@ -314,4 +314,38 @@ class AuthenticationRepositoryImpl<T extends Object>
},
(error) => error,
);
@override
FutureOrResult<Account> reauthenticateWithCredential() =>
Result.tryCatchAsync<Account, AppException, AppException>(
() async {
final account = await _authenticationRemoteDataSource
.reauthenticateWithCredential();
return account;
},
(error) => error,
);
@override
FutureOrResult<Account> updateEmail({required String email}) =>
Result.tryCatchAsync<Account, AppException, AppException>(
() async {
final account =
await _authenticationRemoteDataSource.updateEmail(email: email);
return account;
},
(error) => error,
);
@override
FutureOrResult<Account> updatePassword({required String password}) =>
Result.tryCatchAsync<Account, AppException, AppException>(
() async {
final account = await _authenticationRemoteDataSource.updatePassword(
password: password,
);
return account;
},
(error) => error,
);
}

View File

@ -48,4 +48,10 @@ abstract class AuthenticationRemoteDataSource extends BaseRemoteDataSource {
Future<bool> verifyPasswordResetCode({required String code});
Future<Account> signInAnonymously();
Future<Account> updateEmail({required String email});
Future<Account> updatePassword({required String password});
Future<Account> reauthenticateWithCredential();
}

View File

@ -84,12 +84,45 @@ abstract class AuthenticationRepository<T> extends BaseRepository {
required String password,
});
/// {@template update_email}
/// Update or add [email].
///
/// Throws a UpdateEmailFailureInterface if
/// an exception occurs.
/// {@endtemplate}
FutureOrResult<Account> updateEmail({
required String email,
});
/// {@template update_password}
/// Update or add [password].
///
/// Throws a UpdatePasswordFailureInterface if
/// an exception occurs.
/// {@endtemplate}
FutureOrResult<Account> updatePassword({
required String password,
});
/// {@template reauthenticate}
/// Some security-sensitive actionssuch as deleting an account,
/// setting a primary email address, and changing a passwordrequire that
/// the user has recently signed in.
///
/// Throws a ReauthenticateFailureInterface if
/// an exception occurs.
/// {@endtemplate}
FutureOrResult<Account> reauthenticateWithCredential();
/// {@template signout}
/// Signs out the current user.
/// It also clears the cache and the associated data.
/// {@endtemplate}
FutureOrResult<void> signOut();
/// {@template refresh}
/// Refreshes the current user, if signed in.
/// {@endtemplate}
FutureOrResult<void> refresh();
/// {@template stream_account}