Skip to content

Commit d5f4d40

Browse files
ericgreenmixkevinchalet
authored andcommitted
Extend the LinkedIn provider to add configurable fields and more claims
1 parent 4607c75 commit d5f4d40

File tree

5 files changed

+145
-3
lines changed

5 files changed

+145
-3
lines changed

src/AspNet.Security.OAuth.LinkedIn/LinkedInAuthenticationDefaults.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ public class LinkedInAuthenticationDefaults {
4444

4545
/// <summary>
4646
/// Default value for <see cref="OAuthOptions.UserInformationEndpoint"/>.
47+
/// Note: the endpoint must follow the LinkedIn convention and contain a '~' to append fields to, if they are specified.
48+
/// See https://developer.linkedin.com/docs/signin-with-linkedin for more information.
4749
/// </summary>
48-
public const string UserInformationEndpoint = "https://api.linkedin.com/v1/people/~:(id,first-name,last-name,formatted-name,email-address,public-profile-url,picture-url)";
50+
public const string UserInformationEndpoint = "https://api.linkedin.com/v1/people/~";
4951
}
5052
}

src/AspNet.Security.OAuth.LinkedIn/LinkedInAuthenticationHandler.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ public LinkedInAuthenticationHandler([NotNull] HttpClient client)
2424

