본문으로 바로가기

728x90

파이어 베이스는 각종 앱을 쉽게 만들기 위해 제공되는 프로젝트 플랫폼입니다. OAuth2 로그인, 데이터베이스, 기계학습과 호스팅 등을 제공합니다. 파이어 베이스를 잘 활용하면 비용 없이 앱 서비스를 제공이 가능합니다. 또한, 구글에서 제공되는 다양한 확장 기능을 통합할 수 있습니다.

 

필자는 AWS 기반으로 개인적인 프로젝트를 진행한 적이 있는데 이때 서버의 운영비용이 개인적으로 부담하기에는 지불해야 할 비용이 높았습니다. 물론 제공되는 서버의 사양도 낮아서 간혹 메모리 부족으로 죽는 현상도 발생하였습니다.

 

이번 연재에서는 파이어 베이스 기반의 OAuth2 로그인과 데이터베이스를 사용하는 예제를 위주로 설명합니다. 그리고 추가적으로 카카오와 네이버의 OAuth2를 파이어 베이스에 확장 적용하는 방법에 대해서도 구현할 예정입니다.

 

연재는 아래와 같은 순서로 작업할 연재할 예정이며 순서는 달라질 수 있습니다.

 

프로젝트 생성

1. Vue + Firebase + Vuetify

 

인증

2. 전자우편/비밀번호

3. 전자우편 링크

4. 구글 로그인

5. 페이스북 로그인

6. 카카오톡 로그인

7. 네이버 로그인

 


이 번 글에서는 카카오톡 로그인을 사용하는 방법에 대해서 설명합니다.

 

 

카카오 로그인 처리 개요

카카오 로그인 처리 순서

  1. 카카오 로그인 요청
  2. 카카오 로그인 동의 및 로그인
  3. 콜백 페이지로 카카오 인증 코드 반환
  4. 파이어 베이스에 카카오 인증 코드 전달
    1. 카카오 REST API를 통해 카카오 인증코드를 전달하여 카카오 Access Token 요청 및 응답 처리
    2. 카카오 Access Token을 통해 사용자 정보 요청 및 응답 처리
    3. 카카오 사용자 정보가 있을 경우 파이어 베이스에 회원을 등록 또는 정보 갱신
    4. 등록 및 갱신 후 파이어베이스 계정 정보 취득
      1. 이미 존재하는 전자우편의 경우에는 전자우편을 통해 사용자 정보 조회
    5. 계정 정보 반환 시 파이어베이스 Access Token 생성
    6. 파이어 베이스 Access Token 및 카카오 Access Token을 웹 브라우저에 전달
  5. 파이어 베이스로 전달받은 Access Token을 이용해 카카오 및 파이어 베이스 로그인 처리

 

카카오 프로젝트 생성하기

카카오 프로젝트 생성하기

카카오 프로젝트 시작

카카오 개발자 사이트에 접속하여 [시작하기]를 클릭합니다. 또는 내 애플리케이션을 눌러 신규 프로젝트를 생성합니다.

카카오 개발자 사이트

카카오 애플리케이션 목록

[내 애플리케이션] 메뉴를 선택하여 애플리케이션 목록을 봅니다.

카카오 애플리케이션 추가하기

앱 이름과 회사이름을 입력하고 저장을 클릭합니다.

애플리케이션 추가

생성된 프로젝트 확인

애플리케이션이 추가되었는지 확인합니다.

애플리케이션 생성 확인

생성된 애플리케이션을 클릭하여 생성된 앱 키들을 확인합니다.

애플리케이션 앱 키 확인

[플랫폼 설정하기]를 클릭하여 웹 항목을 선택합니다.

웹 플랫폼 등록

사이트 도메인을 입력합니다. [파이어 베이스 프로젝트 설정 > Authentication > Sign-method]의 승인된 도메인 항목 값을 복사하여 사용합니다.

사이트 도메인 설정

정상적으로 플랫폼이 등록되었는지 확인합니다.

플랫폼 등록 확인

왼쪽 메뉴에서 [제품 설정 > 카카오 로그인]을 선택하고 활성화 설정을 ON으로 변경합니다.

카카오 로그인 활성화 설정

카카오 로그인 Redirect URI를 설정합니다. 이 값은 사이트 도메인 주소에 내부적으로 콜백을 처리할 도메인 주소를 붙여서 사용합니다.

Redirect URI 경로 입력

왼쪽 메뉴에서 [카카오 로그인 > 동의 항목]을 선택하고 필요한 권한을 설정합니다.

카카오 로그인 동의항목

이 프로젝트에서는 사용자의 아바타와 별명을 읽기 위해서 선택 동의를 추가하였습니다.

카카오 로그인 동의 항목

동의 항목에 설정된 내용을 확인합니다.

동의항목 확인

 

프로젝트 수정하기

Firebase Admin Key 다운로드

[Firebase 프로젝트 설정 > 서비스 계정]에서 새 비공개 키 생성을 눌러 관리자 접근을 위한 키를 발급받습니다.

Firebase Admin SDK

경고 문구를 확인하고 키 생성 버튼을 클릭합니다. 파이어 베이스 관리자 키 파일이 자동으로 다운로드됩니다.

Firebase Admin 새 비공개 키 생성

 

Firebase Admin SDK 설치

패키지 설치

카카오 로그인에서 Access Token을 정상적으로 받았을 경우 파이어 베이스에 사용자를 추가하기 위하여 firebase-functions와 firebase-admin을 프로젝트에 추가합니다.

npm install firebase-functions@latest firebase-admin@latest --save
npm install -g firebase-tools

파이어 베이스 함수 초기화

파이어 베이스에 로그인하고 프로젝트에 firebase-functions를 초기화합니다. 여기에서는 javascript 구문으로 초기화하였습니다.

firebase login
firebase init functions

파이어 베이스 함수 프로젝트 수정

functions 경로로 이동한 다음 dotenv 패키지를 추가합니다.

functions> npm install --save dotenv
functions> npm install --save request-promise

다운로드한 키 파일을 편집 프로그램으로 열어 내용을 환경설정 변수로 설정합니다. 다운로드한 파일을 직접 import 하여 사용 가능합니다. 이 프로젝트에서는 외부에 공개하지 않기 위하여. env,. env.local과 같은 파일을 생성하여 환경설정 변수로 설정하였습니다.

FIREBASE_ADMIN_TYPE = ......
FIREBASE_ADMIN_PROJECT_ID = ......
FIREBASE_ADMIN_PRIVATE_KEY_ID = ......
FIREBASE_ADMIN_PRIVATE_KEY = -----BEGIN PRIVATE KEY-----\n......\n-----END PRIVATE KEY-----\n
FIREBASE_ADMIN_CLIENT_EMAIL = ......
FIREBASE_ADMIN_CLIENT_ID = ......
FIREBASE_ADMIN_AUTH_URI = ......
FIREBASE_ADMIN_TOKEN_URI = ......
FIREBASE_ADMIN_AUTH_PROVIDER_CERT_URL = ......
FIREBASE_ADMIN_CLIENT_CERT_URL = ......

파이어 베이스 카카오 로그인 함수 구현

functions/index.js 에 파이어 베이스 함수를 구현할 수 있습니다.

파이어 베이스 확장에서 정의된 코드는 다음 경로에서 확인 가능합니다.

https://github.com/FirebaseExtended/custom-auth-samples/tree/master/kakao

 

FirebaseExtended/custom-auth-samples

Samples showcasing how to sign in Firebase using additional Identity Providers - FirebaseExtended/custom-auth-samples

github.com

아래의 코드는 파이어 베이스 함수를 사용하여 처리가 가능하도록 한 코드입니다. 아래 코드는 다른 분이 이미 구현된 코드를 참고하였습니다.

