Skip to content
Prev Previous commit
Next Next commit
Add MultiLocaleStringResolver to let the integrator decide which loca…
…lized value should be taken
  • Loading branch information
asiffermann committed May 22, 2019
commit 1ca0ddeac58a06e50f7749896645c3e52defa96f
71 changes: 45 additions & 26 deletions src/AspNet.Security.OAuth.LinkedIn/LinkedInAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,46 +63,39 @@ public LinkedInAuthenticationOptions()
};

/// <summary>
/// Gets the <c>MultiLocaleString</c> value.
/// First checks if a preferredLocale is returned from the payload, then try to return the value from it.
/// Then checks if a value is available for the current UI culture.
/// Finally, returns the first localized value.
/// Gets or sets a <c>MultiLocaleString</c> resolver, a function which takes all localized values
/// and an eventual preferred locale from the member and returns the selected localized value.
/// The default implementation resolve it in this order:
/// 1. Returns the <c>preferredLocale</c> value if it is set and has a value.
/// 2. Returns the value corresponding to the <see cref="Thread.CurrentUICulture"/> if it exists.
/// 3. Returns the first value.
/// </summary>
/// <see cref="DefaultMultiLocaleStringResolver(IReadOnlyDictionary{string, string}, string)"/>
public Func<IReadOnlyDictionary<string, string>, string, string> MultiLocaleStringResolver { get; set; } = DefaultMultiLocaleStringResolver;

/// <summary>
/// Gets the <c>MultiLocaleString</c> value using the configured resolver.
/// See <a>https://docs.microsoft.com/en-us/linkedin/shared/references/v2/object-types#multilocalestring</a>
/// </summary>
/// <param name="user">The payload returned by the user info endpoint.</param>
/// <param name="propertyName">The name of the <c>MultiLocaleString</c> property.</param>
/// <returns>The property value.</returns>
private static string GetMultiLocaleString(JObject user, string propertyName)
private string GetMultiLocaleString(JObject user, string propertyName)
{
if (user[propertyName] == null)
const string localizedKey = "localized";
if (user[propertyName] == null || user[propertyName][localizedKey] == null)
{
return null;
}

var preferredLocale = user[propertyName]["preferredLocale"];
const string localizedKey = "localized";

if (preferredLocale != null)
{
var preferredKey = $"{preferredLocale["language"]}_{preferredLocale["country"]}";
var preferredLocalizedValue = user[propertyName][localizedKey][preferredKey];
if (preferredLocalizedValue != null)
{
return preferredLocalizedValue.Value<string>();
}
}

var currentUiKey = Thread.CurrentThread.CurrentUICulture.ToString().Replace('-', '_');
var currentUiLocalizedValue = user[propertyName][localizedKey][currentUiKey];
if (currentUiLocalizedValue != null)
{
return currentUiLocalizedValue.Value<string>();
}
var preferredLocaleKey = preferredLocale == null ? null : $"{preferredLocale["language"]}_{preferredLocale["country"]}";
var values = user[propertyName][localizedKey].ToObject<Dictionary<string, string>>();

return user[propertyName][localizedKey].First.Value<string>();
return MultiLocaleStringResolver(values, preferredLocaleKey);
}

private static string GetFullName(JObject user)
private string GetFullName(JObject user)
{
var nameParts = new string[]
{
Expand All @@ -128,5 +121,31 @@ private static IEnumerable<string> GetPictureUrls(JObject user)
where address.Value<string>("authorizationMethod") == "PUBLIC"
select address.Value<JArray>("identifiers")?.First()?.Value<string>("identifier"));
}

/// <summary>
/// The default <c>MultiLocaleString</c> resolver.
/// Resolve it in this order:
/// 1. Returns the <c>preferredLocale</c> value if it is set and has a value.
/// 2. Returns the value corresponding to the <see cref="Thread.CurrentUICulture"/> if it exists.
/// 3. Returns the first value.
/// </summary>
/// <param name="localizedValues">The localized values with culture keys.</param>
/// <param name="preferredLocale">The preferred locale, if provided by LinkedIn.</param>
/// <returns>The localized value.</returns>
private static string DefaultMultiLocaleStringResolver(IReadOnlyDictionary<string, string> localizedValues, string preferredLocale)
{
if (!string.IsNullOrEmpty(preferredLocale) && localizedValues.ContainsKey(preferredLocale))
{
return localizedValues[preferredLocale];
}

var currentUiKey = Thread.CurrentThread.CurrentUICulture.ToString().Replace('-', '_');
if (localizedValues.ContainsKey(currentUiKey))
{
return localizedValues[currentUiKey];
}

return localizedValues.Values.FirstOrDefault();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* for more information concerning the license and the contributors participating to this project.
*/

using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
Expand All @@ -15,6 +17,8 @@ namespace AspNet.Security.OAuth.LinkedIn
{
public class LinkedInTests : OAuthTests<LinkedInAuthenticationOptions>
{
private Action<LinkedInAuthenticationOptions> additionnalConfiguration = null;

public LinkedInTests(ITestOutputHelper outputHelper)
{
OutputHelper = outputHelper;
Expand All @@ -28,6 +32,7 @@ protected internal override void RegisterAuthentication(AuthenticationBuilder bu
{
ConfigureDefaults(builder, options);
options.Fields.Add(LinkedInAuthenticationConstants.ProfileFields.PictureUrl);
additionnalConfiguration?.Invoke(options);
});
}

Expand All @@ -50,5 +55,33 @@ public async Task Can_Sign_In_Using_LinkedIn(string claimType, string claimValue
AssertClaim(claims, claimType, claimValue);
}
}

[Theory]
[InlineData(ClaimTypes.NameIdentifier, "1R2RtA")]
[InlineData(ClaimTypes.Name, "Frodon Sacquet")]
[InlineData(ClaimTypes.GivenName, "Frodon")]
[InlineData(ClaimTypes.Surname, "Sacquet")]
public async Task Can_Sign_In_Using_LinkedIn_Localized(string claimType, string claimValue)
{
// Arrange
additionnalConfiguration = options => options.MultiLocaleStringResolver = (values, preferredLocale) =>
{
if (values.ContainsKey("fr_FR"))
{
return values["fr_FR"];
}

return values.Values.FirstOrDefault();
};

using (var server = CreateTestServer())
{
// Act
var claims = await AuthenticateUserAsync(server);

// Assert
AssertClaim(claims, claimType, claimValue);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"id": "1R2RtA",
"firstName": {
"localized": {
"en_US": "Frodo"
"en_US": "Frodo",
"fr_FR": "Frodon"
},
"preferredLocale": {
"country": "US",
Expand All @@ -28,7 +29,8 @@
},
"lastName": {
"localized": {
"en_US": "Baggins"
"en_US": "Baggins",
"fr_FR": "Sacquet"
},
"preferredLocale": {
"country": "US",
Expand Down