Skip to content

Commit

Permalink
Merge pull request #98 from auth0/ulp-5290
Browse files Browse the repository at this point in the history
feat: update strategy to new linkedin implementation
  • Loading branch information
ncluer authored Sep 20, 2023
2 parents eb9890c + 1952e37 commit a78e05b
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 469 deletions.
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;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "passport-linkedin-oauth2",
"version": "2.0.0",
"version": "3.0.0",
"description": "Passport for LinkedIn OAuth2 API v2",
"main": "./lib",
"repository": {
Expand Down
Loading

0 comments on commit a78e05b

Please sign in to comment.