출처 : 파이어베이스(Firebase)의 모든 것!
https://github.com/everything-of-firebase
https://github.com/everything-of-firebase/custom-auth-functions-samples
 

everything-of-firebase/custom-auth-functions-samples

Contribute to everything-of-firebase/custom-auth-functions-samples development by creating an account on GitHub.

github.com

const functions = require('firebase-functions')
const admin = require('firebase-admin')

// CORS Express middleware to enable CORS Requests.
const cors = require('cors')({
    origin: true,
});

require('dotenv').config();

const privateKey = (process.env.FIREBASE_ADMIN_PRIVATE_KEY || "").split("\\n").join("\n");
const serviceAccount = {
    "type": process.env.FIREBASE_ADMIN_TYPE,
    "project_id": process.env.FIREBASE_ADMIN_PROJECT_ID,
    "private_key_id": process.env.FIREBASE_ADMIN_PRIVATE_KEY_ID,
    "private_key": privateKey,
    "client_email": process.env.FIREBASE_ADMIN_CLIENT_EMAIL,
    "client_id": process.env.FIREBASE_ADMIN_CLIENT_ID,
    "auth_uri": process.env.FIREBASE_ADMIN_AUTH_URI,
    "token_uri": process.env.FIREBASE_ADMIN_TOKEN_URI,
    "auth_provider_x509_cert_url": process.env.FIREBASE_ADMIN_AUTH_PROVIDER_CERT_URL,
    "client_x509_cert_url": process.env.FIREBASE_ADMIN_CLIENT_CERT_URL
}


admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: process.env.FIREBASE_DATABASE_URL
})

const request = require('request-promise')


/**
 * requestMe - Returns user profile from Kakao API
 *
 * @param  {String} kakaoAccessToken Access token retrieved by Kakao Login API
 * @return {Promiise<Response>}      User profile response in a promise
 */
const kakaoRequestMeUrl = 'https://kapi.kakao.com/v2/user/me?secure_resource=true'
function requestMe(kakaoAccessToken) {
    console.log('Requesting user profile from Kakao API server.')
    return request({
        method: 'GET',
        headers: {'Authorization': 'Bearer ' + kakaoAccessToken},
        url: kakaoRequestMeUrl,
    })
}

const kakaoRequestTokenUrl = 'https://kauth.kakao.com/oauth/token'
function requestAccessToken(kakaoAuthCode) {
    console.log('Requesting user access token from Kakao API server.')
    // console.log('Kakao Client id : ', process.env.KAKAO_APP_KEY_REST)
    
    return request({
        method: 'POST',
        headers: {'Content-type': 'application/x-www-form-urlencoded;charset=utf-8'},
        url: kakaoRequestTokenUrl,
        form: {
            grant_type: 'authorization_code',
            client_id: process.env.KAKAO_APP_KEY_REST,
            redirect_uri: process.env.KAKAO_APP_REDIRECT_URI,
            code: kakaoAuthCode,
            // client_secret: ''
        }
    })

}


/**
 * updateOrCreateUser - Update Firebase user with the give email, create if
 * none exists.
 *
 * @param  {String} userId        user id per app
 * @param  {String} email         user's email address
 * @param  {String} displayName   user
 * @param  {String} photoURL      profile photo url
 * @return {Prommise<UserRecord>} Firebase user record in a promise
 */
function updateOrCreateUser(userId, email, displayName, photoURL) {
    console.log('updating or creating a firebase user');
    const updateParams = {
        provider: 'KAKAO',
        displayName: displayName,
    };
    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)
            .catch((err) => {
                // 동일한 메일주소로 이미 가입되어 있는 경우에 사용자 검색하여 반환
                if(err.code === 'auth/email-already-exists') {
                    console.log(err);
                    // console.log('auth/email-already-exists --------------- ', email);
                    return admin.auth().getUserByEmail(email);
                } else {
                    throw err;
                }
            });
        }
        throw error;
    });
}

