Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#45 GoogleとAppleでのログイン処理作成 #61

Merged
merged 11 commits into from
Jul 23, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,24 @@ class WorkerRepository {
/// 指定した [Worker] を購読する。
Stream<ReadWorker?> subscribeWorker({required String workerId}) =>
_query.subscribeDocument(workerId: workerId);

/// 指定した [Worker] を取得する。
Future<ReadWorker?> fetchWorker({required String workerId}) =>
_query.fetchDocument(workerId: workerId);

/// [Worker] を作成する。
Future<void> setWorker({
required String workerId,
required String displayName,
String imageUrl = '',
bool isHost = false,
}) =>
_query.set(
workerId: workerId,
createWorker: CreateWorker(
displayName: displayName,
imageUrl: imageUrl,
isHost: isHost,
),
);
}
6 changes: 6 additions & 0 deletions packages/mottai_flutter_app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ PODS:
- SDWebImageWebPCoder (0.13.0):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.17)
- sign_in_with_apple (0.0.1):
- Flutter
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
Expand All @@ -194,6 +196,7 @@ DEPENDENCIES:
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)

SPEC REPOS:
Expand Down Expand Up @@ -256,6 +259,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
sign_in_with_apple:
:path: ".symlinks/plugins/sign_in_with_apple/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"

Expand Down Expand Up @@ -305,6 +310,7 @@ SPEC CHECKSUMS:
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102
sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a

PODFILE CHECKSUM: e5e57377bd5fff00d6d1c5fa3559e4346fd8c03e
Expand Down
19 changes: 15 additions & 4 deletions packages/mottai_flutter_app/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
Expand All @@ -20,10 +22,23 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.709197089170-9hsmfhj3ovc79m8f982j7lot5n7iq3rh</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand All @@ -43,9 +58,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
4 changes: 4 additions & 0 deletions packages/mottai_flutter_app/ios/Runner/Runner.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>
102 changes: 98 additions & 4 deletions packages/mottai_flutter_app/lib/auth/auth.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
import 'dart:convert';

import 'package:crypto/crypto.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

import '../user/worker.dart';

/// Firebase Console の Authentication で設定できるサインイン方法の種別。
enum SignInMethod {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firebase Console の表記「Sign-in method」に名前を合わせました。

google,
apple,
line,
// TODO: 後で削除する予定
email,
;
}

/// [FirebaseAuth] のインスタンスを提供する [Provider].
final _authProvider = Provider<FirebaseAuth>((_) => FirebaseAuth.instance);
Expand All @@ -25,14 +42,20 @@ final isSignedInProvider = Provider<bool>(
(ref) => ref.watch(userIdProvider) != null,
);

final authServiceProvider =
Provider.autoDispose<AuthService>((_) => const AuthService());
final authServiceProvider = Provider.autoDispose<AuthService>((ref) {
return AuthService(
workerService: ref.watch(workerServiceProvider),
);
});

/// [FirebaseAuth] の認証関係の振る舞いを記述するモデル。
class AuthService {
const AuthService();
const AuthService({
required WorkerService workerService,
}) : _workerService = workerService;

static final _auth = FirebaseAuth.instance;
final WorkerService _workerService;

// TODO: 開発中のみ使用する。リリース時には消すか、あとで デバッグモード or
// 開発環境接続時のみ使用可能にする。
Expand All @@ -43,6 +66,77 @@ class AuthService {
}) =>
_auth.signInWithEmailAndPassword(email: email, password: password);

/// [FirebaseAuth] に Google でサインインする。
/// https://firebase.flutter.dev/docs/auth/social/#google に従っている。
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlutterFire の公式ドキュメントに従っていることを明記しました。

Future<UserCredential> signInWithGoogle() async {
final googleUser = await GoogleSignIn().signIn(); // サインインダイアログの表示
final googleAuth = await googleUser?.authentication; // アカウントからトークン生成
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken,
idToken: googleAuth?.idToken,
);

final userCredential = await _auth.signInWithCredential(credential);
await _maybeCreateWorkerByUserCredential(userCredential: userCredential);
return userCredential;
}

/// [FirebaseAuth] に Apple でサインインする。
/// https://firebase.flutter.dev/docs/auth/social/#apple に従っている。
Future<UserCredential> signInWithApple() async {
final rawNonce = generateNonce();
final nonce = _sha256ofString(rawNonce);

final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
nonce: nonce,
);

final oauthCredential = OAuthProvider('apple.com').credential(
idToken: appleCredential.identityToken,
rawNonce: rawNonce,
);

final userCredential =
await FirebaseAuth.instance.signInWithCredential(oauthCredential);
await _maybeCreateWorkerByUserCredential(userCredential: userCredential);
return userCredential;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この処理が公式ドキュメント

https://firebase.flutter.dev/docs/auth/social/#apple

import 'dart:convert';
import 'dart:math';

import 'package:crypto/crypto.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

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

Future<UserCredential> 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 = generateNonce();
  final nonce = 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 oauthCredential = 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.
  return await FirebaseAuth.instance.signInWithCredential(oauthCredential);
}

