Skip to content

소셜 로그인 Native, Console 설정 방법

Meeran Kim edited this page Oct 6, 2024 · 2 revisions

해당 페이지에서는 소셜로그인(Google,Github,Apple) + Firebase 기능 작동을 위한 설정법을 안내합니다.

공통(firebase,IOS secretKey 설정)

firebase 설정

  1. firebase console -> Authetication에서 로그인 방법 제공업체 추가
  2. (1번 과정이 안되어있다면 이후에) Google-info.plist, google-servies.json을 다운로드 받아 프로젝트에 적용

🗂️ 폴더 위치

  • [IOS(Google-info.plist)] ios > Runner
  • [Android(google-servies.json)] android > app

IOS Secret key 설정(Google,Apple)

IOS 관련 setting 파일에 민감한 Key를 넣어야하는 경우, Secret.xcconfig를 이용하여 변수 형태로 호출하여 사용할 수 있습니다

  1. Xcode → File → New file 선택
  2. File type으로 Configuration setting file 선택 > Secret으로 이름 지정 ; 다른 이름도 상관없음
  3. 해당 파일에서 변수명=키값 으로 입력하여 저장(공백,"" 없이) 4.Runner의 오른쪽 드롭다운에서 Secret.xcconfig를 선택해줍니다. Screenshot 2024-08-04 at 12 37 22 PM ⚠️ 만약 드롭다운에 Secret이 표시되지 않는다면 해당 파일의 위치가 Runner 내부에 있는지 확인

구글 로그인

IOS 세팅

  • Info.plist에 redirectUrl, clientId 설정 (작성중)

네이버 로그인

Google, Apple, Github 로그인과 달리 네이버 로그인은 파이어베이스에서 직접 지원하지 않습니다. 따라서 네이버 로그인을 파이어베이스 인증과 연동하려면 추가적인 단계가 필요한데, 바로 커스텀 토큰을 생성하는 것입니다. 이 커스텀 토큰은 반드시 서버에서 생성해야 하고, 생성된 토큰은 다시 클라이언트(앱)로 전송되어 파이어베이스 인증을 완료하는 데 사용됩니다.

사용자 관점에서 네이버로 앱에 로그인하는 과정을 보자면 아래와 같습니다.

image
  1. 앱에서 네이버 로그인 버튼 클릭
  2. 네이버 인증 페이지로 이동
  3. 네이버 계정으로 로그인
  4. 서버에서 인증 처리 및 커스텀 토큰 생성
  5. 인증된 상태로 앱으로 복귀 (커스텀 토큰 포함)
  6. 앱에서 받은 커스텀 토큰으로 파이어베이스 인증/앱 로그인 완료

그럼 각 과정을 어떻게 코드로 구현했는지 공유 드리겠습니다.

1. 앱에서 네이버 로그인 버튼 클릭

LoginScreen.dart

SquareTileWidget(
                  onTap: () => SocialAuthService().signInWithNaver(),
                  imagePath: 'assets/login_page_images/naver.png')

로그인 스크린 안에 네이버 로그인 버튼이 있고, 버튼을 누르면 SocialAuthService().signInWithNaver() 함수가 호출됩니다. 이 함수는 네이버 로그인을 위한 인증 페이지로 이동하는 역할을 합니다.

2. 네이버 인증 페이지로 이동

SocialAuthService.dart

  ///Naver Sign In
  Future<void> signInWithNaver() async {
    final remoteConfig = FirebaseRemoteConfig.instance;
    await remoteConfig.fetchAndActivate();

    final String clientId = remoteConfig.getString('naver_client_id');
    final String redirectUri = remoteConfig.getString('naver_redirect_uri');
    final String state = generateNonce();

    final Uri url = Uri.parse(
        'https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=$clientId&redirect_uri=$redirectUri&state=$state');

    try {
      final bool launched =
          await launchUrl(url, mode: LaunchMode.externalApplication);
      if (!launched) {
        talker.error('네이버 로그인 URL 런칭 실패: $url');
        throw Exception('네이버 로그인 URL 런칭 실패');
      }
    } catch (e) {
      talker.error('네이버 로그인 URL 런칭 중 오류 발생: $e');
      throw Exception('네이버 로그인 중 오류 발생: $e');
    }
  }

2-1. 네이버 클라이언트 ID 발급 (네이버 개발자 센터에 앱 등록)

1번 단계에서 signInWithNaver함수가 호출되면 우선 FirebaseRemoteConfig를 사용하여 네이버 클라이언트 ID와 리다이렉트 URI를 가져옵니다. 여기서 FirebaseRemoteConfig를 사용한 이유는 네이버 클라이언트 ID와 리다이렉트 URI를 안전하게 저장하고 관리하기 위함입니다.

