feat(auth): add google, facebook, apple, twitter login

This commit is contained in:
Hugo Pointcheval 2022-05-04 22:50:07 +02:00
parent 74db784973
commit 12f9cf6aa5
15 changed files with 393 additions and 18 deletions

View File

@ -42,6 +42,42 @@
} }
} }
}, },
{
"client_info": {
"mobilesdk_app_id": "1:136771801992:android:8482c9b90bc29de697203d",
"android_client_info": {
"package_name": "com.example.crud_bloc_example"
}
},
"oauth_client": [
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAYS14uXupkS158Q5QAFP1864UrUN_yDSk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "136771801992-e585bm1n9b3lv89t4phrl9u0glsg52ua.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.example.example"
}
}
]
}
}
},
{ {
"client_info": { "client_info": {
"mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d", "mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d",

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:developer';
import 'package:authentication_bloc_example/constants.dart'; import 'package:authentication_bloc_example/constants.dart';
import 'package:authentication_bloc_example/home/home_page.dart'; import 'package:authentication_bloc_example/home/home_page.dart';
import 'package:authentication_bloc_example/login/login_page.dart'; import 'package:authentication_bloc_example/login/login_page.dart';
@ -55,14 +57,32 @@ class App extends StatelessWidget {
if (user.isNotEmpty && !user.isAnonymous) { if (user.isNotEmpty && !user.isAnonymous) {
// Check if user is register in Firesore. // Check if user is register in Firesore.
DocumentSnapshot firestoreUser = await FirebaseFirestore.instance DocumentSnapshot firestoreUser = await FirebaseFirestore.instance
.collection('users') .collection(firestoreCollectionUsers)
.doc(user.uid) .doc(user.uid)
.get(); .get();
return {
'user': if (!firestoreUser.exists) {
UserFirestore.fromMap(firestoreUser.data() as Map<String, dynamic>), // Register user in Firestore when sign in with social account.
...firestoreUser.data() as Map<String, dynamic>? ?? {} final uid = user.uid;
}; final u = {'uid': uid, 'email': user.email};
await FirebaseFirestore.instance
.collection(firestoreCollectionUsers)
.doc(uid)
.set(u);
return {
'user': UserFirestore(
uid: uid,
email: user.email ?? '',
name: user.displayName ?? '',
phone: user.phoneNumber ?? ''),
};
} else {
return {
'user': UserFirestore.fromMap(
firestoreUser.data() as Map<String, dynamic>),
...firestoreUser.data() as Map<String, dynamic>? ?? {}
};
}
} else { } else {
return {}; return {};
} }
@ -73,7 +93,11 @@ class App extends StatelessWidget {
if (uid != null) { if (uid != null) {
final data = state.data.toMap(); final data = state.data.toMap();
final user = {'uid': uid, 'email': state.email.value, ...data}; final user = {'uid': uid, 'email': state.email.value, ...data};
await FirebaseFirestore.instance.collection('users').doc(uid).set(user); log('onSignUpSuccess: $user');
await FirebaseFirestore.instance
.collection(firestoreCollectionUsers)
.doc(uid)
.set(user);
} }
} }

View File

@ -20,3 +20,5 @@ const String formFieldPro = 'isPro';
const String formFieldConfirmedPassword = 'confirmedPassword'; const String formFieldConfirmedPassword = 'confirmedPassword';
const String formFieldSiren = 'siren'; const String formFieldSiren = 'siren';
const String formFieldIban = 'iban'; const String formFieldIban = 'iban';
const String firestoreCollectionUsers = 'authentication_bloc_users';

View File

@ -102,6 +102,24 @@ class _LoginWithPasswordButton extends StatelessWidget {
} }
} }
class _LoginWithGoogleButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SignInCubit, SignInState>(
buildWhen: (previous, current) => previous.status != current.status,
builder: (context, state) {
return state.status.isSubmissionInProgress
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: () =>
context.read<SignInCubit>().signInWithGoogle(),
child: const Text('LOGIN GOOGLE'),
);
},
);
}
}
class _SignUpButton extends StatelessWidget { class _SignUpButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -182,6 +200,8 @@ class LoginForm extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
_LoginAnonButton(), _LoginAnonButton(),
const SizedBox(height: 8), const SizedBox(height: 8),
_LoginWithGoogleButton(),
const SizedBox(height: 8),
_SignUpButton(), _SignUpButton(),
const SizedBox(height: 8), const SizedBox(height: 8),
_SignUpAsProButton(), _SignUpAsProButton(),

View File

@ -255,7 +255,8 @@ class _DebugButton extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return ElevatedButton( return ElevatedButton(
onPressed: () { onPressed: () {
log(state.toString()); // log(state.toString());
log(state.data.toMap().toString());
}, },
child: const Text('DEBUG'), child: const Text('DEBUG'),
); );

View File

@ -106,10 +106,12 @@ class SignInAnonymouslyFailureFirebase
} }
} }
class SignInWithGoogleFailureFirebase extends SignInWithGoogleFailureInterface { class SignInWithCredentialFailureFirebase
SignInWithGoogleFailureFirebase([String? code, String? message]) extends SignInWithCredentialFailureInterface {
SignInWithCredentialFailureFirebase([String? code, String? message])
: super(code ?? 'unknown', message ?? 'An unknown error occurred.'); : super(code ?? 'unknown', message ?? 'An unknown error occurred.');
SignInWithGoogleFailureFirebase.fromCode(String code) : super.fromCode(code) { SignInWithCredentialFailureFirebase.fromCode(String code)
: super.fromCode(code) {
switch (code) { switch (code) {
case 'account-exists-with-different-credential': case 'account-exists-with-different-credential':
message = 'Account exists with different credentials.'; 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 class SignInWithEmailLinkFailureFirebase
extends SignInWithEmailLinkFailureInterface { extends SignInWithEmailLinkFailureInterface {
SignInWithEmailLinkFailureFirebase([String? code, String? message]) SignInWithEmailLinkFailureFirebase([String? code, String? message])

View File

@ -64,6 +64,20 @@ abstract class FetchSignInMethodsForEmailFailureInterface
: super.fromCode(code); : 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} /// {@template sign_in_anonymously_failure}
/// Thrown during the sign in process if a failure occurs. /// Thrown during the sign in process if a failure occurs.
/// {@endtemplate} /// {@endtemplate}
@ -91,6 +105,47 @@ abstract class SignInWithGoogleFailureInterface
SignInWithGoogleFailureInterface.fromCode(String code) : super.fromCode(code); 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} /// {@template sign_in_with_email_link_failure}
/// Thrown during the sign in process if a failure occurs. /// Thrown during the sign in process if a failure occurs.
/// {@endtemplate} /// {@endtemplate}

View File

@ -64,6 +64,9 @@ class UserFirebase implements UserInterface {
@override @override
String get uid => _user?.uid ?? ''; String get uid => _user?.uid ?? '';
@override
String? get providerId => _user?.providerData.first.providerId;
@override @override
bool? get isNewUser { bool? get isNewUser {
if (_user?.metadata.lastSignInTime == null || if (_user?.metadata.lastSignInTime == null ||

View File

@ -71,6 +71,9 @@ abstract class UserInterface {
/// The user's unique ID. /// The user's unique ID.
String get uid; String get uid;
/// The provider ID for the user.
String? get providerId;
/// Whether the user account has been recently created. /// Whether the user account has been recently created.
bool? get isNewUser; bool? get isNewUser;

View File

@ -15,19 +15,28 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:firebase_auth/firebase_auth.dart'; 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/exceptions/exceptions_firebase.dart';
import 'package:wyatt_authentication_bloc/src/models/user/user_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/models/user/user_interface.dart';
import 'package:wyatt_authentication_bloc/src/repositories/authentication_repository_interface.dart'; import 'package:wyatt_authentication_bloc/src/repositories/authentication_repository_interface.dart';
import 'package:wyatt_authentication_bloc/src/utils/cryptography.dart';
class AuthenticationRepositoryFirebase class AuthenticationRepositoryFirebase
implements AuthenticationRepositoryInterface { implements AuthenticationRepositoryInterface {
final FirebaseAuth _firebaseAuth; final FirebaseAuth _firebaseAuth;
final TwitterLogin? _twitterLogin;
UserFirebase _userCache = const UserFirebase.empty(); UserFirebase _userCache = const UserFirebase.empty();
AuthenticationRepositoryFirebase({FirebaseAuth? firebaseAuth}) AuthenticationRepositoryFirebase({
: _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance; FirebaseAuth? firebaseAuth,
TwitterLogin? twitterLogin,
}) : _firebaseAuth = firebaseAuth ?? FirebaseAuth.instance,
_twitterLogin = twitterLogin;
@override @override
Stream<UserInterface> get user { Stream<UserInterface> get user {
@ -99,9 +108,106 @@ class AuthenticationRepositoryFirebase
} }
@override @override
Future<void> signInWithGoogle() { Future<void> signInWithGoogle() async {
// TODO(hpcl): implement signInWithGoogle // Trigger the authentication flow
throw UnimplementedError(); 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 @override

View File

@ -61,6 +61,21 @@ abstract class AuthenticationRepositoryInterface {
/// Throws a [SignInWithGoogleFailureInterface] if an exception occurs. /// Throws a [SignInWithGoogleFailureInterface] if an exception occurs.
Future<void> signInWithGoogle(); 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. /// Signs in using an email address and email sign-in link.
/// ///
/// Throws a [SignInWithEmailLinkFailureInterface] if an exception occurs. /// Throws a [SignInWithEmailLinkFailureInterface] if an exception occurs.

View File

@ -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 { Future<void> signInWithEmailAndPassword() async {
if (!state.status.isValidated) return; if (!state.status.isValidated) return;
emit(state.copyWith(status: FormStatus.submissionInProgress)); emit(state.copyWith(status: FormStatus.submissionInProgress));

View File

@ -45,4 +45,14 @@ class SignInState extends Equatable {
@override @override
List<Object> get props => [email, password, status]; List<Object> get props => [email, password, status];
@override
String toString() {
return '''
email: $email,
password: $password,
status: $status,
errorMessage: $errorMessage,
''';
}
} }

View File

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

View File

@ -11,9 +11,14 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
crypto: ^3.0.2
flutter_bloc: ^8.0.1 flutter_bloc: ^8.0.1
equatable: ^2.0.3 equatable: ^2.0.3
firebase_auth: ^3.3.14 firebase_auth: ^3.3.17
google_sign_in: ^5.3.0
flutter_facebook_auth: ^4.3.0
sign_in_with_apple: ^3.3.0
twitter_login: ^4.2.3
wyatt_form_bloc: wyatt_form_bloc:
git: git: