[Android] Kakao 계정으로 Firebase Authentication 연동







Firebase의 Authentication은 손쉽게 로그인 기능을 구현해준다. Google, Facebook, Twitter, Github 등의 계정으로 사용자 인증을 가능하게 해준다.

위에 사진을 보면 어떤 로그인 제공업체가 있는지 알 수 있다. 익숙한 제공업체들이 보이는데 나는 Kakao 계정으로 Firebase와 연동하는게 필요했다. 공식문서를 뒤지고 구글링을 하니 커스텀 토큰을 만들어서 Firebase Authentication의 인증을 받을 수 있는게 있다고 한다.

오..근데 문제가 있다.

커스텀 토큰을 만들때 JWT라는 토큰을 생성하고 다시 클라이언트(앱 사용자)에게 전달해줘야하는 서버가 필요한 것이다.



주니어 안드로이드 개발자로서 백엔드는 생소할 수 있다. 그래서 최대한 쉽게 설명하고자 한다.

참고로 JavaScript와 TypeScript가 아직 익숙하지 않아서 이상한 부분이 있다면 댓글로 알려주세요.







방법 소개


Firebase Authentication과 Kakao 로그인을 연동한다면, 무조건적으로 Firebase를 쓴다는 말이다. Firebase는 serverless다. 서버 없이도 서비스를 구현할 수 있게 도와준다는 말이다. 당연하게도 백엔드 서버처럼 함수 호출을 할 수 있는 기능을 제공해준다. 바로 Firebase Functions이다. 우린 바로 이것을 사용할 것이다.

javaScript 파일로 함수를 만들어서 배포하면 API 서버처럼 Firebase Functions으로 함수 호출이 가능하다. 이와 비슷한 제품으로는 AWS의 AWS Lambda가 있다.

다만, Firebase Functions 기능을 쓰려면 무료 요금제(spark)로는 안된다. 요금제 업그레이드(Blaze)가 필요하다.








프로세스



출처 : 초고강도 수정


위에 사진은 카카오 인증부터 Firebase Functions을 사용한 JWT 반환 후 Authentication에 인증하는 과정이다. 

처음에 JWT 반환하는 함수 만들 때 4번, 5번 과정을 생략해서 다시 만들었다.



단계별 설명

1. 카카오 인증 요청 사용자가 이제 카카오 계정으로 로그인하기 위해 카카오 로그인 API에서 사용자 인증을 요청한다.

2. Access Token 반환 사용자의 인증 요청을 받은 카카오 서버는 사용자가 있는지 확인하고, 사용자의 Access Token을 반환해준다.

3. Access Token 전달 Firebase Functions의 JWT을 반환해주는 함수를 호출할 때 카카오 서버에서 반환받은 Access Token을 같이 전달한다.

4. Access Token으로 사용자 정보 요청 Firebase Functions에서는 전달받은 Access Token으로 유효한 토큰인지 확인하며 Access Token의 사용자 정보를 요청한다.

5. 사용자 정보 반환 카카오 서버는 Access Token으로 사용자 정보를 조회하고 값을 넘겨준다.

6. 사용자 정보 조회 Firebase Functions은 반환된 사용자 정보의 파라미터 값으로 Firebase Authentication에 등록되어있는 사용자를 업데이트한다.

7. 사용자 계정 등록 유무 반환 사용자 정보 파라미터 값으로 Authentication 사용자를 업데이트하는 데, 업데이트가 되면 업데이트된 값이 반환된다. 등록된 사용자가 없어서 업데이트가 안되면 에러를 반환한다.

8. JWT 생성 업데이트된 값이 반환되면 그대로 JWT를 생성하고, 에러값이 반환되어오면 Authentication 사용자를 생성 후, JWT를 생성한다.

9. JWT 반환 생성한 JWT를 사용자에게 반환해준다.

10. JWT로 인증 요청 Firebase Authentication에 반환받은 JWT로 로그인 요청을 한다.

11. 인증 결과 반환 로그인 요청 결과를 반환한다.














준비


준비물

구현하기 전 준비할게 있다. 아래 과정은 이번 글에서는 생략할것이다.

  • Android Studio Project
    • Firebase(Authentication, Firestore) 세팅 (필수)


  • Firebase Console
    • Android 앱 Project 생성(필수)
    • Authentication 추가 (필수)
    • Firestore 추가(필수)
    • App 서명 인증서 (SHA-1) 등록 (필수)
    • 요금제 업그레이드 (Spark -> Blaze) (필수)


  • KaKao Developers
    • 내 애플리케이션 등록(필수)
    • 카카오 로그인 API 관련 설정 (필수)
    • KaKao Developers






주의사항

  • 해당 Firebase Functions으로 배포한 함수에 쓰인 Library 중 Request는 deprecated되었다. 추후에 다른 걸로 바꿀 예정이다.
  • 여기서 사용한 KakaoLogin API는 v2다. 만약에 v1을 사용하고 있다면 Functions에 배포한 kakao request url 주소를 수정하고 리턴받는 값도 수정해야한다. 내 기억으로는 account 구조가 달랐던거 같다.






