diff --git a/packages/wyatt_authentication_bloc/example/android/app/google-services.json b/packages/wyatt_authentication_bloc/example/android/app/google-services.json index 93ee728e..cd75a682 100644 --- a/packages/wyatt_authentication_bloc/example/android/app/google-services.json +++ b/packages/wyatt_authentication_bloc/example/android/app/google-services.json @@ -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": { "mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d", diff --git a/packages/wyatt_authentication_bloc/example/lib/app/app.dart b/packages/wyatt_authentication_bloc/example/lib/app/app.dart index 1c87d2ed..2886a69d 100644 --- a/packages/wyatt_authentication_bloc/example/lib/app/app.dart +++ b/packages/wyatt_authentication_bloc/example/lib/app/app.dart @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'dart:developer'; + import 'package:authentication_bloc_example/constants.dart'; import 'package:authentication_bloc_example/home/home_page.dart'; import 'package:authentication_bloc_example/login/login_page.dart'; @@ -55,14 +57,32 @@ class App extends StatelessWidget { if (user.isNotEmpty && !user.isAnonymous) { // Check if user is register in Firesore. DocumentSnapshot firestoreUser = await FirebaseFirestore.instance - .collection('users') + .collection(firestoreCollectionUsers) .doc(user.uid) .get(); - return { - 'user': - UserFirestore.fromMap(firestoreUser.data() as Map), - ...firestoreUser.data() as Map? ?? {} - }; + + if (!firestoreUser.exists) { + // Register user in Firestore when sign in with social account. + 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), + ...firestoreUser.data() as Map? ?? {} + }; + } } else { return {}; } @@ -73,7 +93,11 @@ class App extends StatelessWidget { if (uid != null) { final data = state.data.toMap(); 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); } } diff --git a/packages/wyatt_authentication_bloc/example/lib/constants.dart b/packages/wyatt_authentication_bloc/example/lib/constants.dart index 953b8602..03422520 100644 --- a/packages/wyatt_authentication_bloc/example/lib/constants.dart +++ b/packages/wyatt_authentication_bloc/example/lib/constants.dart @@ -19,4 +19,6 @@ const String formFieldPhone = 'phone'; const String formFieldPro = 'isPro'; const String formFieldConfirmedPassword = 'confirmedPassword'; const String formFieldSiren = 'siren'; -const String formFieldIban = 'iban'; \ No newline at end of file +const String formFieldIban = 'iban'; + +const String firestoreCollectionUsers = 'authentication_bloc_users'; \ No newline at end of file diff --git a/packages/wyatt_authentication_bloc/example/lib/login/widgets/login_form.dart b/packages/wyatt_authentication_bloc/example/lib/login/widgets/login_form.dart index e552f8d6..e5c9f55d 100644 --- a/packages/wyatt_authentication_bloc/example/lib/login/widgets/login_form.dart +++ b/packages/wyatt_authentication_bloc/example/lib/login/widgets/login_form.dart @@ -102,6 +102,24 @@ class _LoginWithPasswordButton extends StatelessWidget { } } +class _LoginWithGoogleButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.status != current.status, + builder: (context, state) { + return state.status.isSubmissionInProgress + ? const CircularProgressIndicator() + : ElevatedButton( + onPressed: () => + context.read().signInWithGoogle(), + child: const Text('LOGIN GOOGLE'), + ); + }, + ); + } +} + class _SignUpButton extends StatelessWidget { @override Widget build(BuildContext context) { @@ -182,6 +200,8 @@ class LoginForm extends StatelessWidget { const SizedBox(height: 8), _LoginAnonButton(), const SizedBox(height: 8), + _LoginWithGoogleButton(), + const SizedBox(height: 8), _SignUpButton(), const SizedBox(height: 8), _SignUpAsProButton(), diff --git a/packages/wyatt_authentication_bloc/example/lib/sign_up/widgets/sign_up_form.dart b/packages/wyatt_authentication_bloc/example/lib/sign_up/widgets/sign_up_form.dart index 6e48d10a..a44c0d0f 100644 --- a/packages/wyatt_authentication_bloc/example/lib/sign_up/widgets/sign_up_form.dart +++ b/packages/wyatt_authentication_bloc/example/lib/sign_up/widgets/sign_up_form.dart @@ -255,7 +255,8 @@ class _DebugButton extends StatelessWidget { builder: (context, state) { return ElevatedButton( onPressed: () { - log(state.toString()); + // log(state.toString()); + log(state.data.toMap().toString()); }, child: const Text('DEBUG'), ); diff --git a/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_firebase.dart b/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_firebase.dart index 9a842cbc..eade1e10 100644 --- a/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_firebase.dart +++ b/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_firebase.dart @@ -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]) diff --git a/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_interface.dart b/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_interface.dart index bb3e9b44..4a1d26b8 100644 --- a/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_interface.dart +++ b/packages/wyatt_authentication_bloc/lib/src/models/exceptions/exceptions_interface.dart @@ -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} diff --git a/packages/wyatt_authentication_bloc/lib/src/models/user/user_firebase.dart b/packages/wyatt_authentication_bloc/lib/src/models/user/user_firebase.dart index df1b78bc..53908ab2 100644 --- a/packages/wyatt_authentication_bloc/lib/src/models/user/user_firebase.dart +++ b/packages/wyatt_authentication_bloc/lib/src/models/user/user_firebase.dart @@ -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 || diff --git a/packages/wyatt_authentication_bloc/lib/src/models/user/user_interface.dart b/packages/wyatt_authentication_bloc/lib/src/models/user/user_interface.dart index 5bbcc3f7..43b926bf 100644 --- a/packages/wyatt_authentication_bloc/lib/src/models/user/user_interface.dart +++ b/packages/wyatt_authentication_bloc/lib/src/models/user/user_interface.dart @@ -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; diff --git a/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_firebase.dart b/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_firebase.dart index 20f6e2e4..f2838122 100644 --- a/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_firebase.dart +++ b/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_firebase.dart @@ -15,19 +15,28 @@ // along with this program. If not, see . 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 get user { @@ -99,9 +108,106 @@ class AuthenticationRepositoryFirebase } @override - Future signInWithGoogle() { - // TODO(hpcl): implement signInWithGoogle - throw UnimplementedError(); + Future 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 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 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 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 diff --git a/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_interface.dart b/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_interface.dart index ecdcd03b..4a2dc295 100644 --- a/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_interface.dart +++ b/packages/wyatt_authentication_bloc/lib/src/repositories/authentication_repository_interface.dart @@ -61,6 +61,21 @@ abstract class AuthenticationRepositoryInterface { /// Throws a [SignInWithGoogleFailureInterface] if an exception occurs. Future signInWithGoogle(); + /// Starts the Sign In with Facebook Flow. + /// + /// Throws a [SignInWithFacebookFailureInterface] if an exception occurs. + Future signInWithFacebook(); + + /// Starts the Sign In with Apple Flow. + /// + /// Throws a [SignInWithAppleFailureInterface] if an exception occurs. + Future signInWithApple(); + + /// Starts the Sign In with Twitter Flow. + /// + /// Throws a [SignInWithTwitterFailureInterface] if an exception occurs. + Future signInWithTwitter(); + /// Signs in using an email address and email sign-in link. /// /// Throws a [SignInWithEmailLinkFailureInterface] if an exception occurs. diff --git a/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_cubit.dart b/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_cubit.dart index b357d347..f90b0ca5 100644 --- a/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_cubit.dart +++ b/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_cubit.dart @@ -76,6 +76,28 @@ class SignInCubit extends Cubit { } } + Future 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 signInWithEmailAndPassword() async { if (!state.status.isValidated) return; emit(state.copyWith(status: FormStatus.submissionInProgress)); diff --git a/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_state.dart b/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_state.dart index f6cfb863..f59c536c 100644 --- a/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_state.dart +++ b/packages/wyatt_authentication_bloc/lib/src/sign_in/cubit/sign_in_state.dart @@ -45,4 +45,14 @@ class SignInState extends Equatable { @override List get props => [email, password, status]; + + @override + String toString() { + return ''' + email: $email, + password: $password, + status: $status, + errorMessage: $errorMessage, + '''; + } } diff --git a/packages/wyatt_authentication_bloc/lib/src/utils/cryptography.dart b/packages/wyatt_authentication_bloc/lib/src/utils/cryptography.dart new file mode 100644 index 00000000..f9a0b247 --- /dev/null +++ b/packages/wyatt_authentication_bloc/lib/src/utils/cryptography.dart @@ -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 . + +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(); + } +} diff --git a/packages/wyatt_authentication_bloc/pubspec.yaml b/packages/wyatt_authentication_bloc/pubspec.yaml index e2e63f01..58c3602a 100644 --- a/packages/wyatt_authentication_bloc/pubspec.yaml +++ b/packages/wyatt_authentication_bloc/pubspec.yaml @@ -11,9 +11,14 @@ dependencies: flutter: sdk: flutter + crypto: ^3.0.2 flutter_bloc: ^8.0.1 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: git: