feat(auth): add google, facebook, apple, twitter login
This commit is contained in:
+37
-3
@@ -106,10 +106,12 @@ class SignInAnonymouslyFailureFirebase
|
||||
}
|
||||
}
|
||||
|
||||
class SignInWithGoogleFailureFirebase extends SignInWithGoogleFailureInterface {
|
||||
SignInWithGoogleFailureFirebase([String? code, String? message])
|
||||
class SignInWithCredentialFailureFirebase
|
||||
extends SignInWithCredentialFailureInterface {
|
||||
SignInWithCredentialFailureFirebase([String? code, String? message])
|
||||
: super(code ?? 'unknown', message ?? 'An unknown error occurred.');
|
||||
SignInWithGoogleFailureFirebase.fromCode(String code) : super.fromCode(code) {
|
||||
SignInWithCredentialFailureFirebase.fromCode(String code)
|
||||
: super.fromCode(code) {
|
||||
switch (code) {
|
||||
case 'account-exists-with-different-credential':
|
||||
message = 'Account exists with different credentials.';
|
||||
@@ -143,6 +145,38 @@ class SignInWithGoogleFailureFirebase extends SignInWithGoogleFailureInterface {
|
||||
}
|
||||
}
|
||||
|
||||
class SignInWithGoogleFailureFirebase
|
||||
extends SignInWithCredentialFailureFirebase
|
||||
implements SignInWithGoogleFailureInterface {
|
||||
SignInWithGoogleFailureFirebase([String? code, String? message])
|
||||
: super(code, message);
|
||||
SignInWithGoogleFailureFirebase.fromCode(String code) : super.fromCode(code);
|
||||
}
|
||||
|
||||
class SignInWithFacebookFailureFirebase
|
||||
extends SignInWithCredentialFailureFirebase
|
||||
implements SignInWithFacebookFailureInterface {
|
||||
SignInWithFacebookFailureFirebase([String? code, String? message])
|
||||
: super(code, message);
|
||||
SignInWithFacebookFailureFirebase.fromCode(String code)
|
||||
: super.fromCode(code);
|
||||
}
|
||||
|
||||
class SignInWithAppleFailureFirebase extends SignInWithCredentialFailureFirebase
|
||||
implements SignInWithAppleFailureInterface {
|
||||
SignInWithAppleFailureFirebase([String? code, String? message])
|
||||
: super(code, message);
|
||||
SignInWithAppleFailureFirebase.fromCode(String code) : super.fromCode(code);
|
||||
}
|
||||
|
||||
class SignInWithTwitterFailureFirebase extends SignInWithCredentialFailureFirebase
|
||||
implements SignInWithAppleFailureInterface {
|
||||
SignInWithTwitterFailureFirebase([String? code, String? message])
|
||||
: super(code, message);
|
||||
SignInWithTwitterFailureFirebase.fromCode(String code) : super.fromCode(code);
|
||||
}
|
||||
|
||||
|
||||
class SignInWithEmailLinkFailureFirebase
|
||||
extends SignInWithEmailLinkFailureInterface {
|
||||
SignInWithEmailLinkFailureFirebase([String? code, String? message])
|
||||
|
||||
+55
@@ -64,6 +64,20 @@ abstract class FetchSignInMethodsForEmailFailureInterface
|
||||
: super.fromCode(code);
|
||||
}
|
||||
|
||||
/// {@template sign_in_with_credential_failure}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
abstract class SignInWithCredentialFailureInterface
|
||||
extends AuthenticationFailureInterface {
|
||||
/// {@macro sign_in_with_credential_failure}
|
||||
SignInWithCredentialFailureInterface(String code, String message)
|
||||
: super(code, message);
|
||||
|
||||
/// {@macro sign_in_with_credential_failure}
|
||||
SignInWithCredentialFailureInterface.fromCode(String code)
|
||||
: super.fromCode(code);
|
||||
}
|
||||
|
||||
/// {@template sign_in_anonymously_failure}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
@@ -91,6 +105,47 @@ abstract class SignInWithGoogleFailureInterface
|
||||
SignInWithGoogleFailureInterface.fromCode(String code) : super.fromCode(code);
|
||||
}
|
||||
|
||||
/// {@template sign_in_with_facebook_failure}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
abstract class SignInWithFacebookFailureInterface
|
||||
extends AuthenticationFailureInterface {
|
||||
/// {@macro sign_in_with_facebook_failure}
|
||||
SignInWithFacebookFailureInterface(String code, String message)
|
||||
: super(code, message);
|
||||
|
||||
/// {@macro sign_in_with_facebook_failure}
|
||||
SignInWithFacebookFailureInterface.fromCode(String code)
|
||||
: super.fromCode(code);
|
||||
}
|
||||
|
||||
/// {@template sign_in_with_apple_failure}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
abstract class SignInWithAppleFailureInterface
|
||||
extends AuthenticationFailureInterface {
|
||||
/// {@macro sign_in_with_apple_failure}
|
||||
SignInWithAppleFailureInterface(String code, String message)
|
||||
: super(code, message);
|
||||
|
||||
/// {@macro sign_in_with_apple_failure}
|
||||
SignInWithAppleFailureInterface.fromCode(String code) : super.fromCode(code);
|
||||
}
|
||||
|
||||
/// {@template sign_in_with_twitter_failure}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
abstract class SignInWithTwitterFailureInterface
|
||||
extends AuthenticationFailureInterface {
|
||||
/// {@macro sign_in_with_twitter_failure}
|
||||
SignInWithTwitterFailureInterface(String code, String message)
|
||||
: super(code, message);
|
||||
|
||||
/// {@macro sign_in_with_twitter_failure}
|
||||
SignInWithTwitterFailureInterface.fromCode(String code)
|
||||
: super.fromCode(code);
|
||||
}
|
||||
|
||||
/// {@template sign_in_with_email_link_failure}
|
||||
/// Thrown during the sign in process if a failure occurs.
|
||||
/// {@endtemplate}
|
||||
|
||||
@@ -64,6 +64,9 @@ class UserFirebase implements UserInterface {
|
||||
@override
|
||||
String get uid => _user?.uid ?? '';
|
||||
|
||||
@override
|
||||
String? get providerId => _user?.providerData.first.providerId;
|
||||
|
||||
@override
|
||||
bool? get isNewUser {
|
||||
if (_user?.metadata.lastSignInTime == null ||
|
||||
|
||||
@@ -71,6 +71,9 @@ abstract class UserInterface {
|
||||
/// The user's unique ID.
|
||||
String get uid;
|
||||
|
||||
/// The provider ID for the user.
|
||||
String? get providerId;
|
||||
|
||||
/// Whether the user account has been recently created.
|
||||
bool? get isNewUser;
|
||||
|
||||
|
||||
+111
-5
@@ -15,19 +15,28 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
import 'package:twitter_login/twitter_login.dart';
|
||||
import 'package:wyatt_authentication_bloc/src/models/exceptions/exceptions_firebase.dart';
|
||||
import 'package:wyatt_authentication_bloc/src/models/user/user_firebase.dart';
|
||||
import 'package:wyatt_authentication_bloc/src/models/user/user_interface.dart';
|
||||
import 'package:wyatt_authentication_bloc/src/repositories/authentication_repository_interface.dart';
|
||||
import 'package:wyatt_authentication_bloc/src/utils/cryptography.dart';
|
||||
|
||||
class AuthenticationRepositoryFirebase
|
||||
implements AuthenticationRepositoryInterface {
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
final TwitterLogin? _twitterLogin;
|
||||
|
||||
UserFirebase _userCache = const UserFirebase.empty();
|
||||
|
||||
AuthenticationRepositoryFirebase({FirebaseAuth? firebaseAuth})
|
||||
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance;
|
||||
AuthenticationRepositoryFirebase({
|
||||
FirebaseAuth? firebaseAuth,
|
||||
TwitterLogin? twitterLogin,
|
||||
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
|
||||
_twitterLogin = twitterLogin;
|
||||
|
||||
@override
|
||||
Stream<UserInterface> get user {
|
||||
@@ -99,9 +108,106 @@ class AuthenticationRepositoryFirebase
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithGoogle() {
|
||||
// TODO(hpcl): implement signInWithGoogle
|
||||
throw UnimplementedError();
|
||||
Future<void> signInWithGoogle() async {
|
||||
// Trigger the authentication flow
|
||||
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
|
||||
|
||||
// Obtain the auth details from the request
|
||||
final GoogleSignInAuthentication? googleAuth =
|
||||
await googleUser?.authentication;
|
||||
|
||||
// Create a new credential
|
||||
final credential = GoogleAuthProvider.credential(
|
||||
accessToken: googleAuth?.accessToken,
|
||||
idToken: googleAuth?.idToken,
|
||||
);
|
||||
|
||||
try {
|
||||
await _firebaseAuth.signInWithCredential(credential);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
throw SignInWithGoogleFailureFirebase.fromCode(e.code);
|
||||
} catch (_) {
|
||||
throw SignInWithGoogleFailureFirebase();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithFacebook() async {
|
||||
// Trigger the sign-in flow
|
||||
final LoginResult loginResult = await FacebookAuth.instance.login();
|
||||
|
||||
// Create a credential from the access token
|
||||
final OAuthCredential credential =
|
||||
FacebookAuthProvider.credential(loginResult.accessToken?.token ?? '');
|
||||
|
||||
try {
|
||||
await _firebaseAuth.signInWithCredential(credential);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
throw SignInWithFacebookFailureFirebase.fromCode(e.code);
|
||||
} catch (_) {
|
||||
throw SignInWithFacebookFailureFirebase();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithApple() async {
|
||||
// To prevent replay attacks with the credential returned from Apple, we
|
||||
// include a nonce in the credential request. When signing in with
|
||||
// Firebase, the nonce in the id token returned by Apple, is expected to
|
||||
// match the sha256 hash of `rawNonce`.
|
||||
final rawNonce = Cryptography.generateNonce();
|
||||
final nonce = Cryptography.sha256ofString(rawNonce);
|
||||
|
||||
// Request credential for the currently signed in Apple account.
|
||||
final appleCredential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [
|
||||
AppleIDAuthorizationScopes.email,
|
||||
AppleIDAuthorizationScopes.fullName,
|
||||
],
|
||||
nonce: nonce,
|
||||
);
|
||||
|
||||
// Create an `OAuthCredential` from the credential returned by Apple.
|
||||
final credential = OAuthProvider('apple.com').credential(
|
||||
idToken: appleCredential.identityToken,
|
||||
rawNonce: rawNonce,
|
||||
);
|
||||
|
||||
// Sign in the user with Firebase. If the nonce we generated earlier does
|
||||
// not match the nonce in `appleCredential.identityToken`,
|
||||
// sign in will fail.
|
||||
try {
|
||||
await _firebaseAuth.signInWithCredential(credential);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
throw SignInWithAppleFailureFirebase.fromCode(e.code);
|
||||
} catch (_) {
|
||||
throw SignInWithAppleFailureFirebase();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithTwitter() async {
|
||||
final twitterLogin = _twitterLogin;
|
||||
if (twitterLogin == null) {
|
||||
throw SignInWithTwitterFailureFirebase();
|
||||
}
|
||||
|
||||
// Trigger the sign-in flow
|
||||
final authResult = await twitterLogin.login();
|
||||
|
||||
// Create a credential from the access token
|
||||
final credential = TwitterAuthProvider.credential(
|
||||
accessToken: authResult.authToken!,
|
||||
secret: authResult.authTokenSecret!,
|
||||
);
|
||||
|
||||
try {
|
||||
await _firebaseAuth.signInWithCredential(credential);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
throw SignInWithCredentialFailureFirebase.fromCode(e.code);
|
||||
} catch (_) {
|
||||
throw SignInWithCredentialFailureFirebase();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+15
@@ -61,6 +61,21 @@ abstract class AuthenticationRepositoryInterface {
|
||||
/// Throws a [SignInWithGoogleFailureInterface] if an exception occurs.
|
||||
Future<void> signInWithGoogle();
|
||||
|
||||
/// Starts the Sign In with Facebook Flow.
|
||||
///
|
||||
/// Throws a [SignInWithFacebookFailureInterface] if an exception occurs.
|
||||
Future<void> signInWithFacebook();
|
||||
|
||||
/// Starts the Sign In with Apple Flow.
|
||||
///
|
||||
/// Throws a [SignInWithAppleFailureInterface] if an exception occurs.
|
||||
Future<void> signInWithApple();
|
||||
|
||||
/// Starts the Sign In with Twitter Flow.
|
||||
///
|
||||
/// Throws a [SignInWithTwitterFailureInterface] if an exception occurs.
|
||||
Future<void> signInWithTwitter();
|
||||
|
||||
/// Signs in using an email address and email sign-in link.
|
||||
///
|
||||
/// Throws a [SignInWithEmailLinkFailureInterface] if an exception occurs.
|
||||
|
||||
@@ -76,6 +76,28 @@ class SignInCubit extends Cubit<SignInState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signInWithGoogle() async {
|
||||
if (state.status.isSubmissionInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: FormStatus.submissionInProgress));
|
||||
try {
|
||||
await _authenticationRepository.signInWithGoogle();
|
||||
_authenticationCubit.start();
|
||||
emit(state.copyWith(status: FormStatus.submissionSuccess));
|
||||
} on SignInWithGoogleFailureInterface catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: e.message,
|
||||
status: FormStatus.submissionFailure,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
emit(state.copyWith(status: FormStatus.submissionFailure));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signInWithEmailAndPassword() async {
|
||||
if (!state.status.isValidated) return;
|
||||
emit(state.copyWith(status: FormStatus.submissionInProgress));
|
||||
|
||||
@@ -45,4 +45,14 @@ class SignInState extends Equatable {
|
||||
|
||||
@override
|
||||
List<Object> get props => [email, password, status];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
email: $email,
|
||||
password: $password,
|
||||
status: $status,
|
||||
errorMessage: $errorMessage,
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
class Cryptography {
|
||||
/// Generates a cryptographically secure random nonce, to be included in a
|
||||
/// credential request.
|
||||
static String generateNonce([int length = 32]) {
|
||||
const charset =
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._';
|
||||
final random = Random.secure();
|
||||
return List.generate(length, (_) => charset[random.nextInt(charset.length)])
|
||||
.join();
|
||||
}
|
||||
|
||||
/// Returns the sha256 hash of [input] in hex notation.
|
||||
static String sha256ofString(String input) {
|
||||
final bytes = utf8.encode(input);
|
||||
final digest = sha256.convert(bytes);
|
||||
return digest.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user