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

Conversation

RikitoNoto
Copy link
Collaborator

@RikitoNoto RikitoNoto commented Jul 20, 2023

Issue

close #45

説明

GoogleとAppleアカウントを使用してログインを行う処理を作成しました。

作成した処理

  • GoogleとAppleアカウントでOAuthを使用してログイン処理
  • OAuthを使用してログイン時、Workerが未作成の場合は新規で作成する処理

UI

Googleでのログイン

Simulator Screen Recording - iPhone 14 Pro Max - 2023-07-20 at 23 16 34

Appleでのログイン

Simulator Screen Recording - iPhone 14 Pro Max - 2023-07-20 at 23 18 38

その他

仕様の確認として、同じメールアドレスの Google アカウントと Appple ID アカウントでサインイン・サインアウトすると Firebase Auth のユーザーはどのような扱いになるかを確認してほしい(別アカウント扱いになるのか、自動的に同じアカウント扱いになるのか)

ログインユーザーのuidをドキュメントIDとしてworkerを作成しています。
そのためアカウントごとにユーザ作成となります。
(複数アカウントで一つのworkerの場合はどのように紐づけをするか、仕様の検討が必要かと思いますので相談させてください。)

チェックリスト

  • PR の冒頭に関連する Issue 番号を記載しましたか?

  • 本 PR の変更に関して、エディタや IDE で意図しない警告は増えていませんか?(lint 警告やタイポなど)
    今回対応の部分でlint警告はありませんでした。
    image

  • Issue の完了の定義は満たせていますか?

@RikitoNoto RikitoNoto added the 1. 機能開発 機能開発 label Jul 20, 2023
Comment on lines 23 to 24
SealedTimestamp createdAt = const ServerTimestamp(),
SealedTimestamp updatedAt = const ServerTimestamp(),
Copy link
Owner

Choose a reason for hiding this comment

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

flutterfire_gen の内部のことになるので説明が足りておらずすみませんすが、この生成物:

https://github.com/KosukeSaigusa/mottai-flutter-app/blob/a9a3327e053f2f3de24d297810b343c0bd77d787/packages/firebase_common/lib/src/firestore_documents/worker.flutterfire_gen.dart#L79-L81

のように、指定せずとも勝手に createdAt と updatedAt はフィールドに入る気がします!

一応確認してもらって大丈夫そうだったらそうしてください!(このファイルで flutterfire_json_converters.dart を import する必要もなくなりますね 👍)

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
おっしゃる通り自動で値が入りますので削除しました!



/// [Worker] を作成する。
Future<void> set({
Copy link
Owner

Choose a reason for hiding this comment

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

[軽微]

fetchWorker に合わせて setWorker が良さそうですね!

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
ご指摘ありがとうございます!
修正しました!


static final _auth = FirebaseAuth.instance;
final ProviderRef<dynamic> _ref;
Copy link
Owner

Choose a reason for hiding this comment

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

Riverpod の使い方としてはとても正しいです!が、事前に読んでもらった

https://github.com/KosukeSaigusa/besso-log-book#%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

の規約に従って、ref を丸ごと渡すのは禁止にしようと思っています!🙏(底に書いてあるとおり、冗長になって Riverpod の良さは少し失ってしまうのは分かりつつ、分かり易さをとって、AuthService の中で使用するインスタンスはコンストラクタで直接渡してください)

例:

https://github.com/KosukeSaigusa/mottai-flutter-app/blob/a4e1e5e38464c074479ce4ad8c79585c32e06ce7/packages/mottai_flutter_app/lib/development/sample_todo/sample_todos.dart#L28

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
修正しました。
後学のためにご存じであればご教示いただきたいのですが、refを受け取らずにAuthServiceクラス内からStateProviderの値を更新したり参照したりする方法はあるのでしょうか?
(いろいろ調べたんですが、わかりませんでした。)

isHost: isHost,
);

}
Copy link
Owner

Choose a reason for hiding this comment

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

[軽微] ファイル末尾には空行入れるのをお願いします!(エディタでそういう設定ができると思います、ぜひしてください!)

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
すみません、設定していなかったです。。
修正しました!


/// OAuthを使用したサインイン
Future<void> signInOauth(Authenticator authenticator) async {
switch(authenticator){
Copy link
Owner

Choose a reason for hiding this comment

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

[軽微(だけど必須で直したい)]

このあたり、通常の lint format がかかると switch (authenticator) { という感じで空白が必ず入ると思いますが、エディタの設定はうまくいっていそうですか?(他の箇所でも多数そういうものがありそうです)

私を含む他のメンバーが command + S をすると差分が出てしまうので、直していただきたいです!🙏

もし困ったらサポートします!

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
すみません、そんな設定があるなんて知りませんでした。
助かりました!修正しました!

Comment on lines 34 to 39
final googleUser = await GoogleSignIn().signIn(); // サインインダイアログの表示
final googleAuth = await googleUser?.authentication; // アカウントからトークン生成
if(googleAuth != null){
await _authService.signInWithGoogle(googleAuth: googleAuth);
return ;
}
Copy link
Owner

Choose a reason for hiding this comment

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

この処理はコントローラではなくモデルに移すべきに思います。

コントローラでは単にユーザー操作から

  • ユーザー操作;「A という認証プロバイダでログインしたいです」
  • コントローラ:「モデルのそのメソッド呼びますね(必要ならエラーハンドリングして SnackBar を出したりもします)」
  • モデル:「A という認証プロバイダでのログイン」という振る舞いをします

という感じの役割分担でリファクタできないでしょうか?

また、その際、モデルのコードは公式ドキュメント:

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

import 'package:google_sign_in/google_sign_in.dart';

Future<UserCredential> 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,
  );

  // Once signed in, return the UserCredential
  return await FirebaseAuth.instance.signInWithCredential(credential);
}

とほぼ一致するようになると思います!

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
修正しました!

 GoogleSignIn().signIn()

はUIの表示を行うメソッドでしたので、コントローラに置いていました!

Comment on lines 55 to 59
// キャンセル時
on FirebaseAuthException catch(_){
// Appleはネットワークエラーとキャンセル時の判断ができないため、スナックバーは表示しない
return ;
}
Copy link
Owner

Choose a reason for hiding this comment

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

何もしないならキャッチしなくていいと思います!

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
削除しました!

),
);
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