2525
protected override async Task<AuthenticationTicket> CreateTicketAsync([NotNull] ClaimsIdentity identity,
2626
[NotNull] AuthenticationProperties properties, [NotNull] OAuthTokenResponse tokens) {
27-
var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
27+
var address = Options.UserInformationEndpoint;
28+
29+
// If at least one field is specified,
30+
// append the fields to the endpoint URL.
31+
if (Options.Fields.Count != 0) {
32+
address = address.Insert(address.LastIndexOf("~") + 1, $":({ string.Join(",", Options.Fields)})");
33+
}
34+
35+
var request = new HttpRequestMessage(HttpMethod.Get, address);
2836
request.Headers.Add("x-li-format", "json");
2937
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
3038

@@ -44,8 +52,26 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync([NotNull]
4452
identity.AddOptionalClaim(ClaimTypes.NameIdentifier, LinkedInAuthenticationHelper.GetIdentifier(payload), Options.ClaimsIssuer)
4553
.AddOptionalClaim(ClaimTypes.Name, LinkedInAuthenticationHelper.GetName(payload), Options.ClaimsIssuer)
4654
.AddOptionalClaim(ClaimTypes.Email, LinkedInAuthenticationHelper.GetEmail(payload), Options.ClaimsIssuer)
55+
.AddOptionalClaim(ClaimTypes.GivenName, LinkedInAuthenticationHelper.GetGivenName(payload), Options.ClaimsIssuer)
56+
.AddOptionalClaim(ClaimTypes.Surname, LinkedInAuthenticationHelper.GetFamilyName(payload), Options.ClaimsIssuer)
4757
.AddOptionalClaim("urn:linkedin:profile", LinkedInAuthenticationHelper.GetPublicProfileUrl(payload), Options.ClaimsIssuer)
48-
.AddOptionalClaim("urn:linkedin:profilepicture", LinkedInAuthenticationHelper.GetProfilePictureUrl(payload), Options.ClaimsIssuer);
58+
.AddOptionalClaim("urn:linkedin:profilepicture", LinkedInAuthenticationHelper.GetProfilePictureUrl(payload), Options.ClaimsIssuer)
59+
.AddOptionalClaim("urn:linkedin:industry", LinkedInAuthenticationHelper.GetIndustry(payload), Options.ClaimsIssuer)
60+
.AddOptionalClaim("urn:linkedin:summary", LinkedInAuthenticationHelper.GetSummary(payload), Options.ClaimsIssuer)
61+
.AddOptionalClaim("urn:linkedin:headline", LinkedInAuthenticationHelper.GetHeadline(payload), Options.ClaimsIssuer)
62+
.AddOptionalClaim("urn:linkedin:positions", LinkedInAuthenticationHelper.GetPositions(payload), Options.ClaimsIssuer)
63+
.AddOptionalClaim("urn:linkedin:maidenname", LinkedInAuthenticationHelper.GetMaidenName(payload), Options.ClaimsIssuer)
64+
.AddOptionalClaim("urn:linkedin:phoneticfirstname", LinkedInAuthenticationHelper.GetPhoneticFirstName(payload), Options.ClaimsIssuer)
65+
.AddOptionalClaim("urn:linkedin:phoneticlastname", LinkedInAuthenticationHelper.GetPhoneticLastName(payload), Options.ClaimsIssuer)
66+
.AddOptionalClaim("urn:linkedin:phoneticname", LinkedInAuthenticationHelper.GetPhoneticName(payload), Options.ClaimsIssuer)
67+
.AddOptionalClaim("urn:linkedin:location", LinkedInAuthenticationHelper.GetLocation(payload), Options.ClaimsIssuer)
68+
.AddOptionalClaim("urn:linkedin:specialties", LinkedInAuthenticationHelper.GetSpecialties(payload), Options.ClaimsIssuer)
69+
.AddOptionalClaim("urn:linkedin:numconnections", LinkedInAuthenticationHelper.GetNumConnections(payload), Options.ClaimsIssuer)
70+
.AddOptionalClaim("urn:linkedin:numconnectionscapped", LinkedInAuthenticationHelper.GetNumConnectionsCapped(payload), Options.ClaimsIssuer)
71+
.AddOptionalClaim("urn:linkedin:currentshare", LinkedInAuthenticationHelper.GetCurrentShare(payload), Options.ClaimsIssuer)
72+
.AddOptionalClaim("urn:linkedin:sitestandardprofilerequest", LinkedInAuthenticationHelper.GetSiteStandardProfileRequest(payload), Options.ClaimsIssuer)
73+
.AddOptionalClaim("urn:linkedin:apistandardprofilerequest", LinkedInAuthenticationHelper.GetApiStandardProfileRequest(payload), Options.ClaimsIssuer)
74+
.AddOptionalClaim("urn:linkedin:pictureurls", LinkedInAuthenticationHelper.GetPictureUrls(payload), Options.ClaimsIssuer);
4975

5076
var principal = new ClaimsPrincipal(identity);
5177
var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);

src/AspNet.Security.OAuth.LinkedIn/LinkedInAuthenticationHelper.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,112 @@ public class LinkedInAuthenticationHelper {
2828
/// </summary>
2929
public static string GetName([NotNull] JObject user) => user.Value<string>("formattedName");
3030

31+
/// <summary>
32+
/// Gets the first name corresponding to the authenticated user.
33+
/// </summary>
34+
public static string GetGivenName([NotNull] JObject user) => user.Value<string>("firstName");
35+
36+
/// <summary>
37+
/// Gets the last name corresponding to the authenticated user.
38+
/// </summary>
39+
public static string GetFamilyName([NotNull] JObject user) => user.Value<string>("lastName");
40+
41+
/// <summary>
42+
/// Gets the maiden name corresponding to the authenticated user.
43+
/// </summary>
44+
public static string GetMaidenName([NotNull] JObject user) => user.Value<string>("maidenName");
45+
46+
/// <summary>
47+
/// Gets the phonetic first name corresponding to the authenticated user.
48+
/// </summary>
49+
public static string GetPhoneticFirstName([NotNull] JObject user) => user.Value<string>("phoneticFirstName");
50+
51+
/// <summary>
52+
/// Gets the phonetic last name corresponding to the authenticated user.
53+
/// </summary>
54+
public static string GetPhoneticLastName([NotNull] JObject user) => user.Value<string>("phoneticLastName");
55+
56+
/// <summary>
57+
/// Gets the phonetic name corresponding to the authenticated user.
58+
/// </summary>
59+
public static string GetPhoneticName([NotNull] JObject user) => user.Value<string>("formattedPhoneticName");
60+
3161
/// <summary>
3262
/// Gets the profile picture URL corresponding to the authenticated user.
3363
/// </summary>
3464
public static string GetProfilePictureUrl([NotNull] JObject user) => user.Value<string>("pictureUrl");
3565

66+
/// <summary>
67+
/// Gets the URL of the member's original unformatted profile picture.
68+
/// This image is usually larger than the picture-url value above.
69+
/// </summary>
70+
public static string GetPictureUrls([NotNull] JObject user) => user["pictureUrls"]?.ToString();
71+
3672
/// <summary>
3773
/// Gets the public profile URL corresponding to the authenticated user.
3874
/// </summary>
3975
public static string GetPublicProfileUrl([NotNull] JObject user) => user.Value<string>("publicProfileUrl");
76+
77+
/// <summary>
78+
/// Gets the industry corresponding to the authenticated user.
79+
/// See https://developer.linkedin.com/docs/reference/industry-codes for more information.
80+
/// </summary>
81+
public static string GetIndustry([NotNull] JObject user) => user.Value<string>("industry");
82+
83+
/// <summary>
84+
/// Gets the summary corresponding to the authenticated user.
85+
/// </summary>
86+
public static string GetSummary([NotNull] JObject user) => user.Value<string>("summary");
87+
88+
/// <summary>
89+
/// Gets the headline corresponding to the authenticated user.
90+
/// </summary>
91+
public static string GetHeadline([NotNull] JObject user) => user.Value<string>("headline");
92+
93+
/// <summary>
94+
/// Gets the specialties corresponding to the authenticated user.
95+
/// </summary>
96+
public static string GetSpecialties([NotNull] JObject user) => user.Value<string>("specialties");
97+
98+
/// <summary>
99+
/// Gets the location object corresponding to the authenticated user.
100+
/// See https://developer.linkedin.com/docs/fields/location for more information.
101+
/// </summary>
102+
public static string GetLocation([NotNull] JObject user) => user["location"]?.ToString();
103+
104+
/// <summary>
105+
/// Gets the most recent item the member has shared on LinkedIn. If the member has not shared anything, their 'status' is returned instead.
106+
/// </summary>
107+
public static string GetCurrentShare([NotNull] JObject user) => user.Value<string>("currentShare");
108+
109+
/// <summary>
110+
/// Gets the positions object corresponding to the authenticated user.
111+
/// See https://developer.linkedin.com/docs/fields/positions for more information.
112+
/// </summary>
113+
public static string GetPositions([NotNull] JObject user) => user["positions"]?.ToString();
114+
115+
/// <summary>
116+
/// Gets the number of LinkedIn connections the member has, capped at 500.
117+
/// See 'num-connections-capped' to determine if the value returned has been capped.
118+
/// </summary>
119+
public static string GetNumConnections([NotNull] JObject user) => user.Value<string>("numConnections");
120+
121+
/// <summary>
122+
/// Gets a boolean indicating whether the member's 'num-connections' value
123+
/// has been capped at 500 or represents the user's true value.
124+
/// </summary>
125+
public static string GetNumConnectionsCapped([NotNull] JObject user) => user.Value<string>("numConnectionsCapped");
126+
127+
/// <summary>
128+
/// Gets the URL representing the resource one would request
129+
/// for programmatic access to the member's profile.
130+
/// </summary>
131+
public static string GetApiStandardProfileRequest([NotNull] JObject user) => user.Value<string>("apiStandardProfileRequest");
132+
133+
/// <summary>
134+
/// Gets the URL to the member's authenticated profile on LinkedIn.
135+
/// Note: one must be logged into LinkedIn to view this URL.
136+
/// </summary>
137+
public static string GetSiteStandardProfileRequest([NotNull] JObject user) => user.Value<string>("siteStandardProfileRequest");
40138
}
41139
}

