Skip to content
This repository was archived by the owner on Mar 17, 2025. It is now read-only.

Improved ways to link accounts #17

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions kakao/KakaoLoginAndroid/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileSdkVersion 27
buildToolsVersion "27.0.3"

defaultConfig {
applicationId "com.google.firebase.auth.kakao"
minSdkVersion 14
targetSdkVersion 25
targetSdkVersion 27
versionCode 1
versionName "1.0"

Expand All @@ -26,15 +26,25 @@ android {
}

dependencies {
compile 'com.android.support:appcompat-v7:25.3.0'
implementation 'com.android.support:appcompat-v7:27.1.0'

compile 'com.android.volley:volley:1.0.0'
implementation 'com.android.volley:volley:1.0.0'

compile 'com.github.bumptech.glide:glide:3.7.0'
implementation 'com.github.bumptech.glide:glide:3.7.0'

compile "com.kakao.sdk:usermgmt:${project.KAKAO_SDK_VERSION}"
compile 'com.google.firebase:firebase-core:10.2.0'
compile 'com.google.firebase:firebase-auth:10.2.0'
implementation "com.kakao.sdk:usermgmt:${project.KAKAO_SDK_VERSION}"
implementation 'com.google.firebase:firebase-core:12.0.1'
implementation 'com.google.firebase:firebase-auth:12.0.1'
}

configurations.all {
resolutionStrategy {
eachDependency { details ->
// Force all of the primary support libraries to use the same version.
if (details.requested.group == 'com.android.support') {
details.useVersion '27.1.0'
}
}
}
}
apply plugin: 'com.google.gms.google-services'
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,19 @@ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
Toolbar toolBar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolBar = findViewById(R.id.toolbar);
setSupportActionBar(toolBar);

binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
loggedInView = (LinearLayout) findViewById(R.id.logged_in_view);
loginButton = (LoginButton) findViewById(R.id.login_button);
logoutButton = (Button) findViewById(R.id.logout_button);
imageView = (ImageView) findViewById(R.id.profile_image_view);
loggedInView = findViewById(R.id.logged_in_view);
loginButton = findViewById(R.id.login_button);
logoutButton = findViewById(R.id.logout_button);
imageView = findViewById(R.id.profile_image_view);

logoutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
UserManagement.requestLogout(new LogoutResponseCallback() {
UserManagement.getInstance().requestLogout(new LogoutResponseCallback() {
@Override
public void onCompleteLogout() {
FirebaseAuth.getInstance().signOut();
Expand Down Expand Up @@ -104,7 +104,9 @@ private void updateUI() {
FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
if (currentUser != null) {
binding.setCurrentUser(currentUser);
if (currentUser.getPhotoUrl() != null) {
if (currentUser.getPhotoUrl() == null) {
Glide.clear(imageView);
} else {
Glide.with(this)
.load(currentUser.getPhotoUrl())
.into(imageView);
Expand Down Expand Up @@ -178,10 +180,10 @@ private class KakaoSessionCallback implements ISessionCallback {
@Override
public void onSessionOpened() {
Toast.makeText(getApplicationContext(), "Successfully logged in to Kakao. Now creating or updating a Firebase User.", Toast.LENGTH_LONG).show();
String accessToken = Session.getCurrentSession().getAccessToken();
String accessToken = Session.getCurrentSession().getTokenInfo().getAccessToken();
getFirebaseJwt(accessToken).continueWithTask(new Continuation<String, Task<AuthResult>>() {
@Override
public Task<AuthResult> then(@NonNull Task<String> task) throws Exception {
public Task<AuthResult> then(@NonNull Task<String> task) {
String firebaseToken = task.getResult();
FirebaseAuth auth = FirebaseAuth.getInstance();
return auth.signInWithCustomToken(firebaseToken);
Expand Down
12 changes: 10 additions & 2 deletions kakao/KakaoLoginAndroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
buildscript {
repositories {
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.android.tools.build:gradle:3.1.0'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
classpath 'com.google.gms:google-services:3.0.0'
classpath 'com.google.gms:google-services:3.2.1'
// classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
Expand All @@ -18,6 +22,10 @@ allprojects {
repositories {
jcenter()
maven { url 'http://devrepo.kakao.com:8088/nexus/content/groups/public/' }
maven {
url 'https://maven.google.com/'
name 'Google'
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion kakao/KakaoLoginAndroid/gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
KAKAO_SDK_VERSION=1.1.33
KAKAO_SDK_VERSION=1.9.0
160 changes: 117 additions & 43 deletions kakao/KakaoLoginServer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const serviceAccount = require('./service-account.json');

// Kakao API request url to retrieve user profile based on access token
const requestMeUrl = 'https://kapi.kakao.com/v1/user/me?secure_resource=true';
const accessTokenInfoUrl = 'https://kapi.kakao.com/v1/user/access_token_info';

const config = require('./config.json'); // put your kakao app id in config.json

// Initialize FirebaseApp with service-account.json
firebaseAdmin.initializeApp({
Expand All @@ -24,7 +27,7 @@ firebaseAdmin.initializeApp({
* 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
* @return {Promise<Response>} User profile response in a promise
*/
function requestMe(kakaoAccessToken) {
console.log('Requesting user profile from Kakao API server.');
Expand All @@ -33,47 +36,112 @@ function requestMe(kakaoAccessToken) {
headers: {'Authorization': 'Bearer ' + kakaoAccessToken},
url: requestMeUrl,
});
};
}

/**
* validateToken - Returns access token info from Kakao API,
* which checks if this token is issued by this application.
*
* @param {String} kakaoAccessToken Access token retrieved by Kakao Login API
* @return {Promise<Response>} Access token info response
*/
function validateToken(kakaoAccessToken) {
console.log('Validating access token from Kakao API server.');
return request({
method: 'GET',
headers: {'Authorization': 'Bearer ' + kakaoAccessToken},
url: accessTokenInfoUrl,
});
}


/**
* updateOrCreateUser - Update Firebase user with the give email, create if
* none exists.
* createOrLinkUser - Link firebase user with given email,
* or create one if none exists. If email is not given,
* create a new user since there is no other way to map users.
* If email is not verified, make the user re-authenticate with other means.
*
* @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
* @param {String} kakaoUserId user id per app
* @param {String} email user's email address
* @param {Boolean} emailVerified whether this email is verified or not
* @param {String} displayName user
* @param {String} photoURL profile photo url
* @return {Promise<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 firebaseAdmin.auth().updateUser(userId, updateParams)
.catch((error) => {
if (error.code === 'auth/user-not-found') {
updateParams['uid'] = userId;
if (email) {
updateParams['email'] = email;
function createOrLinkUser(kakaoUserId, email, emailVerified, displayName,
photoURL) {
return getUser(kakaoUserId, email, emailVerified)
.catch((error) => {
if (error.code === 'auth/user-not-found') {
const params = {
uid: `kakao:${kakaoUserId}`,
displayName: displayName,
};
if (email) {
params['email'] = email;
}
if (photoURL) {
params['photoURL'] = photoURL;
}
console.log(`creating a firebase user with email ${email}`);
return firebaseAdmin.auth().createUser(params);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are creating a user with Kakao you should mark it as being a Kakao user using setCustomUserClaim({lineUID: lineMid})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right. :) Added the logic.

}
return firebaseAdmin.auth().createUser(updateParams);
}
throw error;
});
};
throw error;
})
.then((userRecord) => linkUserWithKakao(kakaoUserId, userRecord));
}

/**
* getUser - fetch firebase user with kakao UID first, then with email if
* no user found. If email is not verified, throw an error so that
* the user can re-authenticate.
*
* @param {String} kakaoUserId user id per app
* @param {String} email user's email address
* @param {Boolean} emailVerified whether this email is verified or not
* @return {Promise<admin.auth.UserRecord>}
*/
function getUser(kakaoUserId, email, emailVerified) {
console.log(`fetching a firebase user with uid kakao:${kakaoUserId}`);
return firebaseAdmin.auth().getUser(`kakao:${kakaoUserId}`)
.catch((error) => {
if (error.code !== 'auth/user-not-found') {
throw error;
}
if (!email) {
throw error; // cannot find existing accounts since there is no email.
}
console.log(`fetching a firebase user with email ${email}`);
return firebaseAdmin.auth().getUserByEmail(email)
.then((userRecord) => {
if (!emailVerified) {
throw new Error('This user should authenticate first ' +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I would return a special error with a specific code so that we can act a special way when this happens.
Currently a new User gets created. Maybe do:

const error =  new Error('This user should authenticate first ' +
              'with other providers');
error.code = 'auth/existing-account-need-auth';
error.lineAccessToken = kakaoAccessToken; // Pass the access Token up to here, we'll need it for later.
throw error;

'with other providers');
}
return userRecord;
});
});
}

/**
* linkUserWithKakao - Link current user record with kakao UID
* if not linked yet.
*
* @param {String} kakaoUserId
* @param {admin.auth.UserRecord} userRecord
* @return {Promise<UserRecord>}
*/
function linkUserWithKakao(kakaoUserId, userRecord) {
if (userRecord.customClaims &&
userRecord.customClaims['kakaoUID'] === kakaoUserId) {
console.log(`currently linked with kakao UID ${kakaoUserId}...`);
return Promise.resolve(userRecord);
}
console.log(`linking user with kakao UID ${kakaoUserId}...`);
return firebaseAdmin.auth()
.setCustomUserClaims(userRecord.uid,
{kakaoUID: kakaoUserId}).then(() => userRecord);
}

/**
* createFirebaseToken - returns Firebase token using Firebase Admin SDK
Expand All @@ -82,28 +150,34 @@ function updateOrCreateUser(userId, email, displayName, photoURL) {
* @return {Promise<String>} Firebase token in a promise
*/
function createFirebaseToken(kakaoAccessToken) {
return requestMe(kakaoAccessToken).then((response) => {
return validateToken(kakaoAccessToken).then((response) => {
const body = JSON.parse(response);
const appId = body.appId;
if (appId !== config.kakao.appId) {
throw new Error('The given token does not belong to this application.');
}
return requestMe(kakaoAccessToken);
}).then((response) => {
const body = JSON.parse(response);
console.log(body);
const userId = `kakao:${body.id}`;
const userId = body.id;
if (!userId) {
return res.status(404)
.send({message: 'There was no user with the given access token.'});
throw new Error('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;
}
return updateOrCreateUser(userId, body.kaccount_email, nickname,
profileImage);
return createOrLinkUser(userId, body.kaccount_email,
body.kaccount_email_verified, nickname, profileImage);
}).then((userRecord) => {
const userId = userRecord.uid;
console.log(`creating a custom firebase token based on uid ${userId}`);
return firebaseAdmin.auth().createCustomToken(userId, {provider: 'KAKAO'});
});
};
}


// create an express app and use json body parser
Expand All @@ -126,7 +200,7 @@ app.post('/verifyToken', (req, res) => {
createFirebaseToken(token).then((firebaseToken) => {
console.log(`Returning firebase token to user: ${firebaseToken}`);
res.send({firebase_token: firebaseToken});
});
}).catch((error) => res.status(401).send({message: error}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe I would change message to error since it's really an error more than a message :)

Important point: in the Android Client application we need to display a special message when error.code === 'auth/existing-account-need-auth'; and in that case we need to link the account if the user signs-in with the existing account. To do that we need to add a new endpoints to just "link accounts" where you would pass the kakaoAccessToken and the existing firebase user's ID Token.

});

// Start the server
Expand Down
5 changes: 5 additions & 0 deletions kakao/KakaoLoginServer/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"kakao": {
"appId": -1
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing line break at end of file :)

Loading