[軽微] ファイル末尾には空行入れるのをお願いします!(エディタでそういう設定ができると思います、ぜひしてください!)

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
修正しました!

final UserCredential userCredential;
userCredential = await _auth.signInWithProvider(appleProvider);
await _signIn(Authenticator.apple, 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 ログインはしない前提でいきましょう!変更ありがとうございます!!

Future<void> signOut() async {
await _auth.signOut();
if(_ref.read(authenticatorProvider) == Authenticator.google){
await GoogleSignIn().signOut();
Copy link
Owner

Choose a reason for hiding this comment

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

公式ドキュメントなどで GoogleSignIn().signOut() の必要性は言及されていそうですか?

されていない場合、実際にこれはどのくらい必要かしりたいです!

もしなくても良さそうなら、authenticatorProvider 自体が不要になるのでは?ともぱっと見ですが思っています

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
特に公式では言及されていませんでした。
ただ、この処理を実行しないとアプリケーションとアカウントの紐づけが解除されません。
そのため、

  1. Googleでログイン
  2. アプリのサインアウトボタン押下
  3. 再度Googleでログインボタン押下→アカウントを選ぶことなく即ログイン

という形になってしまいます。

1アカウントだけでしたら上記は便利ですが、別のGoogleアカウントでログインしたいとなった際に
切り替えができず不便かもしれないと思い追加していました。

・コードのフォーマット修正
・ProviderRefをコンストラクタに渡さないように修正
・Appleのログイン方法をflutterFireを使用するやり方に変更
・Googleのログイン処理をすべてServiceに移動
@RikitoNoto
Copy link
Collaborator Author

@kosukesaigusa
ご確認ありがとうございます!
修正しましたので再度ご確認をお願いします!

enum Authenticator {
none,
/// 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」に名前を合わせました。

/// そのため、以下のような条件で判定を行う。
/// [UserInfo.providerId] : 'google.com' => google認証
/// [UserInfo.providerId] : 'apple.com' => apple認証
final authenticatorProvider = Provider<Authenticator>((ref) {
Copy link
Owner

Choose a reason for hiding this comment

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

Google で明示的にサインアウトするために使用していると思いますが、後述の方法で、どの SignInMethod でサインインしたかの記録は特にしない方針に(今のところは)しようと思ったので消しました!(他に必要性が出てきたら復活見当します)

@@ -96,7 +66,8 @@ class AuthService {
}) =>
_auth.signInWithEmailAndPassword(email: email, password: password);

/// Googleでのサインイン
/// [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 の公式ドキュメントに従っていることを明記しました。

Comment on lines 144 to 150
// ignore: avoid_catches_without_on_clauses
} catch (_) {
// TODO: Google でサインインしていない場合に上記の GoogleSignIn().signOut();
// をコールして例外やエラーが発生しうるか調べる。
// もし発生しないならこの try catch 句の記述を省略する。
// もし発生するなら、この TODO コメントを破棄して、代わりに意図的にそこで
// 発生する例外やエラーを握り潰していることをコメントで明記する。
Copy link
Owner

Choose a reason for hiding this comment

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

@RikitoNoto
ここにコメントを書いた内容の対応をしていただきたいです!🙏
簡単に言うと、

Google でログインしていないのに GoogleSignIn().signOut() をした場合に

  • 例外やエラーが出る → 握りつぶす
  • 例外やエラーが出る → 特に何もしない

でいいんじゃないかなと思っています!


/// サインイン中のユーザーに対応するワーカードキュメントが存在するかどうか判定する
/// `Future<bool> Function()` 型の関数を提供する [Provider].
final workerDocumentExistsProvider =
Copy link
Owner

Choose a reason for hiding this comment

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

既存の host で似たような実装(それが理想的かは 100% 自身はないです)があったので真似たのだと思うのですが、そちらは main の処理で、ホストドキュメントの存在を確認して、ProviderScope.overrides で UserModeStateProvider の初期値を決めるための実装でした(つまり ref が必要で、トップレベルのメソッドして定義したかった)。

今回は普通に WorkerRepository にそのようなメソッドをはやしました。

Copy link
Owner

Choose a reason for hiding this comment

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

せっかく書いてもらいましたが、FlutterFire の公式の方法にただ従っていること、private メソッドにすれば(その private メソッドを使用するテストが書かれていれば)テストを本来書かなくていいこと(書いていないけど)、などから消しました。

本当にやるとしたら、AuhService の static メソッドではなく、visibleForTesting な private メソッド化、シンプルに lib 直下の適切なファイルにトップレベルのメソッドして配置してやるのが自然かもなとも思います!(public な static メソッドにすると、他のクラスからも呼ぶことがあるのかないのか不明確になるなどのデメリットがあるなと私は思っています)

@RikitoNoto RikitoNoto merged commit e74ad82 into main Jul 23, 2023
1 check passed
@RikitoNoto RikitoNoto deleted the 45_create_oauth branch July 23, 2023 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1. 機能開発 機能開発
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Google/Apple でのサインインができるようにする
2 participants