From 02d7f797a2a17918479a747ae80fdd0de98acc06 Mon Sep 17 00:00:00 2001 From: Juzer Ali Date: Wed, 26 May 2021 17:33:52 +0530 Subject: [PATCH] feat(apple_login):Implement firebase Apple Auth --- examples/flutter_firebase_login/README.md | 1 + .../lib/login/cubit/login_cubit.dart | 12 ++++ .../lib/login/view/login_form.dart | 24 +++++++ .../lib/src/authentication_repository.dart | 62 ++++++++++++++++++- .../authentication_repository/pubspec.yaml | 2 + 5 files changed, 100 insertions(+), 1 deletion(-) diff --git a/examples/flutter_firebase_login/README.md b/examples/flutter_firebase_login/README.md index e0083d1c3a8..ad1a2c1d563 100644 --- a/examples/flutter_firebase_login/README.md +++ b/examples/flutter_firebase_login/README.md @@ -7,6 +7,7 @@ Example Flutter app built with `flutter_bloc` to implement login using Firebase. ## Features - Sign in with Google +- Sign in with Apple - Sign up with email and password - Sign in with email and password diff --git a/examples/flutter_firebase_login/lib/login/cubit/login_cubit.dart b/examples/flutter_firebase_login/lib/login/cubit/login_cubit.dart index d372315a616..7860eea413a 100644 --- a/examples/flutter_firebase_login/lib/login/cubit/login_cubit.dart +++ b/examples/flutter_firebase_login/lib/login/cubit/login_cubit.dart @@ -52,4 +52,16 @@ class LoginCubit extends Cubit { emit(state.copyWith(status: FormzStatus.pure)); } } + + Future logInWithApple() async { + emit(state.copyWith(status: FormzStatus.submissionInProgress)); + try { + await _authenticationRepository.logInWithApple(); + emit(state.copyWith(status: FormzStatus.submissionSuccess)); + } on Exception { + emit(state.copyWith(status: FormzStatus.submissionFailure)); + } on NoSuchMethodError { + emit(state.copyWith(status: FormzStatus.pure)); + } + } } diff --git a/examples/flutter_firebase_login/lib/login/view/login_form.dart b/examples/flutter_firebase_login/lib/login/view/login_form.dart index 992d457f880..a6c82be56bb 100644 --- a/examples/flutter_firebase_login/lib/login/view/login_form.dart +++ b/examples/flutter_firebase_login/lib/login/view/login_form.dart @@ -38,6 +38,8 @@ class LoginForm extends StatelessWidget { _LoginButton(), const SizedBox(height: 8.0), _GoogleLoginButton(), + const SizedBox(height: 8.0), + _AppleLoginButton(), const SizedBox(height: 4.0), _SignUpButton(), ], @@ -139,6 +141,28 @@ class _GoogleLoginButton extends StatelessWidget { } } +class _AppleLoginButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ElevatedButton.icon( + key: const Key('loginForm_appleLogin_raisedButton'), + label: const Text( + 'SIGN IN WITH Apple', + style: TextStyle(color: Colors.white), + ), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0.0), + ), + primary: Colors.black, + ), + icon: const Icon(FontAwesomeIcons.apple, color: Colors.white), + onPressed: () => context.read().logInWithApple(), + ); + } +} + class _SignUpButton extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/examples/flutter_firebase_login/packages/authentication_repository/lib/src/authentication_repository.dart b/examples/flutter_firebase_login/packages/authentication_repository/lib/src/authentication_repository.dart index 800b4377092..e71c648808d 100644 --- a/examples/flutter_firebase_login/packages/authentication_repository/lib/src/authentication_repository.dart +++ b/examples/flutter_firebase_login/packages/authentication_repository/lib/src/authentication_repository.dart @@ -1,10 +1,14 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:cache/cache.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; import 'package:meta/meta.dart'; +import 'package:crypto/crypto.dart'; /// Thrown if during the sign up process if a failure occurs. class SignUpFailure implements Exception {} @@ -15,6 +19,9 @@ class LogInWithEmailAndPasswordFailure implements Exception {} /// Thrown during the sign in with google process if a failure occurs. class LogInWithGoogleFailure implements Exception {} +/// Thrown during the sign in with apple process if a failure occurs. +class LogInWithAppleFailure implements Exception {} + /// Thrown during the logout process if a failure occurs. class LogOutFailure implements Exception {} @@ -27,13 +34,16 @@ class AuthenticationRepository { CacheClient? cache, firebase_auth.FirebaseAuth? firebaseAuth, GoogleSignIn? googleSignIn, + SignInWithApple? appleSignIn, }) : _cache = cache ?? CacheClient(), _firebaseAuth = firebaseAuth ?? firebase_auth.FirebaseAuth.instance, - _googleSignIn = googleSignIn ?? GoogleSignIn.standard(); + _googleSignIn = googleSignIn ?? GoogleSignIn.standard(), + _appleSignIn = appleSignIn ?? SignInWithApple(); final CacheClient _cache; final firebase_auth.FirebaseAuth _firebaseAuth; final GoogleSignIn _googleSignIn; + final SignInWithApple _appleSignIn; /// User cache key. /// Should only be used for testing purposes. @@ -89,6 +99,56 @@ class AuthenticationRepository { } } + /// Starts the Sign In with Apple Flow. + /// + /// Throws a [logInWithApple] if an exception occurs. + Future logInWithApple() 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 = generateNonce(); + final nonce = sha256ofString(rawNonce); + + try { + final appleCredential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + nonce: nonce, + ); + + // Create an `OAuthCredential` from the credential returned by Apple. + final credential = firebase_auth.OAuthProvider('apple.com').credential( + idToken: appleCredential.identityToken, + rawNonce: rawNonce, + ); + print(credential); + + await _firebaseAuth.signInWithCredential(credential); + } on Exception { + throw LogInWithAppleFailure(); + } + } + + /// Generates a cryptographically secure random nonce, to be included in a + /// credential request. + String generateNonce([int length = 32]) { + final 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. + String sha256ofString(String input) { + final bytes = utf8.encode(input); + final digest = sha256.convert(bytes); + return digest.toString(); + } + /// Signs in with the provided [email] and [password]. /// /// Throws a [LogInWithEmailAndPasswordFailure] if an exception occurs. diff --git a/examples/flutter_firebase_login/packages/authentication_repository/pubspec.yaml b/examples/flutter_firebase_login/packages/authentication_repository/pubspec.yaml index 86d701a2174..3ce80eb7953 100644 --- a/examples/flutter_firebase_login/packages/authentication_repository/pubspec.yaml +++ b/examples/flutter_firebase_login/packages/authentication_repository/pubspec.yaml @@ -17,6 +17,8 @@ dependencies: google_sign_in: ^5.0.0 meta: ^1.3.0 very_good_analysis: ^2.0.0 + sign_in_with_apple: ^3.0.0 + crypto: ^3.0.0 dev_dependencies: flutter_test: