소셜 로그인 인증 경로 · 세션 전이 · Apple 로그인 콜백 · 인증 테스트
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as fb;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart' as gsi;
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart' as apple;
|
||||
|
||||
@@ -12,6 +16,8 @@ import 'auth.dart';
|
||||
final class GoogleSignInProvider implements SocialAuthProvider {
|
||||
const GoogleSignInProvider();
|
||||
|
||||
static Future<void>? _initialization;
|
||||
|
||||
@override
|
||||
SocialProvider get provider => SocialProvider.google;
|
||||
|
||||
@@ -32,14 +38,16 @@ final class GoogleSignInProvider implements SocialAuthProvider {
|
||||
try {
|
||||
final signIn = gsi.GoogleSignIn.instance;
|
||||
// clientId/serverClientId 는 설정 파일에서 읽힌다 — 여기서 하드코딩하지 않는다.
|
||||
await signIn.initialize();
|
||||
await (_initialization ??= signIn.initialize());
|
||||
final account = await signIn.authenticate();
|
||||
final idToken = account.authentication.idToken;
|
||||
if (idToken == null) {
|
||||
return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.');
|
||||
}
|
||||
final credential = fb.GoogleAuthProvider.credential(idToken: idToken);
|
||||
final result = await fb.FirebaseAuth.instance.signInWithCredential(credential);
|
||||
final result = await fb.FirebaseAuth.instance.signInWithCredential(
|
||||
credential,
|
||||
);
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
} on gsi.GoogleSignInException catch (error) {
|
||||
if (error.code == gsi.GoogleSignInExceptionCode.canceled) {
|
||||
@@ -54,8 +62,9 @@ final class GoogleSignInProvider implements SocialAuthProvider {
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
if (!FirebaseBoot.isLive) return;
|
||||
await (_initialization ??= gsi.GoogleSignIn.instance.initialize());
|
||||
await gsi.GoogleSignIn.instance.signOut();
|
||||
await fb.FirebaseAuth.instance.signOut();
|
||||
// Firebase 세션 종료는 AuthSession이 한 번만 담당한다.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +74,11 @@ final class GoogleSignInProvider implements SocialAuthProvider {
|
||||
/// Apple Developer 콘솔의 Service ID + redirect URI 를 각각 요구한다.
|
||||
/// 둘 다 스토어 계정 명의가 정해져야(Q5) 발급할 수 있어 지금은 설정 파일 게이트에서 끊긴다.
|
||||
final class AppleSignInProvider implements SocialAuthProvider {
|
||||
const AppleSignInProvider();
|
||||
const AppleSignInProvider({this.webAuthenticationOptions});
|
||||
|
||||
/// Android·Web에서 필요한 Service ID와 검증된 callback URL.
|
||||
/// 값이 없으면 SDK를 호출하지 않고 [AuthNotConfigured]로 닫는다.
|
||||
final apple.WebAuthenticationOptions? webAuthenticationOptions;
|
||||
|
||||
@override
|
||||
SocialProvider get provider => SocialProvider.apple;
|
||||
@@ -74,9 +87,12 @@ final class AppleSignInProvider implements SocialAuthProvider {
|
||||
bool get isLive => true;
|
||||
|
||||
@override
|
||||
List<String> get missingConfig => FirebaseBoot.isLive
|
||||
? const []
|
||||
: [...FirebaseBoot.missingLabels, 'Apple 개발자 설정'];
|
||||
List<String> get missingConfig => [
|
||||
if (!FirebaseBoot.isLive) ...FirebaseBoot.missingLabels,
|
||||
if (_requiresWebAuthentication && webAuthenticationOptions == null)
|
||||
'Apple Android 웹 인증 설정',
|
||||
if (!FirebaseBoot.isLive) 'Apple 개발자 설정',
|
||||
];
|
||||
|
||||
@override
|
||||
Future<AuthOutcome> signIn() async {
|
||||
@@ -87,11 +103,17 @@ final class AppleSignInProvider implements SocialAuthProvider {
|
||||
return const AuthFailed('E-AUTH-03', '이 기기에서는 Apple 로그인을 쓸 수 없습니다.');
|
||||
}
|
||||
try {
|
||||
final rawNonce = apple.generateNonce();
|
||||
final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString();
|
||||
final credential = await apple.SignInWithApple.getAppleIDCredential(
|
||||
scopes: const [
|
||||
apple.AppleIDAuthorizationScopes.email,
|
||||
apple.AppleIDAuthorizationScopes.fullName,
|
||||
],
|
||||
webAuthenticationOptions: _requiresWebAuthentication
|
||||
? webAuthenticationOptions
|
||||
: null,
|
||||
nonce: hashedNonce,
|
||||
);
|
||||
final idToken = credential.identityToken;
|
||||
if (idToken == null) {
|
||||
@@ -100,6 +122,7 @@ final class AppleSignInProvider implements SocialAuthProvider {
|
||||
final oauth = fb.OAuthProvider('apple.com').credential(
|
||||
idToken: idToken,
|
||||
accessToken: credential.authorizationCode,
|
||||
rawNonce: rawNonce,
|
||||
);
|
||||
final result = await fb.FirebaseAuth.instance.signInWithCredential(oauth);
|
||||
return AuthSucceeded(_toAccount(result.user, provider));
|
||||
@@ -108,6 +131,9 @@ final class AppleSignInProvider implements SocialAuthProvider {
|
||||
return const AuthCancelled();
|
||||
}
|
||||
return AuthFailed('E-AUTH-03', error.message);
|
||||
} on apple.SignInWithAppleException catch (error) {
|
||||
debugPrint('[AppleSignInProvider] failed: ${error.runtimeType}');
|
||||
return const AuthFailed('E-AUTH-03', '소셜 로그인에 실패했습니다.');
|
||||
} on fb.FirebaseAuthException catch (error) {
|
||||
return AuthFailed('E-AUTH-03', error.message ?? error.code);
|
||||
}
|
||||
@@ -115,9 +141,11 @@ final class AppleSignInProvider implements SocialAuthProvider {
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
if (!FirebaseBoot.isLive) return;
|
||||
await fb.FirebaseAuth.instance.signOut();
|
||||
// Apple SDK에는 앱 로컬 로그아웃 API가 없다. Firebase 세션은 AuthSession이 닫는다.
|
||||
}
|
||||
|
||||
bool get _requiresWebAuthentication =>
|
||||
kIsWeb || defaultTargetPlatform == TargetPlatform.android;
|
||||
}
|
||||
|
||||
/// 앱 키가 없어 아직 붙이지 못한 제공자 — 카카오·네이버.
|
||||
@@ -137,12 +165,32 @@ final class KeylessSocialProvider implements SocialAuthProvider {
|
||||
bool get isLive => false;
|
||||
|
||||
@override
|
||||
Future<AuthOutcome> signIn() async => AuthNotConfigured(provider, missingConfig);
|
||||
Future<AuthOutcome> signIn() async =>
|
||||
AuthNotConfigured(provider, missingConfig);
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {}
|
||||
}
|
||||
|
||||
/// Apple Service ID와 callback URL은 비밀값이 아니지만 환경별 값이다.
|
||||
/// 저장소에 임시 상수를 박지 않고 빌드 시 `--dart-define`으로만 받는다.
|
||||
apple.WebAuthenticationOptions? appleWebAuthenticationOptionsFromEnvironment() {
|
||||
const clientId = String.fromEnvironment('FANUZU_APPLE_SERVICE_ID');
|
||||
const redirect = String.fromEnvironment('FANUZU_APPLE_REDIRECT_URI');
|
||||
if (clientId.isEmpty || redirect.isEmpty) return null;
|
||||
|
||||
final redirectUri = Uri.tryParse(redirect);
|
||||
if (redirectUri == null ||
|
||||
!redirectUri.hasScheme ||
|
||||
!redirectUri.hasAuthority) {
|
||||
return null;
|
||||
}
|
||||
return apple.WebAuthenticationOptions(
|
||||
clientId: clientId,
|
||||
redirectUri: redirectUri,
|
||||
);
|
||||
}
|
||||
|
||||
AuthAccount _toAccount(fb.User? user, SocialProvider provider) => AuthAccount(
|
||||
uid: user?.uid ?? '',
|
||||
provider: provider,
|
||||
|
||||
Reference in New Issue
Block a user