-
Notifications
You must be signed in to change notification settings - Fork 97
Improved ways to link accounts #17
base: master
Are you sure you want to change the base?
Changes from all commits
fabb737
46470c1
7987dc0
43bee04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
KAKAO_SDK_VERSION=1.1.33 | ||
KAKAO_SDK_VERSION=1.9.0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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({ | ||
|
@@ -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.'); | ||
|
@@ -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); | ||
} | ||
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 ' + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. 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 | ||
|
@@ -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 | ||
|
@@ -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})); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe I would change Important point: in the Android Client application we need to display a special message when |
||
}); | ||
|
||
// Start the server | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"kakao": { | ||
"appId": -1 | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing line break at end of file :) |
There was a problem hiding this comment.
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})
There was a problem hiding this comment.
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.