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

Update strategy to work with new "Sign in with Linkedin" feature #63

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
@@ -1,21 +1,21 @@
A simple [Passport](http://passportjs.org/) strategy for LinkedIn OAuth2.
A simple [Passport](http://passportjs.org/) strategy for LinkedIn OAuth2 that works with lite profile.

## Install

npm install passport-linkedin-oauth2
npm install @sokratis/passport-linkedin-oauth2

## Usage

Register the strategy

~~~javascript
var LinkedInStrategy = require('passport-linkedin-oauth2').Strategy;
var LinkedInStrategy = require('@sokratis/passport-linkedin-oauth2').Strategy;

passport.use(new LinkedInStrategy({
clientID: LINKEDIN_KEY,
clientSecret: LINKEDIN_SECRET,
callbackURL: "http://127.0.0.1:3000/auth/linkedin/callback",
scope: ['r_emailaddress', 'r_basicprofile'],
scope: ['r_emailaddress', 'r_liteprofile'],
}, function(accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
Expand Down Expand Up @@ -48,7 +48,7 @@ app.get('/auth/linkedin/callback', passport.authenticate('linkedin', {
}));
~~~

See [this](http://developer.linkedin.com/) for details on LinkedIn API.
See [this](https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context?trk=eml_mktg_gco_dev_api_comms) for details on LinkedIn API.

## Auto-handle `state` param

Expand All @@ -61,7 +61,7 @@ passport.use(new LinkedInStrategy({
clientID: LINKEDIN_KEY,
clientSecret: LINKEDIN_SECRET,
callbackURL: "http://127.0.0.1:3000/auth/linkedin/callback",
scope: ['r_emailaddress', 'r_basicprofile'],
scope: ['r_emailaddress', 'r_liteprofile'],
state: true
}, function(accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
Expand Down
11 changes: 6 additions & 5 deletions example/server.js
Expand Up @@ -4,8 +4,9 @@ var express = require('express')

// API Access link for creating client ID and secret:
// https://www.linkedin.com/secure/developer
var LINKEDIN_CLIENT_ID = "50p3e3wo29te";
var LINKEDIN_CLIENT_SECRET = "CUFBLLzfpU24oQ3B";
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';


// Passport session setup.
Expand All @@ -31,8 +32,8 @@ passport.deserializeUser(function(obj, done) {
passport.use(new LinkedinStrategy({
clientID: LINKEDIN_CLIENT_ID,
clientSecret: LINKEDIN_CLIENT_SECRET,
callbackURL: "http://localhost:3000/auth/linkedin/callback",
scope: [ 'r_basicprofile', 'r_emailaddress'],
callbackURL: CALLBACK_URL,
scope: ['r_liteprofile', 'r_emailaddress'],
passReqToCallback: true
},
function(req, accessToken, refreshToken, profile, done) {
Expand Down Expand Up @@ -120,4 +121,4 @@ http.createServer(app).listen(3000);
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) { return next(); }
res.redirect('/login');
}
}
172 changes: 70 additions & 102 deletions lib/oauth2.js
Expand Up @@ -7,141 +7,109 @@ function Strategy(options, verify) {
options = options || {};
Copy link

Choose a reason for hiding this comment

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

should we move it in arguments as options={}, verify=false?

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_basicprofile'];
options.scope = options.scope || ['r_liteprofile'];
options.profileFields = options.profileFields || null;

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

OAuth2Strategy.call(this, options, verify);

this.options = options;
this.name = 'linkedin';
this.profileUrl = 'https://api.linkedin.com/v1/people/~:(' + this._convertScopeToUserProfileFields(options.scope, options.profileFields) + ')';
this.profileUrl = 'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))';
this.emailUrl = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))';
}

util.inherits(Strategy, OAuth2Strategy);

Strategy.prototype.userProfile = function(accessToken, done) {

//LinkedIn uses a custom name for the access_token parameter
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)); }

try {
if (err) {
throw new InternalOAuthError('failed to fetch user profile', err)
}

var json = JSON.parse(body);

var profile = { provider: 'linkedin' };

profile.id = json.id;
profile.displayName = json.formattedName;

var firstName = json.firstName.localized[Object.keys(json.firstName.localized)[0]];
var lastName = json.lastName.localized[Object.keys(json.lastName.localized)[0]];

profile.name = {
familyName: json.lastName,
givenName: json.firstName
};
profile.emails = [{ value: json.emailAddress }];
profile.photos = [];
if (json.pictureUrl) {
profile.photos.push({ value: json.pictureUrl });
givenName: firstName,
familyName: lastName
};

profile.displayName = firstName + ' ' + lastName;

if (
json.profilePicture &&
json.profilePicture['displayImage~'] &&
json.profilePicture['displayImage~'].elements &&
json.profilePicture['displayImage~'].elements.length > 0
) {
profile.photos = json.profilePicture['displayImage~'].elements.reduce(function (memo, el) {
if (el && el.identifiers && el.identifiers.length > 0) {
memo.push({ value: el.identifiers[0].identifier }); // Keep the first pic for now
}
return memo;
}, []);
}

Copy link

Choose a reason for hiding this comment

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

var profilePicture = _.defaults(json.profilePicture, {'displayImage~': { elements:[]}); 

if (profilePicture.elements.length> 0){
profilePicture.elements.reduce(...)
}

profile._profileRaw = body;
profile._profileJson = json;

Choose a reason for hiding this comment

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

was wondering: why this breaking change?

Copy link
Author

Choose a reason for hiding this comment

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

In order to conform with the Linkedin OAuth API, two separate requests need to be executed in order to retrieve the user profile and then the email.

Having code consistency in mind as well as taking into account the JS convention that says that keys starting with _ are considered private, I kept both API raw and JSON responses under the following structure:

profile._profileRaw = body;
profile._profileJson = json;
profile._emailRaw = body;
profile._emailJson = json;

Hope that helps.

Choose a reason for hiding this comment

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

but passport already saved those under profile._raw and profile._json

As I'm using profile._json to get more data I had a bug when switching to your fork. That's how and why I'm calling it breaking. But it's fine as long as it is documented :)


if (!this.options.scope.includes('r_emailaddress')) {
return done(null, profile);
}
profile._raw = body;
profile._json = json;

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

var json = JSON.parse(body);

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

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

done(null, profile);
} catch (e) {
console.log(e);
done(e);
}
}.bind(this));
} catch(e) {
done(e);
}
});
}.bind(this));
}