src/AspNet.Security.OAuth.LinkedIn/LinkedInAuthenticationMiddleware.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* for more information concerning the license and the contributors participating to this project.
55
*/
66

7+
using System;
78
using System.Text.Encodings.Web;
89
using JetBrains.Annotations;
910
using Microsoft.AspNetCore.Authentication;
@@ -23,6 +24,10 @@ public LinkedInAuthenticationMiddleware(
2324
[NotNull] UrlEncoder encoder,
2425
[NotNull] IOptions<SharedAuthenticationOptions> externalOptions)
2526
: base(next, dataProtectionProvider, loggerFactory, encoder, externalOptions, options) {
27+
if (Options.Fields.Count != 0 && !Options.UserInformationEndpoint.Contains("~")) {
28+
throw new ArgumentException("The user information endpoint is improperly formatted. " +
29+
"The endpoint must contain a '~' to append the supplied fields.", nameof(options));
30+
}
2631
}
2732

2833
protected override AuthenticationHandler<LinkedInAuthenticationOptions> CreateHandler() {

src/AspNet.Security.OAuth.LinkedIn/LinkedInAuthenticationOptions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* for more information concerning the license and the contributors participating to this project.
55
*/
66

7+
using System.Collections.Generic;
78
using Microsoft.AspNetCore.Builder;
89
using Microsoft.AspNetCore.Http;
910

@@ -23,5 +24,15 @@ public LinkedInAuthenticationOptions() {
2324
TokenEndpoint = LinkedInAuthenticationDefaults.TokenEndpoint;
2425
UserInformationEndpoint = LinkedInAuthenticationDefaults.UserInformationEndpoint;
2526
}
27+
28+
/// <summary>
29+
/// Gets the list of fields to retrieve from the user information endpoint.
30+
/// See https://developer.linkedin.com/docs/fields/basic-profile for more information.
31+
/// </summary>
32+
public ICollection<string> Fields { get; } = new HashSet<string> {
33+
"id",
34+
"formatted-name",
35+
"email-address"
36+
};
2637
}
2738
}

0 commit comments

Comments
 (0)