と一致していないのには理由がありますか?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kosukesaigusa
勘違いでしたらすみません。。
Androidで試したときに例外が発生してできなかったので、調べていたところこちらのissueを見つけました。
上記Issueやほかのサイトなどを確認するとAndroidではこの方法が非対応みたいでしたので、別のログイン方法にしていました。
ただ、AndroidではAppleログインを行わないとのことでしたので、ソースはflutterfireを使用する方法に修正しました!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android での Apple ログインのためだったのですね!🙏私も知りませんでした、勉強になりました!
今回は Android で Apple ログインはしない前提でいきましょう!変更ありがとうございます!!

}

/// 文字列から SHA-256 ハッシュを作成する。
String _sha256ofString(String input) {
final bytes = utf8.encode(input);
final digest = sha256.convert(bytes);
return digest.toString();
}

/// サインイン時に、まだ Worker ドキュメントが存在していなければ、Firebase
/// の [UserCredential] をもとに生成する。
/// Google や Apple によるはじめてのログインのときに相当する。
Future<void> _maybeCreateWorkerByUserCredential({
required UserCredential userCredential,
}) async {
final user = userCredential.user;
if (user == null) {
// UserCredential
return;
}
final workerExists = await _workerService.workerExists(workerId: user.uid);
if (workerExists) {
return;
}
await _workerService.createWorker(
workerId: user.uid,
displayName: user.displayName ?? '',
);
}

/// [FirebaseAuth] からサインアウトする。
Future<void> signOut() => _auth.signOut();
Future<void> signOut() async {
await _auth.signOut();
await GoogleSignIn().signOut();
}
}
50 changes: 50 additions & 0 deletions packages/mottai_flutter_app/lib/auth/ui/auth_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../../scaffold_messenger_controller.dart';
import '../auth.dart';

final authControllerProvider = Provider.autoDispose<AuthController>(
(ref) => AuthController(
authService: ref.watch(authServiceProvider),
appScaffoldMessengerController:
ref.watch(appScaffoldMessengerControllerProvider),
),
);

class AuthController {
const AuthController({
required AuthService authService,
required AppScaffoldMessengerController appScaffoldMessengerController,
}) : _authService = authService,
_appScaffoldMessengerController = appScaffoldMessengerController;

final AuthService _authService;
final AppScaffoldMessengerController _appScaffoldMessengerController;

/// 選択した [SignInMethod] でサインインする。
Future<void> signIn(SignInMethod authenticator) async {
switch (authenticator) {
case SignInMethod.google:
try {
await _authService.signInWithGoogle();
}
// キャンセル時
on PlatformException catch (e) {
if (e.code == 'network_error') {
_appScaffoldMessengerController
.showSnackBar('接続できませんでした。\nネットワーク状況を確認してください。');
}
_appScaffoldMessengerController.showSnackBar('キャンセルしました。');
}

case SignInMethod.apple:
// Apple はキャンセルやネットワークエラーの判定ができないので、try-catchしない
await _authService.signInWithApple();
case SignInMethod.line:
case SignInMethod.email:
throw UnimplementedError();
}
return;
}
}
46 changes: 46 additions & 0 deletions packages/mottai_flutter_app/lib/auth/ui/sign_in_buttons.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sign_in_button/sign_in_button.dart';

import '../auth.dart';
import 'auth_controller.dart';

class GoogleAppleSignin extends ConsumerWidget {
const GoogleAppleSignin({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('サインインページ'),
),
body: Center(
child: Column(
children: [
// Google
SizedBox(
height: 50,
child: SignInButton(
Buttons.google,
text: 'Google でサインイン',
onPressed: () async => ref
.read(authControllerProvider)
.signIn(SignInMethod.google),
),
),
// Apple
SizedBox(
height: 50,
child: SignInButton(
Buttons.apple,
text: 'Apple でサインイン',
onPressed: () async =>
ref.read(authControllerProvider).signIn(SignInMethod.apple),
),
),
],
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../../../auth/auth.dart';
import '../../../auth/ui/sign_in_buttons.dart';
import '../../../chat/ui/chat_room.dart';
import '../../../map/ui/map.dart';
import '../../../scaffold_messenger_controller.dart';
Expand All @@ -11,6 +12,7 @@ import '../../../user/user_mode.dart';
import '../../color/ui/color.dart';
import '../../sample_todo/ui/sample_todos.dart';


/// 開発中の各ページへの導線を表示するページ。
class DevelopmentItemsPage extends ConsumerWidget {
const DevelopmentItemsPage({super.key});
Expand Down Expand Up @@ -189,15 +191,15 @@ class DevelopmentItemsPage extends ConsumerWidget {
// ),
// ),
),
const ListTile(
title: Text('サインイン (Google, Apple)'),
ListTile(
title: const Text('サインイン (Google, Apple)'),
// TODO: 後に auto_route を採用して Navigator.pushNamed を使用する予定
// onTap: () => Navigator.push<void>(
// context,
// MaterialPageRoute<void>(
// builder: (context) => FooPage(),
// ),
// ),
onTap: () => Navigator.push<void>(
context,
MaterialPageRoute<void>(
builder: (context) => const GoogleAppleSignin(),
),
),
),
const ListTile(
title: Text('サインイン (LINE, Firebase Functions)'),
Expand Down
Loading