Strategy.prototype._convertScopeToUserProfileFields = function(scope, profileFields) {
var self = this;
var map = {
'r_basicprofile': [
'id',
'first-name',
'last-name',
'picture-url',
'picture-urls::(original)',
'formatted-name',
'maiden-name',
'phonetic-first-name',
'phonetic-last-name',
'formatted-phonetic-name',
'headline',
'location:(name,country:(code))',
'industry',
'distance',
'relation-to-viewer:(distance,connections)',
'current-share',
'num-connections',
'num-connections-capped',
'summary',
'specialties',
'positions',
'site-standard-profile-request',
'api-standard-profile-request:(headers,url)',
'public-profile-url'
],
'r_emailaddress': ['email-address'],
'r_fullprofile': [
'last-modified-timestamp',
'proposal-comments',
'associations',
'interests',
'publications',
'patents',
'languages',
'skills',
'certifications',
'educations',
'courses',
'volunteer',
'three-current-positions',
'three-past-positions',
'num-recommenders',
'recommendations-received',
'mfeed-rss-url',
'following',
'job-bookmarks',
'suggestions',
'date-of-birth',
'member-url-resources:(name,url)',
'related-profile-views',
'honors-awards'
]
};

var fields = [];

// To obtain pre-defined field mappings
if(Array.isArray(scope) && profileFields === null)
{
if(scope.indexOf('r_basicprofile') === -1){
scope.unshift('r_basicprofile');
}

scope.forEach(function(f) {
if (typeof map[f] === 'undefined') return;

if (Array.isArray(map[f])) {
Array.prototype.push.apply(fields, map[f]);
} else {
fields.push(map[f]);
}
});
}else if (Array.isArray(profileFields)){
fields = profileFields;
}

return fields.join(',');
}



Strategy.prototype.authorizationParams = function(options) {

var params = {};
Expand Down