네이버 클라이언트 ID는 네이버 개발자 센터에서 발급하는 고유한 애플리케이션 식별자입니다. 네이버 클라이언트 ID를 발급받기 위해서는 아래 단계를 거쳐야 합니다.

  1. 네이버 개발자 센터에 가입 후 애플리케이션 등록 클릭
  2. 애플리케이션 이름 설정 및 사용 API는 네이버 로그인으로 선택
  3. 제공 정보 선택 및 로그인 오픈 API 선택 후 서비스 이용동의
  4. 마지막으로 하단의 등록하기 클릭

위 단계를 거치고 나면 내 애플리케이션 > 내 애플리케이션 이름 클릭 시 Application 목록 아래 Client ID를 확인하실 수 있습니다.

2-2. 리디렉트 URI 설정 (구글 클라우드 함수 생성)

리디렉트 URI는 네이버 인증 후 리디렉트될 주소로, 앞서 말씀드렸듯이, 저희의 경우 서버입니다. 서버를 구축하는 방법은 다양하나, 저희는 node js 서버를 구글 클라우드 함수를 통해 구축하였습니다. 구글 클라우드 함수를 생성하는 방법은 아래와 같습니다.

  1. https://console.cloud.google.com/functions 접속
  2. 함수 생성 버튼 클릭
  3. 함수 이름 설정 (예. naverLogin)
  4. 리전은 us-central1로 설정
  5. 트리거는 HTTPS로 설정
  6. 일단 함수는 업데이트하지 않고 배포 클릭

이렇게 하면 함수가 생성되고, 트리거 탭으로 가면 트리거 URL이 생성된 것을 보실 수 있습니다. 이 트리거 URL이 네이버 로그인 후 리디렉트될 주소가 됩니다.

2-3. 네이버 클라이언트 ID 및 리티렉트 URI 파이어베이스 remote config에 저장

이제 생성한 클라이언트 ID와 리디렉트 URI를 파이어베이스 remote config에 저장합니다.

  1. 파이어베이스 콘솔 접속 후 프로젝트 선택
  2. 좌측 Run탭 아래 Remote Config 클릭
  3. Add Parameter 클릭하여 네이버 클라이언트 ID 및 리디렉트 URI 추가

저희의 경우 네이버 클라이언트 ID는 'naver_client_id', 리디렉트 URI는 'naver_redirect_uri'로 설정하였습니다.

2-4. 네이버 인증 로그인 url 생성

다음으로는 state 값을 생성하여 네이버 인증 로그인 URL을 생성합니다. state는 CSRF 공격을 방지하기 위해 무작위로 만들어내는 문자열입니다. 그 후 Uri.parse()를 사용하여 네이버 OAuth 2.0 인증 URL을 생성합니다. 이 URL에는 클라이언트 ID, 리다이렉트 URI, 그리고 방금 생성한 state 값이 포함됩니다.

URL이 생성되면 launchUrl() 함수를 사용하여 이를 외부 애플리케이션(브라우저)에서 실행합니다.

3. 네이버 계정으로 로그인

이 단계에서는 사용자가 앞서 생성한 네이버 인증 로그인 URL을 통해 네이버 계정으로 로그인을 시작합니다. 사용자는 네이버 로그인 페이지로 이동하고, 네이버 계정의 아이디와 비밀번호를 입력하여 로그인을 완료합니다.

4. 서버에서 인증 처리 및 커스텀 토큰 생성

이제 아까 비워뒀던 구글 클라우드 함수, 즉 서버 코드를 작성합니다. 참고로, 저희는 구글 클라우드에 직접 코드를 작성하지 않고, 대신 IDE에서 코드를 작성한 후 아래 명령어로 배포하였습니다.

firebase deploy --only functions:naverLogin

현재 템플릿 함수에는 네이버 로그인 함수 뿐만 아니라 다른 함수들도 함께 있는데, 이렇게 하면 다른 함수들에는 영향을 주지 않고 네이버로그인 함수만 배포됩니다.

naverLogin은 간략히 말해 네이버 로그인을 Firebase와 연동하는 클라우드 함수로 네이버 인증 코드로 액세스 토큰을 얻고, 이를 통해 사용자 프로필을 가져옵니다. 그 후 Firebase에 사용자를 생성하거나 업데이트하고, Firebase 커스텀 토큰을 생성합니다. 마지막으로 이 토큰과 사용자 정보를 포함한 URL로 앱에 리다이렉트합니다.

이 과정을 통해 네이버 계정으로 로그인한 사용자를 Firebase에서도 인증할 수 있게 됩니다. 자세한 내용은 아래 주석들을 참고해주세요.