/**
 * createFirebaseToken - returns Firebase token using Firebase Admin SDK
 *
 * @param  {String} kakaoAccessToken access token from Kakao Login API
 * @return {Promise<String>}                  Firebase token in a promise
 */
function createFirebaseToken(kakaoAccessToken) {
    return requestMe(kakaoAccessToken).then((response) => {
        console.log("RequestMe : ", response);
        
        const body = JSON.parse(response)
        console.log(body)

        const userId = `kakao:${body.id}`
        if (!userId) {
            return res.status(404)
            .send({message: 'There was no user with the given access token.'})
        }
        let nickname = null
        let profileImage = null
        if (body.properties) {
            nickname = body.properties.nickname
            profileImage = body.properties.profile_image
        }
        let accountEmail = null;
        if(body.kakao_account) {
            accountEmail = body.kakao_account.email;
            console.log("Email", accountEmail);
        }
        return updateOrCreateUser(userId, accountEmail, 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'})
    }).catch((error) => {
        console.log('Error createFirebaseToken', error);
        throw error;
    });
}


// exports.kakaoCustomAuth = functions.region('asia-northeast1').https
exports.KakaoAuth = functions.https.onRequest((req, res) => {
    // console.log("Kakao Request:", req);
    try {
        if(req.method === 'POST') {
            let kakaoToken = null;
            let firebaseToken = null;

            const authCode = req.body.data.code;
            console.log("Kakao Auth Code:", authCode);
            if (!authCode) {
                return cors(req, res, () => {
                    res
                    .status(400)
                    .json({error: 'There is no token.', message: 'Access token is a required parameter.'});
                });
            }

            
            console.log(`Verifying Kakao Auth Code: ${authCode}`);
            requestAccessToken(authCode).then((response) => {
                console.log(response);

                const body = JSON.parse(response)
                console.log(body);

                kakaoToken = body.access_token;
                console.log("Kakao Access Token:", kakaoToken);
                // console.log(`Verifying Kakao token: ${kakaoToken}`);
                return createFirebaseToken(kakaoToken);
            }).then((fireToken) => {
                firebaseToken = fireToken;
                console.log(`Returning firebase token to user: ${fireToken}`);
                
                return cors(req, res, () => {
                    return res.status(200).json({
                        data : {
                            kakao_token: kakaoToken,
                            firebase_token: fireToken
                        }
                    });
                });
            }).catch((error) => {
                console.log(error);
                // console.log(error.body);
                return cors(req, res, () => {
                    if(error.error) {
                        const body = JSON.parse(error.error);
                        res.status(error.statusCode).json({
                            error : {
                                status: error.statusCode,
                                message: body.error,
                                details: body.error_description
                            }
                        });
                    } else {
                        res.status(500).json({error: 'Error'});
                    }
                });
            });

        } else {
            return cors(req, res, () => {
                res.json({});
            });
        }
    } catch(error) {
        console.log(error);
    }
});

파이어 베이스 에뮬레이터 실행

functions 경로에서 npm run serve를 실행하여 정상적으로 동작하는지 확인합니다.

functions> npm run serve

파이어베이스 함수 에뮬레이터

파이어 베이스 함수 배포

functions 경로에서 npm run deploy를 실행하여 파일을 배포합니다. Node 10 버전에서는 파이어 베이스 유료계정이 필요하여 배포가 안됩니다. 아래의 방법을 참고하여 에뮬레이터로 실행될 수 있도록 수정합니다.

functions> npm run deploy

파이어 베이스 에뮬레이터와 프로젝트 수정

에뮬레이터에서는 실행이 되어 배포를 진행하면 유료계정이 필요하다고 배포가 되지 않습니다. Node 10 버전부터 파이어 베이스 유료계정이 필요하다고 합니다. Node 8 버전을 사용하시거나(추후 서비스가 중지될 수 있다고 합니다.) 에뮬레이터를 사용해야 합니다.

/src/main.js 에 다음 구문을 추가합니다. 이때 경로는 파이어 베이스 함수 에뮬레이터 실행 후 표시되는 정보를 참고하십시오. 아래 값은 기본값으로 되어 있습니다.

// Initialize Firebase
firebase.initializeApp(firebaseConfig);

if(window.location.hostname === 'localhost') {
  firebase.firestore().settings({ host: 'localhost:8080', ssl: false });
  firebase.functions().useFunctionsEmulator('http://localhost:5001');
}

 

카카오 로그인 추가

Kakao Javascript SDK 설치

Kakao Javascript SDK 페이지에 접속하여 SDK파일을 다운로드합니다. 다운로드한 파일을 /public/js/kakao.js에 복사합니다.

https://developers.kakao.com/sdk/js/kakao.js

/public/index.html 파일을 열어 자바스크립트를 추가합니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    ......중간 생략......
    <script type="text/javascript" src="<%= BASE_URL %>js/kakao.js"></script>
  </head>
  <body>
    ......중간 생략......
  </body>
</html>

로그인 화면 수정

/src/views/SignIn.vue 파일을 열어 카카오 SDK를 초기화하고 카카오 로그인 버튼을 추가합니다. 카카오 로그인 버튼의 클릭이벤트를 kakao() 함수에 바인딩시킵니다.

<script>
import firebase from 'firebase'
// import KakaoAuth from '@/shared/KakaoAuth'

if(!window.Kakao.isInitialized()) {
  window.Kakao.init(process.env.VUE_APP_KAKAO_APP_KEY_WEB);
  window.Kakao.isInitialized();
}

......중간 생략......
export default {
  methods: {
  ......중간 생략......
    kakao() {
      const protocol = location.protocol;
      const hostName = location.hostname;
      const port = location.port;

      let url = protocol + '//' + hostName + (port ? ':' + port : '');
      url += '/callback/kakaotalk';

      window.Kakao.Auth.authorize({
        redirectUri: url
//        , state:""
//        , scope:""
        , throughTalk: true
      });
    },
  ......중간 생략......
</script>

콜백 페이지 추가

/src/router/index.js 파일에 /callback/kakaotalk 를 추가합니다.

......상단 생략......
  {
    path: '/callback',
    name: 'callback',
    redirect: '/signin',
    component: BlankContainer,
    children: [
      ......중간 생략......
      {
        path: 'kakaotalk',
        name: 'callback-kakaotalk',
        component: () => import('@/views/callback/KakaoTalk'),
      },
    ]
  },
......이하 생략......  

콜백 페이지를 다음과 같이 구성합니다. 카카오 로그인이 성공적으로 이루어질 경우 콜백 페이지는 다음과 같은 형식으로 페이지가 호출됩니다.

http://localhost:8080/callback/kakaotalk? code=2 sbl05 sa84 zV2 gd...... fgopb1 UAAAF0 JOxQHg

반환되는 주소에서 code의 값은 Authentication Code로 Access Token이 아닙니다. 따라서 바로 사용이 불가능하며 본 코드를 사용하여 REST API를 사용하여 사용자 Access Token을 받도록 구현해야 합니다.

 

다음과 같이 KakaoTalk.vue를 추가합니다. 아래 코드에서는 <template> 부분이 의미가 없어 <script> 블록만 표시하였습니다.

......상단 생략......
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/functions'

if(!window.Kakao.isInitialized()) {
  window.Kakao.init(process.env.VUE_APP_KAKAO_APP_KEY_WEB);
  window.Kakao.isInitialized();
}

export default {
  data() {
    return {
      token: '',
    }
  },
  mounted() {
    const _this = this;

    const kakaoAuthCode = this.$route.query.code;
    if(kakaoAuthCode) {
      console.log(kakaoAuthCode);
      // this.token = kakaoAuthCode;

      // 카카오 로그인 토큰을 파이어베이스 함수에 전달합니다.
      var kakaoAuth = firebase.functions().httpsCallable('KakaoAuth');
      kakaoAuth({code: kakaoAuthCode}).then(function(result) {
        console.log(result);

        // Read result of the Cloud Function.
        var kakaoToken = result.data.kakao_token;
        var fireToken = result.data.firebase_token;

        // 토근이 정상적으로 처리될 경우 로그인 처리합니다.
        firebase.auth().signInWithCustomToken(fireToken)
        .then(function(result) {

          _this.token = kakaoToken;
          window.Kakao.Auth.setAccessToken(kakaoToken);

          const user = result.user;
          console.log("User : ", user);
          if(result.additionalUserInfo.isNewUser) {
            console.log("신규 사용자...");
            _this.$router.push("/welcome");   // welcome
          } else {
            _this.$router.push("/profile");
          }
        })
        .catch(function(error) {
          // Handle Errors here.
          var errorCode = error.code;
          var errorMessage = error.message;
          console.log(errorCode, errorMessage);

          // console.log(error);
          alert("토큰이 정상적이지 않습니다. 만료된 토큰이거나 이미 사용된 토큰입니다.");
          _this.$router.push("/signin");
          return;
        });
      }).catch(function(error) {
        // Getting the Error details.
        var code = error.code;
        var message = error.message;
        var details = error.details;

        // console.log(error);
        // console.log(code, message, details);
        // console.log(error.body);
        
        alert("정상적이지 않은 접근입니다. 만료된 데이터이거나 이미 사용된 데이터입니다.");
        // alert(message + ' ' + details);
        _this.$router.push("/signin");
        return;
      });
    } else {
      alert("잘못된 방법으로 접근하셨습니다. 로그인 페이지로 이동합니다.");
      _this.$router.push("/signin");
      return;
    }

  },
  methods: {
    signin() {
      // this.$router.push("/signin");
      location.href='/signin'
    }
  }
}
......하단 생략......

로그인 테스트

카카오 로그인 버튼을 눌러 카카오 로그인 페이지로 이동합니다.

카카오톡 로그인 버튼

카카오 로그인 페이지의 동의하기를 선택하고 계속하기를 클릭합니다.

카카오 로그인

아래와 같이 firebase functions의 로그가 출력됩니다. 사용자의 정보가 출력되면 정상 동작합니다.

Firebase 함수 로그

 

글을 마치며

카카오 로그인의 최소의 기능만 구현하였습니다. 오류 처리에 대한 부분은 조금 더 다듬을 필요가 있습니다. 참조한 여러 사이트에서는 모바일앱과 연동하는 방법으로만 설명되어 있어 웹 앱에서 구현하는 방법을 강구하느라 본 포스트를 작성하는데 시간이 좀 걸렸습니다. CORS와 같은 보안 오류를 회피하기 위한 방법, 이미 동일한 전자우편 주소가 firebase에 등록된 경우의 처리방법 구현이 더 필요하였습니다.

본 글을 보시는 방문자께서 구현 시 추가되는 오류에 대한 방안에 대한 피드백을 주시면 본 글에 반영하도록 하겠습니다.

 

참고자료

Kakao developers

https://developers.kakao.com/

Develop with Documents

https://developers.kakao.com/docs

Firebase Auth에 카카오 로그인 연동하기

https://nextus.tistory.com/10

Use Kakao Login with Firebase Authentication

https://github.com/FirebaseExtended/custom-auth-samples/tree/master/kakao

시작하기: 첫 번째 함수 작성, 테스트, 배포

firebase.google.com/docs/functions/get-started?authuser=0

FirebaseAuthSocialLogin

https://github.com/everything-of-firebase/custom-auth-functions-samples

Going Serverless with Vue.js and Firebase Cloud Functions

https://labs.thisdot.co/blog/going-serverless-with-vue-js-and-firebase-cloud-functions

 

소스코드

본 포스트 관련 소스코드는 여기에서 다운로드 가능합니다.

 

728x90