Firebase Functions으로 백엔드 설정 (OS : Mac)


npm 및 firebase-tools 설치

// npm 설치
brew install npm

// firebase-tool 설치
npm install -g firebase-tools




firebase 초기화 및 functions 세팅

1. 프로젝트 초기화를 위해 로그인을 먼저 해야한다. 터미널에 아래 명령어 입력 후 엔터치면 브라우저에서 로그인하기

firebase login

2. Firebase 프로젝트 디렉터리 만든 후 해당 디렉터리로 이동

// firebase 프로젝트 디렉터리 생성
mkdir /Users/crystal/firebase

// 디렉터리로 이동
cd /Users/crystal/firebase

3. 터미널에 firebase init functions를 입력하면 창이 뜰텐데 보고 답 입력하면 된다. (사실 이 부분을 날려서 나중에 한번 다시 설치하게 되면 추가할 예정이다)


4. 터미널에 Firebase initialization complete!라는 문구가 출력되면 우리가 firebase 초기화한 경로에 functions이라는 폴더와 몇개의 파일이 생성된다.



5. [Firebase Console > 톱니바퀴 > 프로젝트 설정] 클릭





6. [서비스 계정 > 새 비공개 키 생성] 클릭하면 새 비공개 키 생성이라는 팝업창이 뜨는데 키 생성 버튼 클릭한다. 그러면 확장자가 json인 파일이 다운로드 될것이다.




7. 다운로드한 json 파일의 파일명을 admin.json으로 바꾸고 firebase를 초기화한 프로젝트의 폴더에 있는 'functions' 폴더 하위로 이동시킨다.










커스텀 토큰 생성 함수 구현 - JavaScript



1. 'functions'폴더 하위에 보면 index.js 파일을 편집기로 연다. 메모장으로 열어도 되고, Visual Studio Code로 열어도 된다. 난 Visual Studio Code로 열었다. 아래가 index.js 파일의 내용이다. 어떻게 함수를 짜면 되는지 설명이 나와있다.


const functions = require("firebase-functions");
// // Create and deploy your first functions
// // https://firebase.google.com/docs/functions/get-started
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
//   functions.logger.info("Hello logs!", {structuredData: true});
//   response.send("Hello from Firebase!");
// });

2. 8번의 index.js 파일 내용을 아래로 바꿔준다. 기존 파일 내용 지우고 아래 내용 복사해서 붙여넣으면 된다.


const functions = require('firebase-functions')
const admin = require('firebase-admin')
const serviceAccount = require("./admin.json")
const { auth } = require('firebase-admin')
const { error } = require('firebase-functions/logger')

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
});

const db = admin.firestore()

const request = require('request-promise')

const kakaoRequestMeUrl = 'https://kapi.kakao.com/v2/user/me'

function requestMe(kakaoAccessToken) {
    console.log('Requesting user profile from Kakao API server.')
    return request({
        method: 'GET',
        headers: { 'Authorization': 'Bearer ' + kakaoAccessToken },
        url: kakaoRequestMeUrl,
    })
}

function updateOrCreateUser(userId, email, displayName, photoURL) {
    console.log('updating or creating a firebase user');
    const updateParams = {
        provider: 'KAKAO',
        displayName: displayName,
        email: email,
    };
    if (displayName) {
        updateParams['displayName'] = displayName;
    } else {
        updateParams['displayName'] = email;
    }
    if (photoURL) {
        updateParams['photoURL'] = photoURL;
    }
    console.log(updateParams);
    return admin.auth().updateUser(userId, updateParams)
        .catch((error) => {
            if (error.code === 'auth/user-not-found') {
                updateParams['uid'] = userId;
                if (email) {
                    updateParams['email'] = email;
                }
                return admin.auth().createUser(updateParams);
            }
            throw error;
        });
}

function createFirebaseToken(kakaoAccessToken) {
    return requestMe(kakaoAccessToken).then((response) => {
        const body = JSON.parse(response)
        console.log(body)
        const userId = `kakao:${body.id}`
        if (!userId) {
            throw new functions.https.HttpsError('invalid-argument', 'Not response: Failed get userId');
        }

        let nickname = null
        let profileImage = null
        let email = null
        if (body.properties) {
            nickname = body.properties.nickname
            profileImage = body.properties.profile_image
        }
        if (body.kakao_account) {
            email = body.kakao_account.email
        }
        return updateOrCreateUser(userId, email, nickname,
            profileImage)
    }).then((userRecord) => {
        const userId = userRecord.uid
        console.log(`creating a custom firebase token based on uid ${userId}`)
        return admin.auth().createCustomToken(userId, { provider: 'KAKAO' })
    })
}

exports.kakaoCustomAuth = functions.region('asia-northeast3').https
    .onCall((data) => {
        const token = data.token

        if (!(typeof token === 'string') || token.length === 0) {
            throw new functions.https.HttpsError('invalid-argument', 'The function must be called with ' +
                'one arguments "data" containing the token to add.');
        }

        console.log(`Verifying Kakao token: ${token}`)

        return createFirebaseToken(token).then((firebaseToken) => {
            console.log(`Returning firebase token to user: ${firebaseToken}`)
            return { "custom_token": firebaseToken };
        })

    })