naverLogin.js

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const cors = require("cors")({ origin: true });
const fetch = require("node-fetch");

const SOCIALCOMPANY = "naver 로그인을 사용 중 입니다.";

/**  네이버 로그인 요청을 처리하는 Firebase 클라우드 함수*/
exports.naverLogin = functions
  .region("us-central1")
  .https.onRequest((req, res) => {
    cors(req, res, async () => {
      const { code, state } = req.query;

      // 환경 변수에서 클라이언트 ID 및 시크릿 불러오기
      const clientId = functions.config().naver.client_id;
      const clientSecret = functions.config().naver.client_secret;
      const redirectUri = "blueberrytemplate://login-callback";

      try {
        // 네이버 토큰 가져오기
        const accessToken = await fetchNaverToken(
          code,
          state,
          clientId,
          clientSecret,
          redirectUri
        );

        // 가져온 네이버 토큰으로 네이버 유저 프로필 가져오기
        const naverProfile = await fetchNaverUserProfile(accessToken);

        // 네이버 유저 프로필로 파이어베이스 토큰 생성
        const firebaseToken = await createFirebaseToken(naverProfile);

        // 앱으로 복귀
        res.redirect(
          `${redirectUri}?firebaseToken=${firebaseToken}
              &name=${encodeURIComponent(
                naverProfile.name
              )}&profileImage=${encodeURIComponent(naverProfile.profile_image)}`
        );
      } catch (error) {
        console.error("Error processing the authentication:", error);
        res.status(500).send(error.message);
      }
    });
  });

/** 네이버 OAuth 서버에서 토큰 가져오기 */
async function fetchNaverToken(
  code,
  state,
  clientId,
  clientSecret,
  redirectUri
) {
  const tokenUrl = `https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&client_id=${clientId}&client_secret=${clientSecret}&redirect_uri=${redirectUri}&code=${code}&state=${state}`;

  const response = await fetch(tokenUrl);
  const data = await response.json();

  if (!response.ok) {
    throw new Error(
      `Error from Naver: ${response.status} ${response.statusText}`
    );
  }

  if (data.error) {
    throw new Error(
      `Error from Naver: ${data.error} ${data.error_description}`
    );
  }

  return data.access_token;
}

