Merge pull request #253 from todorm85/master
Migration to LinkedIn v2.0 API (deadline March 2019)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
@@ -17,7 +18,9 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
{
|
||||
private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string";
|
||||
private const string TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
|
||||
private const string UserInfoEndpoint = "https://api.linkedin.com/v1/people/";
|
||||
private const string UserInfoEndpoint = "https://api.linkedin.com/v2/me";
|
||||
private const string AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
|
||||
private const string EmailEndpoint = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))";
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
@@ -34,21 +37,7 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
|
||||
try
|
||||
{
|
||||
string code = null;
|
||||
string state = null;
|
||||
|
||||
var query = Request.Query;
|
||||
var values = query.GetValues("code");
|
||||
if (values != null && values.Count == 1)
|
||||
{
|
||||
code = values[0];
|
||||
}
|
||||
values = query.GetValues("state");
|
||||
if (values != null && values.Count == 1)
|
||||
{
|
||||
state = values[0];
|
||||
}
|
||||
|
||||
var state = GetQueryValue("state");
|
||||
properties = Options.StateDataFormat.Unprotect(state);
|
||||
if (properties == null)
|
||||
{
|
||||
@@ -61,94 +50,31 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
return new AuthenticationTicket(null, properties);
|
||||
}
|
||||
|
||||
var requestPrefix = Request.Scheme + "://" + this.GetHostName();
|
||||
var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath;
|
||||
|
||||
// Build up the body for the token request
|
||||
var body = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("redirect_uri", redirectUri),
|
||||
new KeyValuePair<string, string>("client_id", Options.ClientId),
|
||||
new KeyValuePair<string, string>("client_secret", Options.ClientSecret)
|
||||
};
|
||||
|
||||
// Request the token
|
||||
var tokenResponse = await _httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body));
|
||||
tokenResponse.EnsureSuccessStatusCode();
|
||||
var text = await tokenResponse.Content.ReadAsStringAsync();
|
||||
|
||||
// Deserializes the token response
|
||||
dynamic response = JsonConvert.DeserializeObject<dynamic>(text);
|
||||
var code = GetQueryValue("code");
|
||||
var accessTokenResponse = await GetAccessToken(code);
|
||||
dynamic response = JsonConvert.DeserializeObject<dynamic>(accessTokenResponse);
|
||||
var accessToken = (string)response.access_token;
|
||||
var expires = (string) response.expires_in;
|
||||
var expires = (string)response.expires_in;
|
||||
|
||||
// Get the LinkedIn user
|
||||
var userInfoEndpoint = UserInfoEndpoint
|
||||
+ "~:("+ string.Join(",", Options.ProfileFields.Distinct().ToArray()) +")"
|
||||
+ "?oauth2_access_token=" + Uri.EscapeDataString(accessToken);
|
||||
var userRequest = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint);
|
||||
userRequest.Headers.Add("x-li-format", "json");
|
||||
var graphResponse = await _httpClient.SendAsync(userRequest, Request.CallCancelled);
|
||||
graphResponse.EnsureSuccessStatusCode();
|
||||
text = await graphResponse.Content.ReadAsStringAsync();
|
||||
var user = JObject.Parse(text);
|
||||
var userInfoResponse = await GetUserInfo(accessToken);
|
||||
var user = JObject.Parse(userInfoResponse);
|
||||
string email = null;
|
||||
if (Options.Scope.Contains(LinkedInAuthenticationOptions.EmailAddressScopeName))
|
||||
{
|
||||
email = await GetUserEmail(accessToken);
|
||||
}
|
||||
|
||||
var context = new LinkedInAuthenticatedContext(Context, user, accessToken, expires)
|
||||
var context = new LinkedInAuthenticatedContext(Context, user, accessToken, expires, email)
|
||||
{
|
||||
Identity = new ClaimsIdentity(
|
||||
Options.AuthenticationType,
|
||||
ClaimsIdentity.DefaultNameClaimType,
|
||||
ClaimsIdentity.DefaultRoleClaimType)
|
||||
};
|
||||
if (!string.IsNullOrEmpty(context.Id))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Email))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.GivenName))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.FamilyName))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Name))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, XmlSchemaString, Options.AuthenticationType));
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:name", context.Name, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Industry))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:industry", context.Industry, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Positions))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:positions", context.Positions, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Summary))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:summary", context.Summary, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Headline))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:headline", context.Headline, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Link))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:url", context.Link, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.AccessToken))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:accesstoken", context.AccessToken, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
context.Properties = properties;
|
||||
|
||||
AddClaimsToContextIdentity(context);
|
||||
|
||||
context.Properties = properties;
|
||||
await Options.Provider.Authenticated(context);
|
||||
|
||||
return new AuthenticationTicket(context.Identity, context.Properties);
|
||||
@@ -206,7 +132,7 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
var state = Options.StateDataFormat.Protect(properties);
|
||||
|
||||
var authorizationEndpoint =
|
||||
"https://www.linkedin.com/oauth/v2/authorization" +
|
||||
AuthorizationEndpoint +
|
||||
"?response_type=code" +
|
||||
"&client_id=" + Uri.EscapeDataString(Options.ClientId) +
|
||||
"&redirect_uri=" + Uri.EscapeDataString(redirectUri) +
|
||||
@@ -281,5 +207,123 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(Options.ProxyHost) ? Request.Host.ToString() : Options.ProxyHost;
|
||||
}
|
||||
|
||||
private static void SetAuthorizedRequestHeaders(string accessToken, HttpRequestMessage userRequest)
|
||||
{
|
||||
userRequest.Headers.Add("x-li-format", "json");
|
||||
userRequest.Headers.Add("Authorization", "Bearer " + accessToken);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserEmail(string accessToken)
|
||||
{
|
||||
var emailRequest = new HttpRequestMessage(HttpMethod.Get, EmailEndpoint);
|
||||
SetAuthorizedRequestHeaders(accessToken, emailRequest);
|
||||
var graphResponse = await _httpClient.SendAsync(emailRequest, Request.CallCancelled);
|
||||
try
|
||||
{
|
||||
graphResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.WriteError(string.Format("Retrieving the user email using authorization from provider {0} failed. Message: {1}", Options.AuthenticationType, e.Message));
|
||||
throw;
|
||||
}
|
||||
|
||||
var text = await graphResponse.Content.ReadAsStringAsync();
|
||||
var emailResponse = JObject.Parse(text);
|
||||
string email = null;
|
||||
var emailValue = emailResponse.SelectToken("elements[0].handle~.emailAddress");
|
||||
if (emailValue != null)
|
||||
{
|
||||
email = emailValue.Value<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorMessageValue = emailResponse.SelectToken("elements[0].handle!.message");
|
||||
var errorMessage = string.Empty;
|
||||
if (errorMessageValue != null)
|
||||
{
|
||||
errorMessage = errorMessageValue.Value<string>();
|
||||
}
|
||||
|
||||
_logger.WriteWarning("Could not retrieve the user email from LinkedIn. Message: " + errorMessage);
|
||||
}
|
||||
|
||||
return await Task.FromResult<string>(email);
|
||||
}
|
||||
|
||||
private async Task<string> GetAccessToken(string code)
|
||||
{
|
||||
var requestPrefix = Request.Scheme + "://" + this.GetHostName();
|
||||
var redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath;
|
||||
|
||||
// Build up the body for the token request
|
||||
var body = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("redirect_uri", redirectUri),
|
||||
new KeyValuePair<string, string>("client_id", Options.ClientId),
|
||||
new KeyValuePair<string, string>("client_secret", Options.ClientSecret)
|
||||
};
|
||||
|
||||
// Request the token
|
||||
var tokenResponse = await _httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body));
|
||||
tokenResponse.EnsureSuccessStatusCode();
|
||||
return await tokenResponse.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
private async Task<string> GetUserInfo(string accessToken)
|
||||
{
|
||||
var userInfoEndpoint = UserInfoEndpoint
|
||||
+ "?projection=(" + string.Join(",", Options.ProfileFields.Distinct().ToArray()) + ")";
|
||||
var userRequest = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint);
|
||||
SetAuthorizedRequestHeaders(accessToken, userRequest);
|
||||
var graphResponse = await _httpClient.SendAsync(userRequest, Request.CallCancelled);
|
||||
graphResponse.EnsureSuccessStatusCode();
|
||||
return await graphResponse.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
private void AddClaimsToContextIdentity(LinkedInAuthenticatedContext context)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(context.Id))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Email))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.GivenName))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.FamilyName))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.Name))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, XmlSchemaString, Options.AuthenticationType));
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:name", context.Name, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(context.AccessToken))
|
||||
{
|
||||
context.Identity.AddClaim(new Claim("urn:linkedin:accesstoken", context.AccessToken, XmlSchemaString, Options.AuthenticationType));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetQueryValue(string name)
|
||||
{
|
||||
string result = null;
|
||||
var query = Request.Query;
|
||||
var values = query.GetValues(name);
|
||||
if (values != null && values.Count == 1)
|
||||
{
|
||||
result = values[0];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,11 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
{
|
||||
public class LinkedInAuthenticationOptions : AuthenticationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The calim name that is used to request permission to retrieve the user address during the authorization call
|
||||
/// </summary>
|
||||
public const string EmailAddressScopeName = "r_emailaddress";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the a pinned certificate validator to use to validate the endpoints used
|
||||
/// in back channel communications belong to LinkedIn.
|
||||
@@ -118,22 +123,15 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
AuthenticationMode = AuthenticationMode.Passive;
|
||||
Scope = new List<string>
|
||||
{
|
||||
"r_basicprofile",
|
||||
"r_emailaddress"
|
||||
"r_liteprofile",
|
||||
EmailAddressScopeName
|
||||
};
|
||||
ProfileFields = new List<string>
|
||||
{
|
||||
"id",
|
||||
"first-name",
|
||||
"last-name",
|
||||
"formatted-name",
|
||||
"email-address",
|
||||
"public-profile-url",
|
||||
"picture-url",
|
||||
"industry",
|
||||
"headline",
|
||||
"summary",
|
||||
"positions"
|
||||
"firstName",
|
||||
"lastName",
|
||||
"profilePicture(displayImage~:playableStreams)"
|
||||
};
|
||||
BackchannelTimeout = TimeSpan.FromSeconds(60);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ using System.Security.Claims;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security;
|
||||
using Microsoft.Owin.Security.Provider;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Owin.Security.Providers.LinkedIn
|
||||
{
|
||||
@@ -26,26 +26,35 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
public LinkedInAuthenticatedContext(IOwinContext context, JObject user, string accessToken, string expires)
|
||||
: base(context)
|
||||
{
|
||||
User = user;
|
||||
AccessToken = accessToken;
|
||||
this.User = user;
|
||||
this.AccessToken = accessToken;
|
||||
|
||||
int expiresValue;
|
||||
if (int.TryParse(expires, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresValue))
|
||||
{
|
||||
ExpiresIn = TimeSpan.FromSeconds(expiresValue);
|
||||
this.ExpiresIn = TimeSpan.FromSeconds(expiresValue);
|
||||
}
|
||||
|
||||
Id = TryGetValue(user, "id");
|
||||
Name = TryGetValue(user, "formattedName");
|
||||
FamilyName = TryGetValue(user, "lastName");
|
||||
GivenName = TryGetValue(user, "firstName");
|
||||
Link = TryGetValue(user, "publicProfileUrl");
|
||||
UserName = TryGetValue(user, "formattedName").Replace(" ", "");
|
||||
Email = TryGetValue(user, "emailAddress");
|
||||
Industry = TryGetValue(user, "industry");
|
||||
Summary = TryGetValue(user, "summary");
|
||||
Headline = TryGetValue(user, "headline");
|
||||
Positions = TryGetValueAndSerialize(user, "positions");
|
||||
this.Id = TryGetValue(user, "id");
|
||||
this.FamilyName = TryGetLocalizedValue(user, "lastName");
|
||||
this.GivenName = TryGetLocalizedValue(user, "firstName");
|
||||
if (this.FamilyName != null || this.GivenName != null)
|
||||
{
|
||||
this.Name = string.Join(" ", this.GivenName, this.FamilyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a <see cref="LinkedInAuthenticatedContext"/>
|
||||
/// </summary>
|
||||
/// <param name="context">The OWIN environment</param>
|
||||
/// <param name="user">The JSON-serialized user</param>
|
||||
/// <param name="accessToken">LinkedIn Access token</param>
|
||||
/// <param name="expires">Seconds until expiration</param>
|
||||
/// <param name="expires">User email returned from dedicated endpoint</param>
|
||||
public LinkedInAuthenticatedContext(IOwinContext context, JObject user, string accessToken, string expires, string email) : this(context, user, accessToken, expires)
|
||||
{
|
||||
this.Email = email;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -95,25 +104,30 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
/// <summary>
|
||||
/// Describes the users membership profile
|
||||
/// </summary>
|
||||
[Obsolete("LinkedIn doesn't return this claim anymore.")]
|
||||
public string Summary { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Industry the member belongs to
|
||||
/// https://developer.linkedin.com/docs/reference/industry-codes
|
||||
/// </summary>
|
||||
[Obsolete("LinkedIn doesn't return this claim anymore.")]
|
||||
public string Industry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The members headline
|
||||
/// </summary>
|
||||
[Obsolete("LinkedIn doesn't return this claim anymore.")]
|
||||
public string Headline { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Member's current positions
|
||||
/// https://developer.linkedin.com/docs/fields/positions
|
||||
/// </summary>
|
||||
[Obsolete("LinkedIn doesn't return this claim anymore.")]
|
||||
public string Positions { get; set; }
|
||||
|
||||
[Obsolete("LinkedIn doesn't return this claim anymore.")]
|
||||
public string Link { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -142,5 +156,28 @@ namespace Owin.Security.Providers.LinkedIn
|
||||
JToken value;
|
||||
return user.TryGetValue(propertyName, out value) ? JsonConvert.SerializeObject(value) : null;
|
||||
}
|
||||
|
||||
private static string TryGetLocalizedValue(JObject container, string propertyName)
|
||||
{
|
||||
var localizedValues = container.SelectToken(propertyName + ".localized") as JObject;
|
||||
if (localizedValues == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var defaultValue = TryGetValue(localizedValues, "en-US");
|
||||
if (defaultValue != null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (localizedValues.HasValues)
|
||||
{
|
||||
return localizedValues.First.First.Value<string>();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user