3. 만든 함수를 배포하기위해 functions의 상위 디렉토리에서 터미널을 열고 아래 명령어를 치면 된다. functions만 배포하겠다는 의미다.


firebase deploy --only functions

    
    아래 사진이 배포가 성공됐을 때 사진이다.

    i, 체크로 표시되는건 상관없는데 세모 경고 표시되는건 조심해서 봐야된다. 지금 경고로 뜬거는 firebase-functions를 업그레이드하라는 거라 상관없는데 다른 경고 표시는 메세지를 잘 봐야된다.

    아래 사진에서 배포된 functions은 kakaoCustomAuth, recursiveDeleteUser 두 개다. 사진을 나중에 캡쳐해서 추가된 함수가 하나 더 들어있다. 9번에서 그대로 복붙한 사람은 kakaoCustomAuth만 나오면 된다.




    배포된 함수는 Firebase console의 Functions에서 아래처럼 확인 가능하다. 사진은 글을 포스팅할 때 새로 한거라 함수 이름은 다르다.
    












서비스 계정에 권한주기



1. firebase console > 톱니바퀴 > 서비스 계정 > 서비스 계정 권한 관리 클릭





2. 새로 뜬 화면에서 1번에서 보았던 서비스 계정에 해당하는 작업을 누르고 권한 관리 클릭




3. 액세스 권한 부여 > 새 주 구성원에 소유자 계정 입력 후 엔터 > 역할 드롭다운하여 서비스 계정 > 서비스 계정 토큰 생성자 클릭 > 저장 클릭









Android Application에서 로그인하기


이제 Application에서 호출해보자. Android Studio에서 로그인 화면단이 있을것이다. 그 화면단에서 카카오톡으로 로그인하기같은 버튼을 누를 시 클라이언트에서 카카오톡 로그인 서버로 요청하고 반환되어온 Access Token으로 Firebase Functions로 배포해놓은 카카오 로그인 함수를 호출할 것이다.

아래는 과정이고 Firebase Functions로 배포한 함수를 호출하는 부분만 필요하면 getCutomToken()을 보면 된다.


class LoginActivity : AppCompatActivity() {
    private lateinit var auth: FirebaseAuth
    private var _binding: ActivityLoginBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
        auth = Firebase.auth

        binding.kakaoLoginButton.setOnClickListener {
            kakaoLogin()
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        _binding = null
    }

    private fun kakaoLogin() {

            // 카카오톡 어플이 있으면 카톡으로 로그인, 없으면 카카오 계정으로 로그인
        if (UserApiClient.instance.isKakaoTalkLoginAvailable(this)) {
            UserApiClient.instance.loginWithKakaoTalk(this) { token, error ->
                if (error != null) {
                    if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
                        return@loginWithKakaoTalk
                    }
                    loginWithKaKaoAccount(this)
                } else if (token != null) {
                    getCustomToken(token.accessToken)
                }
            }
        } else {
            loginWithKaKaoAccount(this)
        }
    }

    // 카카오 계정으로 로그인
    private fun loginWithKaKaoAccount(context: Context) {
        UserApiClient.instance.loginWithKakaoAccount(context) { token: OAuthToken?, error: Throwable? ->
            if (token != null) {
                getCustomToken(token.accessToken)
            }
        }
    }

    // firebase functions에 배포한 kakaoCustomAuth 호출
    private fun getCustomToken(accessToken: String) {

        val functions: FirebaseFunctions = Firebase.functions("asia-northeast3")

        val data = hashMapOf(
            "token" to accessToken
        )

        functions
            .getHttpsCallable("kakaoCustomAuth")
            .call(data)
            .addOnCompleteListener { task ->
                try {
                    // 호출 성공
                    val result = task.result?.data as HashMap<*, *>
                    var mKey: String? = null
                    for (key in result.keys) {
                        mKey = key.toString()
                    }
                    val customToken = result[mKey!!].toString()

                    // 호출 성공해서 반환받은 커스텀 토큰으로 Firebase Authentication 인증받기
                    firebaseAuthWithKakao(customToken)
                } catch (e: RuntimeExecutionException) {
                    // 호출 실패
                }
            }
    }

    private fun firebaseAuthWithKakao(customToken: String) {
        auth.signInWithCustomToken(customToken).addOnCompleteListener { result ->
            if (result.isSuccessful) {
                // Firebase Authentication 인증 성공 후 로직
            } else {
                // 실패 후 로직
            }
        }
    }

}  










배포가 안될때 확인사항



혹시 배포가 안되는 사람은 로그를 보는게 가장 좋다. 그 중에 내가 막혔던 게 request module이 설치안되어있어서 배포가 안됐었다.

request라는게 모듈이 아니고 javascript 내장 함수인줄 알았다..이 경우에는 npm으로 설치해주면 된다.

npm install --save request
npm install --save request-promise-native

로 설치해주면 된다.

그리고 배포하고 나서도 처음 몇번은 로그를 보면서 잘 동작하는 지 체크해주자








참고문서



댓글