/** 네이버에서 유저 프로파일 가져오기 */
async function fetchNaverUserProfile(accessToken) {
  const profileUrl = "https://openapi.naver.com/v1/nid/me";

  const response = await fetch(profileUrl, {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
  const data = await response.json();

  if (!response.ok) {
    throw new Error(
      `Error fetching Naver user profile: ${response.status}
        ${response.statusText}`
    );
  }

  if (data.error) {
    throw new Error(
      `Error from Naver: ${data.error} ${data.error_description}`
    );
  }

  console.log("Got the data from Naver");

  return data.response;
}

/** 네이버 유저 정보를 기반으로 파이어베이스 Auth 및 파이어스토어 유저 생성 및 파이어베이스 토큰 생성 */
async function createFirebaseToken(naverProfile) {
  const uid = naverProfile.id;
  const db = admin.firestore();
  const userRef = db.collection("users").doc(uid);

  try {
    const userDoc = await userRef.get();

    if (!userDoc.exists) {
      // Firebase Auth와 Firestore에 사용자 생성
      await createUserInFBAuth(uid, naverProfile);
      await createUserInFS(userRef, naverProfile);
    } else {
      // Firestore에 사용자 정보가 존재하는 경우 업데이트 처리
      await updateUserInFBAuth(uid, naverProfile);
      await updateUserInFS(userRef, naverProfile);
    }
  } catch (error) {
    console.error("Error creating or updating user in Firestore:", error);
    throw error;
  }

  return admin.auth().createCustomToken(uid);
}

/** 파이어베이스 Auth 유저 생성 */
async function createUserInFBAuth(uid, naverProfile) {
  await admin.auth().createUser({
    uid: uid,
    displayName: naverProfile.name,
    photoURL: naverProfile.profile_image,
    email: naverProfile.email,
  });
}

/** 파이어스토어 유저 생성 */
async function createUserInFS(userRef, naverProfile) {
  await userRef.set({
    name: naverProfile.name,
    profilePicture: naverProfile.profile_image,
    nickName: naverProfile.nickname,
    email: naverProfile.email,
    socialLogin: true,
    socialCompany: SOCIALCOMPANY,
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
  });
}

/** 파이어베이스 Auth 유저 업데이트 */
async function updateUserInFBAuth(uid, naverProfile) {
  await admin.auth().updateUser(uid, {
    nickName: naverProfile.name,
    profilePicture: naverProfile.profile_image,
    email: naverProfile.email,
  });
}

/** 파이어스토어 유저 업데이트 */
async function updateUserInFS(userRef, naverProfile) {
  await userRef.update({
    name: naverProfile.name,
    profilePicture: naverProfile.profile_image,
    nickName: naverProfile.nickname,
    email: naverProfile.email,
    lastLogin: admin.firestore.FieldValue.serverTimestamp(),
  });
}

5. 인증된 상태로 앱으로 복귀 (커스텀 토큰 포함)

위 서버에서 redirectUri를 "blueberrytemplate://login-callback"로 설정하였는데, 여기서 blueberrytemplate은 앱의 스키마입니다. 앱의 스키마란 앱을 식별하는 고유한 식별자로, 앱마다 다르게 설정할 수 있습니다. 여러분은 여러분의 앱 식별자로 설정해주시면 됩니다. 설정하시고 나면, Android와 iOS 모두 앱 식별자를 설정해주어야 합니다.

Android의 경우 AndroidManifest.xml에 앱 식별자를 설정해주어야 합니다. 아래 코드를 태그 포함하여 추가해주시면 됩니다. (여러 개의 intent-filter가 있을 수 있음)

android>app>src>main>AndroidManifest.xml

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="blueberrytemplate" />
</intent-filter>

iOS의 경우 Info.plist에 앱 식별자를 설정해주어야 합니다. 아래 코드를 태그 포함하여 추가해주시면 됩니다.

ios>Runner>Info.plist

<dict>
  <key>CFBundleURLTypes</key>
   <array>
      <dict>
        <key>CFBundleURLSchemes</key>
        <array>
          <string>blueberrytemplate</string>
        </array>
      </dict>
  </array>
</dict>

6. 앱에서 받은 커스텀 토큰으로 파이어베이스 인증/앱 로그인 완료

마지막 단계는 앱에서 받은 커스텀 토큰을 사용하여 파이어베이스 인증을 완료하고 앱 로그인을 완료하는 것입니다.

로그인 화면에서 앱이 시작될 때 초기 딥 링크를 확인하고, 실시간으로 들어오는 딥 링크도 처리합니다. 딥 링크에 포함된 Firebase 토큰, 사용자 이름, 프로필 이미지 URL을 추출하여 Firebase 인증을 완료하고 사용자 정보를 업데이트합니다.

이 과정은 initAppLinks(), handleDeepLink() 함수를 통해 이루어지며, initState()에서 초기 설정과 실시간 딥 링크 구독을 시작합니다. 마지막으로 dispose() 메서드에서 딥 링크 스트림 구독을 정리합니다.

자세한 내용은 아래 코드 및 주석을 참고해주세요.

LoginScreen.dart

class LoginScreen extends ConsumerStatefulWidget {
  const LoginScreen({super.key});

  @override
  LoginScreenState createState() => LoginScreenState();
}

class LoginScreenState extends ConsumerState<LoginScreen> {
  StreamSubscription? _sub;

  //initAppLinks()는 초기 링크만 처리하고, 실시간 딥 링크 구독은 initState()에서 처리
  Future<void> initAppLinks() async {
    final appLinks = AppLinks();
    final initialLink = await appLinks.getInitialLink();
    if (initialLink != null) {
      try {
        handleDeepLink(initialLink); // 초기 딥링크 처리
      } catch (e) {
        talker.error("초기 딥 링크 처리 중 오류 발생: $e");
      }
    }
  }

  Future<void> handleDeepLink(Uri uri) async {
    try {
      if (uri.authority == 'login-callback') {
        final firebaseToken = uri.queryParameters['firebaseToken'];
        final String? name = uri.queryParameters['name'];
        final String? profileImage = uri.queryParameters['profileImage'];

        if (firebaseToken != null) {
          UserCredential userCredential =
           await FirebaseAuth.instance.signInWithCustomToken(firebaseToken);
           await userCredential.user?.updateDisplayName(name);
           await userCredential.user?.updatePhotoURL(profileImage);
        } else {
          throw Exception('Firebase token이 존재하지 않습니다.');
        }
      }
    } catch (e) {
      talker.error("딥 링크 처리 중 오류 발생: $e");
    }
  }

  @override
  void initState() {
    super.initState();
    initAppLinks(); // 초기 딥 링크 처리

    // 실시간 딥 링크 구독
    _sub = AppLinks().uriLinkStream.listen((Uri? link) {
      if (link != null) {
        handleDeepLink(link);
      }
    }, onError: (err) {
      talker.error("딥 링크 수신 중 오류 발생: $err");
    });
  }

  @override
  void dispose() {
    _sub?.cancel(); // 구독 해제
    super.dispose();
  }

  ... 나머지 코드 ...
}