Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update strategy to new linkedin implementation #98

Merged
merged 2 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
87 changes: 45 additions & 42 deletions example/server.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
var express = require('express')
, passport = require('passport')
, LinkedinStrategy = require('../lib').Strategy;
var express = require('express'),
passport = require('passport'),
LinkedinStrategy = require('../lib').Strategy;

// API Access link for creating client ID and secret:
// https://www.linkedin.com/secure/developer
var LINKEDIN_CLIENT_ID = process.env.LINKEDIN_CLIENT_ID;
var LINKEDIN_CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET;
var CALLBACK_URL = process.env.CALLBACK_URL || 'http://localhost:3000/auth/linkedin/callback';

var CALLBACK_URL =
process.env.CALLBACK_URL || 'http://localhost:3000/auth/linkedin/callback';

// Passport session setup.
// To support persistent login sessions, Passport needs to be able to
Expand All @@ -16,41 +16,40 @@ var CALLBACK_URL = process.env.CALLBACK_URL || 'http://localhost:3000/auth/linke
// the user by ID when deserializing. However, since this example does not
// have a database of user records, the complete Linkedin profile is
// serialized and deserialized.
passport.serializeUser(function(user, done) {
passport.serializeUser(function (user, done) {
done(null, user);
});

passport.deserializeUser(function(obj, done) {
passport.deserializeUser(function (obj, done) {
done(null, obj);
});


// Use the LinkedinStrategy within Passport.
// Strategies in Passport require a `verify` function, which accept
// credentials (in this case, an accessToken, refreshToken, and Linkedin
// profile), and invoke a callback with a user object.
passport.use(new LinkedinStrategy({
clientID: LINKEDIN_CLIENT_ID,
clientSecret: LINKEDIN_CLIENT_SECRET,
callbackURL: CALLBACK_URL,
scope: ['r_liteprofile', 'r_emailaddress'],
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
req.session.accessToken = accessToken;
process.nextTick(function () {
// To keep the example simple, the user's Linkedin profile is returned to
// represent the logged-in user. In a typical application, you would want
// to associate the Linkedin account with a user record in your database,
// and return that user instead.
return done(null, profile);
});
}
));



passport.use(
new LinkedinStrategy(
{
clientID: LINKEDIN_CLIENT_ID,
clientSecret: LINKEDIN_CLIENT_SECRET,
callbackURL: CALLBACK_URL,
scope: ['profile', 'email', 'openid'],
passReqToCallback: true,
},
function (req, accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
req.session.accessToken = accessToken;
process.nextTick(function () {
// To keep the example simple, the user's Linkedin profile is returned to
// represent the logged-in user. In a typical application, you would want
// to associate the Linkedin account with a user record in your database,
// and return that user instead.
return done(null, profile);
});
}
)
);

var app = express();

Expand All @@ -69,12 +68,11 @@ app.use(passport.session());
app.use(app.router);
app.use(express.static(__dirname + '/public'));


app.get('/', function(req, res){
app.get('/', function (req, res) {
res.render('index', { user: req.user });
});

app.get('/account', ensureAuthenticated, function(req, res){
app.get('/account', ensureAuthenticated, function (req, res) {
res.render('account', { user: req.user });
});

Expand All @@ -83,25 +81,29 @@ app.get('/account', ensureAuthenticated, function(req, res){
// request. The first step in Linkedin authentication will involve
// redirecting the user to linkedin.com. After authorization, Linkedin
// will redirect the user back to this application at /auth/linkedin/callback
app.get('/auth/linkedin',
app.get(
'/auth/linkedin',
passport.authenticate('linkedin', { state: 'SOME STATE' }),
function(req, res){
function (req, res) {
// The request will be redirected to Linkedin for authentication, so this
// function will not be called.
});
}
);

// GET /auth/linkedin/callback
// Use passport.authenticate() as route middleware to authenticate the
// request. If authentication fails, the user will be redirected back to the
// login page. Otherwise, the primary route function function will be called,
// which, in this example, will redirect the user to the home page.
app.get('/auth/linkedin/callback',
app.get(
'/auth/linkedin/callback',
passport.authenticate('linkedin', { failureRedirect: '/login' }),
function(req, res) {
function (req, res) {
res.redirect('/');
});
}
);

app.get('/logout', function(req, res){
app.get('/logout', function (req, res) {
req.logout();
res.redirect('/');
});
Expand All @@ -110,13 +112,14 @@ var http = require('http');

http.createServer(app).listen(3000);


// Simple route middleware to ensure user is authenticated.
// Use this route middleware on any resource that needs to be protected. If
// the request is authenticated (typically via a persistent login session),
// the request will proceed. Otherwise, the user will be redirected to the
// login page.
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) { return next(); }
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
180 changes: 43 additions & 137 deletions lib/oauth2.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,60 @@
var util = require('util')
var OAuth2Strategy = require('passport-oauth2')
var util = require('util');
var OAuth2Strategy = require('passport-oauth2');
var InternalOAuthError = require('passport-oauth2').InternalOAuthError;

var liteProfileUrl = 'https://api.linkedin.com/v2/me?projection=(' +
'id,' +
'firstName,' +
'lastName,' +
'maidenName,' +
'profilePicture(displayImage~:playableStreams)' +
')';

// Most of these fields are only available for members of partner programs.
var basicProfileUrl = 'https://api.linkedin.com/v2/me?projection=(' +
'id,' +
'firstName,' +
'lastName,' +
'maidenName,' +
'profilePicture(displayImage~:playableStreams),' +
'phoneticFirstName,' +
'phoneticLastName,' +
'headline,' +
'location,' +
'industryId,' +
'summary,' +
'positions,' +
'vanityName,' +
'lastModified' +
')';
var profileUrl = 'https://api.linkedin.com/v2/userinfo';

function Strategy(options, verify) {
options = options || {};
options.authorizationURL = options.authorizationURL || 'https://www.linkedin.com/oauth/v2/authorization';
options.tokenURL = options.tokenURL || 'https://www.linkedin.com/oauth/v2/accessToken';
options.scope = options.scope || ['r_liteprofile'];
options.authorizationURL =
options.authorizationURL ||
'https://www.linkedin.com/oauth/v2/authorization';
options.tokenURL =
options.tokenURL || 'https://www.linkedin.com/oauth/v2/accessToken';
options.scope = options.scope || ['profile', 'email', 'openid'];

//By default we want data in JSON
options.customHeaders = options.customHeaders || {"x-li-format":"json"};
options.customHeaders = options.customHeaders || { 'x-li-format': 'json' };

OAuth2Strategy.call(this, options, verify);

this.options = options;
this.name = 'linkedin';
this.profileUrl = options.scope.indexOf('r_basicprofile') !== -1 ?
basicProfileUrl : liteProfileUrl;
this.emailUrl = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))';
this.profileUrl = profileUrl;
}

util.inherits(Strategy, OAuth2Strategy);

Strategy.prototype.userProfile = function(accessToken, done) {
Strategy.prototype.userProfile = function (accessToken, done) {
//LinkedIn uses a custom name for the access_token parameter
this._oauth2.setAccessTokenName("oauth2_access_token");
this._oauth2.setAccessTokenName('oauth2_access_token');

this._oauth2.get(this.profileUrl, accessToken, function (err, body, res) {
if (err) {
return done(new InternalOAuthError('failed to fetch user profile', err));
}

var profile;

try {
profile = parseProfile(body);
} catch(e) {
return done(new InternalOAuthError('failed to parse profile response', e));
}

if (!this.options.scope.includes('r_emailaddress')) {
return done(null, profile);
}

this._oauth2.get(this.emailUrl, accessToken, function (err, body, res) {
this._oauth2.get(
this.profileUrl,
accessToken,
function (err, body, _res) {
if (err) {
return done(new InternalOAuthError('failed to fetch user email', err));
return done(
new InternalOAuthError('failed to fetch user profile', err)
);
}

var profile;

try {
addEmails(profile, body);
} catch(e) {
return done(new InternalOAuthError('failed to parse email response', e));
profile = parseProfile(body);
} catch (e) {
return done(
new InternalOAuthError('failed to parse profile response', e)
);
}

return done(null, profile);
}.bind(this));
done(null, profile);
}.bind(this)
);
};

}.bind(this));
}

Strategy.prototype.authorizationParams = function(options) {
Strategy.prototype.authorizationParams = function (options) {
var params = {};

// LinkedIn requires state parameter. It will return an error if not set.
Expand All @@ -95,84 +63,22 @@ Strategy.prototype.authorizationParams = function(options) {
}

return params;
}

function getName(nameObj) {
var locale = nameObj.preferredLocale.language + '_' + nameObj.preferredLocale.country;
return nameObj.localized[locale];
}

function getProfilePictures(profilePictureObj) {
// This is the format we used to return in the past.
var result = [];

if(!profilePictureObj) {
// Picture is optional.
return result;
}

try {
profilePictureObj['displayImage~'].elements.forEach(function(pic) {
// We keep only public profile pictures.
if(pic.authorizationMethod !== 'PUBLIC') {
return;
}

// This should not happen, but...
if(pic.identifiers.length === 0) {
return;
}

var url = pic.identifiers[0].identifier;

result.push({ value: url });
});
} catch(e) {
// Profile picture object changed format?
return result;
}

return result;
}
};

function parseProfile(body) {
var json = JSON.parse(body);

var profile = { provider: 'linkedin' };

profile.id = json.id;

profile.name = {
givenName: getName(json.firstName),
familyName: getName(json.lastName)
return {
provider: 'linkedin',
id: json.sub,
email: json.email,
givenName: json.given_name,
familyName: json.family_name,
displayName: `${json.given_name} ${json.family_name}`,
picture: json.picture,
_raw: body,
_json: json,
};

profile.displayName = profile.name.givenName + ' ' + profile.name.familyName;

profile.photos = getProfilePictures(json.profilePicture);

profile._raw = body;
profile._json = json;

return profile;
}

function addEmails(profile, body) {
var json = JSON.parse(body);

if(json.elements && json.elements.length > 0) {
profile.emails = json.elements.reduce(function (acc, el) {
if (el['handle~'] && el['handle~'].emailAddress) {
acc.push({
value: el['handle~'].emailAddress
});
}
return acc;
}, []);
}

profile._emailRaw = body;
profile._emailJson = json;
}

module.exports = Strategy;
Loading
Loading