Kakao 로그인(카카오 SDK · 서버 커스텀 토큰)과 LINE 로그인(Firebase OIDC) 연결 · SNS 연동 · 인증 취소 처리

This commit is contained in:
Sn-Kinos
2026-10-05 19:01:28 +09:00
parent f483e8d94b
commit c301f3a1db
23 changed files with 1179 additions and 53 deletions
@@ -26,7 +26,8 @@ final class FirebaseAccountLinker implements AccountLinker {
AuthProviderKind.google: 'google.com',
AuthProviderKind.apple: 'apple.com',
// 카카오·LINE 은 Firebase 기본 제공자가 아니라 OIDC 커스텀 제공자로 붙는다.
// 접두사 `oidc.` 는 Firebase 콘솔 등록 시 강제되는 형식이다.
// 접두사 `oidc.` 는 Firebase 콘솔 등록 시 강제되는 형식이다. 카카오는 로그인을
// 서버 커스텀 토큰으로 옮겼지만, 서버(fnAuthKakao)가 같은 `oidc.kakao` 로 연결을 남긴다.
AuthProviderKind.kakao: 'oidc.kakao',
AuthProviderKind.line: 'oidc.line',
AuthProviderKind.email: 'password',
@@ -55,13 +56,16 @@ final class FirebaseAccountLinker implements AccountLinker {
}
final missing = impl.missingConfig;
if (missing.isNotEmpty) return AuthNotConfigured(provider, missing);
// 키 대기 제공자(카카오·LINE)는 자격증명을 만들 수 없다.
if (impl is! CredentialProvider) {
if (impl is! CredentialProvider &&
impl is! OidcSignInProvider &&
impl is! KakaoSignInProvider) {
return AuthNotConfigured(provider, const ['자격증명 구현']);
}
final source = impl as CredentialProvider;
try {
if (impl is KakaoSignInProvider) return await impl.link(user);
if (impl is OidcSignInProvider) return await _linkOidc(user, impl);
final source = impl as CredentialProvider;
final obtained = await source.obtainCredential();
// 사용자가 소셜 창을 닫았다. 실패로 세지 않는다.
if (obtained == null) return const AuthCancelled();
@@ -83,6 +87,7 @@ final class FirebaseAccountLinker implements AccountLinker {
final result = await user.linkWithCredential(obtained.credential);
return AuthSucceeded(_accountOf(result.user, provider));
} on fb.FirebaseAuthException catch (error) {
if (isWebFlowCancelled(error)) return const AuthCancelled();
return AuthFailed(
authCatalogCode(error),
authMessage(error, flow: AuthFlow.link),
@@ -120,6 +125,33 @@ final class FirebaseAccountLinker implements AccountLinker {
}
}
/// 카카오·LINE(OIDC) 연결.
///
/// ponytail: OIDC 웹 흐름은 자격증명을 먼저 내주지 않고 연결까지 한 번에 끝낸다.
/// 그래서 「동일 이메일만 연동」을 **붙인 뒤** 판정하고 어긋나면 바로 뗀다.
/// 떼기가 실패하면 남의 이메일 계정이 붙은 채 남는다 — 막아야 하면 Functions 의
/// blocking 함수(beforeSignIn)로 서버에서 거른다.
Future<AuthOutcome> _linkOidc(fb.User user, OidcSignInProvider impl) async {
final mine = user.email;
final result = await untilAbandoned(
user.linkWithProvider(impl.authProvider),
);
// 연결된 제공자 정보에 그 계정의 이메일이 실린다(동의 항목에 이메일이 있을 때).
final theirs = _emailOf(result.user, impl.providerId);
if (mine != null && theirs != null && !sameAccountIdentity(mine, theirs)) {
await user.unlink(impl.providerId);
return const AuthFailed('E-AUTH-08', '가입하신 이메일과 다른 계정입니다.');
}
return AuthSucceeded(_accountOf(result.user, impl.provider));
}
static String? _emailOf(fb.User? user, String providerId) {
for (final info in user?.providerData ?? const <fb.UserInfo>[]) {
if (info.providerId == providerId) return info.email;
}
return null;
}
SocialAuthProvider? _implOf(AuthProviderKind provider) {
for (final candidate in _providers) {
if (candidate.provider == provider) return candidate;