Skip to content
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
139 changes: 75 additions & 64 deletions Node-1st-gen/linkedin-auth/functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
'use strict';

const functions = require('firebase-functions/v1');
const {onInit} = require('firebase-functions/v1/init');
const {defineSecret} = require('firebase-functions/params');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
Expand All @@ -30,24 +29,21 @@ admin.initializeApp({
databaseURL: `https://${process.env.GCLOUD_PROJECT}.firebaseio.com`,
});

const OAUTH_SCOPES = ['r_basicprofile', 'r_emailaddress'];
// Modern LinkedIn OpenID Connect Scopes
const OAUTH_SCOPES = ['openid', 'profile', 'email'];

const linkedinClientId = defineSecret('LINKEDIN_CLIENT_ID');
const linkedinClientSecret = defineSecret('LINKEDIN_CLIENT_SECRET');

let Linkedin;
onInit(() => {
Linkedin = require('node-linkedin')(
linkedinClientId.value(),
linkedinClientSecret.value(),
`https://${process.env.GCLOUD_PROJECT}.firebaseapp.com/popup.html`);
});
function getRedirectUri() {
return `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com/popup.html`;
}

/**
* Redirects the User to the LinkedIn authentication consent screen. ALso the 'state' cookie is set for later state
* Redirects the User to the LinkedIn authentication consent screen. Also the 'state' cookie is set for later state
* verification.
*/
exports.redirect = functions.runWith({secrets: [linkedinClientId, linkedinClientSecret]}).https.onRequest((req, res) => {
exports.redirect = functions.runWith({secrets: [linkedinClientId]}).https.onRequest((req, res) => {
cookieParser()(req, res, () => {
const state = req.cookies.state || crypto.randomBytes(20).toString('hex');
functions.logger.log('Setting verification state:', state);
Expand All @@ -56,84 +52,106 @@ exports.redirect = functions.runWith({secrets: [linkedinClientId, linkedinClient
secure: true,
httpOnly: true,
});
Linkedin.auth.authorize(res, OAUTH_SCOPES, state.toString());

const params = new URLSearchParams({
response_type: 'code',
client_id: linkedinClientId.value(),
redirect_uri: getRedirectUri(),
state: state.toString(),
scope: OAUTH_SCOPES.join(' ')
});
res.redirect(`https://www.linkedin.com/oauth/v2/authorization?${params.toString()}`);
});
});

async function getLinkedInAccessToken(code, clientId, clientSecret, redirectUri) {
const params = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
client_id: clientId,
client_secret: clientSecret
});
const res = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
if (!res.ok) {
throw new Error(`LinkedIn Token API failed: ${res.status} ${await res.text()}`);
}
const data = await res.json();
return data.access_token;
}

async function getLinkedInUserProfile(accessToken) {
const res = await fetch('https://api.linkedin.com/v2/userinfo', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
if (!res.ok) {
throw new Error(`LinkedIn UserInfo failed: ${res.status} ${await res.text()}`);
}
return await res.json();
}

/**
* Exchanges a given LinkedIn auth code passed in the 'code' URL query parameter for a Firebase auth token.
* The request also needs to specify a 'state' query parameter which will be checked against the 'state' cookie.
* The Firebase custom auth token is sent back in a JSONP callback function with function name defined by the
* 'callback' query parameter.
*/
exports.token = functions.runWith({secrets: [linkedinClientId, linkedinClientSecret]}).https.onRequest((req, res) => {
try {
return cookieParser()(req, res, () => {
exports.token = functions.runWith({secrets: [linkedinClientId, linkedinClientSecret]}).https.onRequest(async (req, res) => {
return cookieParser()(req, res, async () => {
try {
if (!req.cookies.state) {
throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
}
functions.logger.log('Received verification state:', req.cookies.state);
Linkedin.auth.authorize(OAUTH_SCOPES, req.cookies.state); // Makes sure the state parameter is set
if (req.cookies.state !== req.query.state) {
throw new Error('State query parameter does not match the state cookie.');
}

functions.logger.log('Received auth code:', req.query.code);
functions.logger.log('Received state:', req.query.state);
Linkedin.auth.getAccessToken(res, req.query.code, req.query.state, (error, results) => {
if (error) {
throw error;
}
functions.logger.log('Received Access Token:', results.access_token);
const linkedin = Linkedin.init(results.access_token);
linkedin.people.me(async (error, userResults) => {
if (error) {
throw error;
}
functions.logger.log(
'Auth code exchange result received:',
userResults
);
const accessToken = await getLinkedInAccessToken(
req.query.code,
linkedinClientId.value(),
linkedinClientSecret.value(),
getRedirectUri()
);

functions.logger.log('Received Access Token:', accessToken);
const userResults = await getLinkedInUserProfile(accessToken);
functions.logger.log('Auth code exchange result received:', userResults);

// We have a LinkedIn access token and the user identity now.
const accessToken = results.access_token;
const linkedInUserID = userResults.id;
const profilePic = userResults.pictureUrl;
const userName = userResults.formattedName;
const email = userResults.emailAddress;
const linkedInUserID = userResults.sub;
const profilePic = userResults.picture;
const userName = userResults.name;
const email = userResults.email;

// Create a Firebase account and get the Custom Auth Token.
const firebaseToken = await createFirebaseAccount(linkedInUserID, userName, profilePic, email, accessToken);
// Serve an HTML page that signs the user in and updates the user profile.
res.jsonp({
token: firebaseToken,
});
});
});
});
} catch (error) {
return res.jsonp({ error: error.toString });
}
const firebaseToken = await createFirebaseAccount(linkedInUserID, userName, profilePic, email, accessToken);
return res.jsonp({ token: firebaseToken });
} catch (error) {
functions.logger.error('Token exchange failed:', error);
return res.jsonp({ error: error.toString() });
}
});
});

/**
* Creates a Firebase account with the given user profile and returns a custom auth token allowing
* signing-in this account.
* Also saves the accessToken to the datastore at /linkedInAccessToken/$uid
*
* @returns {Promise<string>} The Firebase custom auth token in a promise.
*/
async function createFirebaseAccount(linkedinID, displayName, photoURL, email, accessToken) {
// The UID we'll assign to the user.
const uid = `linkedin:${linkedinID}`;

// Save the access token tot he Firebase Realtime Database.
const databaseTask = admin.database().ref(`/linkedInAccessToken/${uid}`).set(accessToken);

// Create or update the user account.
const userCreationTask = admin.auth().updateUser(uid, {
displayName: displayName,
photoURL: photoURL,
email: email,
emailVerified: true,
}).catch((error) => {
// If user does not exists we create it.
if (error.code === 'auth/user-not-found') {
return admin.auth().createUser({
uid: uid,
Expand All @@ -146,15 +164,8 @@ async function createFirebaseAccount(linkedinID, displayName, photoURL, email, a
throw error;
});

// Wait for all async task to complete then generate and return a custom auth token.
await Promise.all([userCreationTask, databaseTask]);
// Create a Firebase custom auth token.
const token = await admin.auth().createCustomToken(uid);
functions.logger.log(
'Created Custom token for UID "',
uid,
'" Token:',
token
);
functions.logger.log('Created Custom token for UID "', uid, '" Token:', token);
return token;
}
Loading
Loading