Skip to content

Commit 7a9f5e0

Browse files
committed
Tricky github->linkedin changes
1 parent 2eab7f2 commit 7a9f5e0

File tree

5 files changed

+62
-74
lines changed

5 files changed

+62
-74
lines changed

src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module.exports = {
55
LINKEDIN_API_URL: process.env.LINKEDIN_API_URL,
66
LINKEDIN_LOGIN_URL: process.env.LINKEDIN_LOGIN_URL,
77
PORT: parseInt(process.env.PORT, 10) || undefined,
8+
LINKEDIN_SCOPE: process.env.LINKEDIN_SCOPE,
89

910
// Splunk logging variables
1011
SPLUNK_URL: process.env.SPLUNK_URL,

src/connectors/logger.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ if (SPLUNK_URL) {
3939
new winston.transports.Console({
4040
format: winston.format.combine(
4141
winston.format.splat(),
42-
winston.format.colorize({ all: true }),
4342
winston.format.simple()
4443
)
4544
})

src/connectors/web/handlers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = {
1515
jwks: (req, res) => controllers(responder(res)).jwks(),
1616
authorize: (req, res) =>
1717
responder(res).redirect(
18-
`https://github.com/login/oauth/authorize?client_id=${
18+
`https://www.linkedin.com/oauth/v2/authorization?client_id=${
1919
req.query.client_id
2020
}&scope=${req.query.scope}&state=${req.query.state}&response_type=${
2121
req.query.response_type

src/linkedin.js

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
11
const axios = require('axios');
2+
const qs = require('qs');
23
const {
3-
GITHUB_CLIENT_ID,
4-
GITHUB_CLIENT_SECRET,
4+
LINKEDIN_CLIENT_ID,
5+
LINKEDIN_CLIENT_SECRET,
56
COGNITO_REDIRECT_URI,
6-
GITHUB_API_URL,
7-
GITHUB_LOGIN_URL
7+
LINKEDIN_API_URL,
8+
LINKEDIN_LOGIN_URL,
9+
LINKEDIN_SCOPE,
810
} = require('./config');
911
const logger = require('./connectors/logger');
1012

1113
const getApiEndpoints = (
12-
apiBaseUrl = GITHUB_API_URL,
13-
loginBaseUrl = GITHUB_LOGIN_URL
14+
apiBaseUrl = LINKEDIN_API_URL,
15+
loginBaseUrl = LINKEDIN_LOGIN_URL
1416
) => ({
15-
userDetails: `${apiBaseUrl}/user`,
16-
userEmails: `${apiBaseUrl}/user/emails`,
17-
oauthToken: `${loginBaseUrl}/login/oauth/access_token`,
18-
oauthAuthorize: `${loginBaseUrl}/login/oauth/authorize`
17+
userDetails: `${apiBaseUrl}/v2/me`,
18+
userEmails: `${apiBaseUrl}/v2/clientAwareMemberHandles?q=members&projection=(elements*(primary,type,handle~))`,
19+
oauthToken: `${apiBaseUrl}/oauth/v2/accessToken`,
20+
oauthAuthorize: `${loginBaseUrl}/oauth/v2/authorization`,
1921
});
2022

2123
const check = response => {
2224
logger.debug('Checking response: %j', response, {});
2325
if (response.data) {
2426
if (response.data.error) {
2527
throw new Error(
26-
`GitHub API responded with a failure: ${response.data.error}, ${
28+
`LinkedIn API responded with a failure: ${response.data.error}, ${
2729
response.data.error_description
2830
}`
2931
);
@@ -32,42 +34,42 @@ const check = response => {
3234
}
3335
}
3436
throw new Error(
35-
`GitHub API responded with a failure: ${response.status} (${
37+
`LinkedIn API responded with a failure: ${response.status} (${
3638
response.statusText
3739
})`
3840
);
3941
};
4042

41-
const gitHubGet = (url, accessToken) =>
43+
const linkedinGet = (url, accessToken) =>
4244
axios({
4345
method: 'get',
4446
url,
4547
headers: {
46-
Accept: 'application/vnd.github.v3+json',
47-
Authorization: `token ${accessToken}`
48+
Authorization: `Bearer ${accessToken}`
4849
}
4950
});
5051

5152
module.exports = (apiBaseUrl, loginBaseUrl) => {
5253
const urls = getApiEndpoints(apiBaseUrl, loginBaseUrl || apiBaseUrl);
5354
return {
54-
getAuthorizeUrl: (client_id, scope, state, response_type) =>
55-
`${urls.oauthAuthorize}?client_id=${client_id}&scope=${encodeURIComponent(
56-
scope
57-
)}&state=${state}&response_type=${response_type}`,
55+
getAuthorizeUrl: (client_id, scope, state, response_type) => {
56+
const scopesToSend = scope.split(' ').filter(s => s !== 'openid').join(' ');
57+
return `${urls.oauthAuthorize}?client_id=${client_id}&scope=${encodeURIComponent(
58+
scopesToSend
59+
)}&state=${state}&response_type=${response_type}&redirect_uri=${COGNITO_REDIRECT_URI}`;
60+
},
5861
getUserDetails: accessToken =>
59-
gitHubGet(urls.userDetails, accessToken).then(check),
62+
linkedinGet(urls.userDetails, accessToken).then(check),
6063
getUserEmails: accessToken =>
61-
gitHubGet(urls.userEmails, accessToken).then(check),
64+
linkedinGet(urls.userEmails, accessToken).then(check),
6265
getToken: (code, state) => {
6366
const data = {
6467
// OAuth required fields
6568
grant_type: 'authorization_code',
6669
redirect_uri: COGNITO_REDIRECT_URI,
67-
client_id: GITHUB_CLIENT_ID,
68-
// GitHub Specific
70+
client_id: LINKEDIN_CLIENT_ID,
6971
response_type: 'code',
70-
client_secret: GITHUB_CLIENT_SECRET,
72+
client_secret: LINKEDIN_CLIENT_SECRET,
7173
code,
7274
// State may not be present, so we conditionally include it
7375
...(state && { state })
@@ -79,15 +81,22 @@ module.exports = (apiBaseUrl, loginBaseUrl) => {
7981
data,
8082
{}
8183
);
82-
return axios({
83-
method: 'post',
84-
url: urls.oauthToken,
85-
headers: {
86-
Accept: 'application/json',
87-
'Content-Type': 'application/json'
88-
},
89-
data
90-
}).then(check);
84+
return axios.post(
85+
urls.oauthToken,
86+
qs.stringify(data),
87+
{
88+
headers: {
89+
Accept: 'application/json',
90+
// 'Content-Type': 'application/json'
91+
},
92+
}
93+
).then(check)
94+
.then(data => {
95+
// Because LinkedIn doesn't return the scopes
96+
data.scope = LINKEDIN_SCOPE;
97+
data.token_type = 'bearer';
98+
return data;
99+
})
91100
}
92101
};
93102
};

src/openid.js

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,40 @@
11
const logger = require('./connectors/logger');
22
const { NumericDate } = require('./helpers');
33
const crypto = require('./crypto');
4-
const github = require('./linkedin');
4+
const linkedin = require('./linkedin');
55

66
const getJwks = () => ({ keys: [crypto.getPublicKey()] });
77

88
const getUserInfo = accessToken =>
99
Promise.all([
10-
github()
10+
linkedin()
1111
.getUserDetails(accessToken)
1212
.then(userDetails => {
1313
logger.debug('Fetched user details: %j', userDetails, {});
14-
// Here we map the github user response to the standard claims from
14+
// Here we map the linkedin user response to the standard claims from
1515
// OpenID. The mapping was constructed by following
16-
// https://developer.github.com/v3/users/
17-
// and http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
16+
// https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api?context=linkedin/consumer/context
17+
// and
18+
// https://docs.microsoft.com/en-us/linkedin/shared/references/v2/profile/lite-profile
1819
const claims = {
1920
sub: `${userDetails.id}`, // OpenID requires a string
20-
name: userDetails.name,
21-
preferred_username: userDetails.login,
22-
profile: userDetails.html_url,
23-
picture: userDetails.avatar_url,
24-
website: userDetails.blog,
25-
updated_at: NumericDate(
26-
// OpenID requires the seconds since epoch in UTC
27-
new Date(Date.parse(userDetails.updated_at))
28-
)
21+
name: `${userDetails.firstName.localized} ${userDetails.lastName.localized}`,
2922
};
3023
logger.debug('Resolved claims: %j', claims, {});
3124
return claims;
3225
}),
33-
github()
26+
linkedin()
3427
.getUserEmails(accessToken)
3528
.then(userEmails => {
3629
logger.debug('Fetched user emails: %j', userEmails, {});
37-
const primaryEmail = userEmails.find(email => email.primary);
30+
const primaryEmail = userEmails.elements.find(email => email.primary && (email.type === 'EMAIL'));
3831
if (primaryEmail === undefined) {
3932
throw new Error('User did not have a primary email address');
4033
}
34+
const emailAddress = primaryEmail['handle~'].emailAddress;
4135
const claims = {
42-
email: primaryEmail.email,
43-
email_verified: primaryEmail.verified
36+
email: emailAddress,
37+
email_verified: true,
4438
};
4539
logger.debug('Resolved claims: %j', claims, {});
4640
return claims;
@@ -55,23 +49,16 @@ const getUserInfo = accessToken =>
5549
});
5650

5751
const getAuthorizeUrl = (client_id, scope, state, response_type) =>
58-
github().getAuthorizeUrl(client_id, scope, state, response_type);
52+
linkedin().getAuthorizeUrl(client_id, scope, state, response_type);
5953

6054
const getTokens = (code, state, host) =>
61-
github()
55+
linkedin()
6256
.getToken(code, state)
63-
.then(githubToken => {
64-
logger.debug('Got token: %s', githubToken, {});
65-
// GitHub returns scopes separated by commas
66-
// But OAuth wants them to be spaces
67-
// https://tools.ietf.org/html/rfc6749#section-5.1
68-
// Also, we need to add openid as a scope,
69-
// since GitHub will have stripped it
70-
const scope = `openid ${githubToken.scope.replace(',', ' ')}`;
71-
57+
.then(linkedinToken => {
58+
logger.debug('Got token: %s', linkedinToken, {});
7259
// ** JWT ID Token required fields **
7360
// iss - issuer https url
74-
// aud - audience that this token is valid for (GITHUB_CLIENT_ID)
61+
// aud - audience that this token is valid for (LINKEDIN_CLIENT_ID)
7562
// sub - subject identifier - must be unique
7663
// ** Also required, but provided by jsonwebtoken **
7764
// exp - expiry time for the id token (seconds since epoch in UTC)
@@ -87,8 +74,7 @@ const getTokens = (code, state, host) =>
8774

8875
const idToken = crypto.makeIdToken(payload, host);
8976
const tokenResponse = {
90-
...githubToken,
91-
scope,
77+
...linkedinToken,
9278
id_token: idToken
9379
};
9480

@@ -128,15 +114,8 @@ const getConfigFor = host => ({
128114
claims_supported: [
129115
'sub',
130116
'name',
131-
'preferred_username',
132-
'profile',
133-
'picture',
134-
'website',
135117
'email',
136118
'email_verified',
137-
'updated_at',
138-
'iss',
139-
'aud'
140119
]
141120
});
142121

0 commit comments

Comments
 (0)