diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index 4c0faaa16..42ce6a29d 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -112,7 +112,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) // You must first create an app with GitHub and add its ID and Secret to your user-secrets. // https://console.developers.google.com/project - app.UseGoogleAuthentication(new GoogleOptions + var googleOptions = new GoogleOptions { ClientId = Configuration["google:clientid"], ClientSecret = Configuration["google:clientsecret"], @@ -126,11 +126,14 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) return Task.FromResult(0); } } - }); + }; + googleOptions.ClaimActions.MapJsonSubKey("urn:google:image", "image", "url"); + googleOptions.ClaimActions.Remove(ClaimTypes.GivenName); + app.UseGoogleAuthentication(googleOptions); // You must first create an app with Twitter and add its key and Secret to your user-secrets. // https://apps.twitter.com/ - app.UseTwitterAuthentication(new TwitterOptions + var twitterOptions = new TwitterOptions { ConsumerKey = Configuration["twitter:consumerkey"], ConsumerSecret = Configuration["twitter:consumersecret"], @@ -140,12 +143,6 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) SaveTokens = true, Events = new TwitterEvents() { - OnCreatingTicket = ctx => - { - var profilePic = ctx.User.Value("profile_image_url"); - ctx.Principal.Identities.First().AddClaim(new Claim("urn:twitter:profilepicture", profilePic, ClaimTypes.Uri, ctx.Options.ClaimsIssuer)); - return Task.FromResult(0); - }, OnRemoteFailure = ctx => { ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message)); @@ -153,7 +150,9 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) return Task.FromResult(0); } } - }); + }; + twitterOptions.ClaimActions.MapJsonKey("urn:twitter:profilepicture", "profile_image_url", ClaimTypes.Uri); + app.UseTwitterAuthentication(twitterOptions); /* Azure AD app model v2 has restrictions that prevent the use of plain HTTP for redirect URLs. Therefore, to authenticate through microsoft accounts, tryout the sample using the following URL: @@ -200,7 +199,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) // You must first create an app with GitHub and add its ID and Secret to your user-secrets. // https://github.com/settings/applications/ - app.UseOAuthAuthentication(new OAuthOptions + var githubOptions = new OAuthOptions { AuthenticationScheme = "GitHub", DisplayName = "Github", @@ -227,48 +226,16 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) var user = JObject.Parse(await response.Content.ReadAsStringAsync()); - var identifier = user.Value("id"); - if (!string.IsNullOrEmpty(identifier)) - { - context.Identity.AddClaim(new Claim( - ClaimTypes.NameIdentifier, identifier, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } - - var userName = user.Value("login"); - if (!string.IsNullOrEmpty(userName)) - { - context.Identity.AddClaim(new Claim( - ClaimsIdentity.DefaultNameClaimType, userName, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } - - var name = user.Value("name"); - if (!string.IsNullOrEmpty(name)) - { - context.Identity.AddClaim(new Claim( - "urn:github:name", name, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } - - var email = user.Value("email"); - if (!string.IsNullOrEmpty(email)) - { - context.Identity.AddClaim(new Claim( - ClaimTypes.Email, email, - ClaimValueTypes.Email, context.Options.ClaimsIssuer)); - } - - var link = user.Value("url"); - if (!string.IsNullOrEmpty(link)) - { - context.Identity.AddClaim(new Claim( - "urn:github:url", link, - ClaimValueTypes.String, context.Options.ClaimsIssuer)); - } + context.RunClaimActions(user); } } - }); + }; + githubOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + githubOptions.ClaimActions.MapJsonKey(ClaimTypes.Name, "login"); + githubOptions.ClaimActions.MapJsonKey("urn:github:name", "name"); + githubOptions.ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); + githubOptions.ClaimActions.MapJsonKey("urn:github:url", "url"); + app.UseOAuthAuthentication(githubOptions); // Choose an authentication type app.Map("/login", signoutApp => @@ -357,7 +324,7 @@ public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory) } await context.Response.WriteAsync("Tokens:
"); - + await context.Response.WriteAsync("Access Token: " + await context.Authentication.GetTokenAsync("access_token") + "
"); await context.Response.WriteAsync("Refresh Token: " + await context.Authentication.GetTokenAsync("refresh_token") + "
"); await context.Response.WriteAsync("Token Type: " + await context.Authentication.GetTokenAsync("token_type") + "
"); diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs index be1c8f8f8..3c3c14c86 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHandler.cs @@ -45,90 +45,7 @@ protected override async Task CreateTicketAsync(ClaimsIden var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), properties, Options.AuthenticationScheme); var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens, payload); - - var identifier = FacebookHelper.GetId(payload); - if (!string.IsNullOrEmpty(identifier)) - { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var ageRangeMin = FacebookHelper.GetAgeRangeMin(payload); - if (!string.IsNullOrEmpty(ageRangeMin)) - { - identity.AddClaim(new Claim("urn:facebook:age_range_min", ageRangeMin, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var ageRangeMax = FacebookHelper.GetAgeRangeMax(payload); - if (!string.IsNullOrEmpty(ageRangeMax)) - { - identity.AddClaim(new Claim("urn:facebook:age_range_max", ageRangeMax, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var birthday = FacebookHelper.GetBirthday(payload); - if (!string.IsNullOrEmpty(birthday)) - { - identity.AddClaim(new Claim(ClaimTypes.DateOfBirth, birthday, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var email = FacebookHelper.GetEmail(payload); - if (!string.IsNullOrEmpty(email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var firstName = FacebookHelper.GetFirstName(payload); - if (!string.IsNullOrEmpty(firstName)) - { - identity.AddClaim(new Claim(ClaimTypes.GivenName, firstName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var gender = FacebookHelper.GetGender(payload); - if (!string.IsNullOrEmpty(gender)) - { - identity.AddClaim(new Claim(ClaimTypes.Gender, gender, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var lastName = FacebookHelper.GetLastName(payload); - if (!string.IsNullOrEmpty(lastName)) - { - identity.AddClaim(new Claim(ClaimTypes.Surname, lastName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var link = FacebookHelper.GetLink(payload); - if (!string.IsNullOrEmpty(link)) - { - identity.AddClaim(new Claim("urn:facebook:link", link, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var location = FacebookHelper.GetLocation(payload); - if (!string.IsNullOrEmpty(location)) - { - identity.AddClaim(new Claim("urn:facebook:location", location, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var locale = FacebookHelper.GetLocale(payload); - if (!string.IsNullOrEmpty(locale)) - { - identity.AddClaim(new Claim(ClaimTypes.Locality, locale, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var middleName = FacebookHelper.GetMiddleName(payload); - if (!string.IsNullOrEmpty(middleName)) - { - identity.AddClaim(new Claim("urn:facebook:middle_name", middleName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var name = FacebookHelper.GetName(payload); - if (!string.IsNullOrEmpty(name)) - { - identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var timeZone = FacebookHelper.GetTimeZone(payload); - if (!string.IsNullOrEmpty(timeZone)) - { - identity.AddClaim(new Claim("urn:facebook:timezone", timeZone, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + context.RunClaimActions(); await Options.Events.CreatingTicket(context); diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHelper.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHelper.cs deleted file mode 100644 index 48e359099..000000000 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookHelper.cs +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNetCore.Authentication.Facebook -{ - /// - /// Contains static methods that allow to extract user's information from a - /// instance retrieved from Facebook after a successful authentication process. - /// - public static class FacebookHelper - { - /// - /// Gets the Facebook user ID. - /// - public static string GetId(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("id"); - } - - /// - /// Gets the user's min age. - /// - public static string GetAgeRangeMin(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return TryGetValue(user, "age_range", "min"); - } - - /// - /// Gets the user's max age. - /// - public static string GetAgeRangeMax(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return TryGetValue(user, "age_range", "max"); - } - - /// - /// Gets the user's birthday. - /// - public static string GetBirthday(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("birthday"); - } - - /// - /// Gets the Facebook email. - /// - public static string GetEmail(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("email"); - } - - /// - /// Gets the user's first name. - /// - public static string GetFirstName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("first_name"); - } - - /// - /// Gets the user's gender. - /// - public static string GetGender(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("gender"); - } - - /// - /// Gets the user's family name. - /// - public static string GetLastName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("last_name"); - } - - /// - /// Gets the user's link. - /// - public static string GetLink(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("link"); - } - - /// - /// Gets the user's location. - /// - public static string GetLocation(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return TryGetValue(user, "location", "name"); - } - - /// - /// Gets the user's locale. - /// - public static string GetLocale(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("locale"); - } - - /// - /// Gets the user's middle name. - /// - public static string GetMiddleName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("middle_name"); - } - - /// - /// Gets the user's name. - /// - public static string GetName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("name"); - } - - /// - /// Gets the user's timezone. - /// - public static string GetTimeZone(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - return user.Value("timezone"); - } - - // Get the given subProperty from a property. - private static string TryGetValue(JObject user, string propertyName, string subProperty) - { - JToken value; - if (user.TryGetValue(propertyName, out value)) - { - var subObject = JObject.Parse(value.ToString()); - if (subObject != null && subObject.TryGetValue(subProperty, out value)) - { - return value.ToString(); - } - } - return null; - } - - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs index 8e86b37c1..ae875bfaf 100644 --- a/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Facebook/FacebookOptions.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Facebook; using Microsoft.AspNetCore.Http; @@ -30,6 +32,21 @@ public FacebookOptions() Fields.Add("email"); Fields.Add("first_name"); Fields.Add("last_name"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonSubKey("urn:facebook:age_range_min", "age_range", "min"); + ClaimActions.MapJsonSubKey("urn:facebook:age_range_max", "age_range", "max"); + ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday"); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name"); + ClaimActions.MapJsonKey("urn:facebook:middle_name", "middle_name"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name"); + ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender"); + ClaimActions.MapJsonKey("urn:facebook:link", "link"); + ClaimActions.MapJsonSubKey("urn:facebook:location", "location", "name"); + ClaimActions.MapJsonKey(ClaimTypes.Locality, "locale"); + ClaimActions.MapJsonKey("urn:facebook:timezone", "timezone"); } // Facebook uses a non-standard term for this field. diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs index f28ab4d14..87506e080 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHandler.cs @@ -42,42 +42,7 @@ protected override async Task CreateTicketAsync( var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens, payload); - - var identifier = GoogleHelper.GetId(payload); - if (!string.IsNullOrEmpty(identifier)) - { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var givenName = GoogleHelper.GetGivenName(payload); - if (!string.IsNullOrEmpty(givenName)) - { - identity.AddClaim(new Claim(ClaimTypes.GivenName, givenName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var familyName = GoogleHelper.GetFamilyName(payload); - if (!string.IsNullOrEmpty(familyName)) - { - identity.AddClaim(new Claim(ClaimTypes.Surname, familyName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var name = GoogleHelper.GetName(payload); - if (!string.IsNullOrEmpty(name)) - { - identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var email = GoogleHelper.GetEmail(payload); - if (!string.IsNullOrEmpty(email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var profile = GoogleHelper.GetProfile(payload); - if (!string.IsNullOrEmpty(profile)) - { - identity.AddClaim(new Claim("urn:google:profile", profile, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + context.RunClaimActions(); await Options.Events.CreatingTicket(context); diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs index 336536c51..2cac949a0 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleHelper.cs @@ -12,71 +12,6 @@ namespace Microsoft.AspNetCore.Authentication.Google /// public static class GoogleHelper { - /// - /// Gets the Google user ID. - /// - public static string GetId(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("id"); - } - - /// - /// Gets the user's name. - /// - public static string GetName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("displayName"); - } - - /// - /// Gets the user's given name. - /// - public static string GetGivenName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return TryGetValue(user, "name", "givenName"); - } - - /// - /// Gets the user's family name. - /// - public static string GetFamilyName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return TryGetValue(user, "name", "familyName"); - } - - /// - /// Gets the user's profile link. - /// - public static string GetProfile(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("url"); - } - /// /// Gets the user's email. /// @@ -90,21 +25,6 @@ public static string GetEmail(JObject user) return TryGetFirstValue(user, "emails", "value"); } - // Get the given subProperty from a property. - private static string TryGetValue(JObject user, string propertyName, string subProperty) - { - JToken value; - if (user.TryGetValue(propertyName, out value)) - { - var subObject = JObject.Parse(value.ToString()); - if (subObject != null && subObject.TryGetValue(subProperty, out value)) - { - return value.ToString(); - } - } - return null; - } - // Get the given subProperty from a list property. private static string TryGetFirstValue(JObject user, string propertyName, string subProperty) { diff --git a/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs b/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs index 3d93b96ea..d26977970 100644 --- a/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Google/GoogleOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Http; @@ -25,6 +27,13 @@ public GoogleOptions() Scope.Add("openid"); Scope.Add("profile"); Scope.Add("email"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName"); + ClaimActions.MapJsonSubKey(ClaimTypes.GivenName, "name", "givenName"); + ClaimActions.MapJsonSubKey(ClaimTypes.Surname, "name", "familyName"); + ClaimActions.MapJsonKey("urn:google:profile", "url"); + ClaimActions.MapCustomJson(ClaimTypes.Email, GoogleHelper.GetEmail); } /// diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs index 21f567d15..2426cafe0 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHandler.cs @@ -35,39 +35,7 @@ protected override async Task CreateTicketAsync(ClaimsIden var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), properties, Options.AuthenticationScheme); var context = new OAuthCreatingTicketContext(ticket, Context, Options, Backchannel, tokens, payload); - var identifier = MicrosoftAccountHelper.GetId(payload); - if (!string.IsNullOrEmpty(identifier)) - { - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:id", identifier, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var name = MicrosoftAccountHelper.GetDisplayName(payload); - if (!string.IsNullOrEmpty(name)) - { - identity.AddClaim(new Claim(ClaimTypes.Name, name, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:name", name, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var givenName = MicrosoftAccountHelper.GetGivenName(payload); - if (!string.IsNullOrEmpty(givenName)) - { - identity.AddClaim(new Claim(ClaimTypes.GivenName, givenName, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:givenname", givenName, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var surname = MicrosoftAccountHelper.GetSurname(payload); - if (!string.IsNullOrEmpty(surname)) - { - identity.AddClaim(new Claim(ClaimTypes.Surname, surname, ClaimValueTypes.String, Options.ClaimsIssuer)); - identity.AddClaim(new Claim("urn:microsoftaccount:surname", surname, ClaimValueTypes.String, Options.ClaimsIssuer)); - } - - var email = MicrosoftAccountHelper.GetEmail(payload); - if (!string.IsNullOrEmpty(email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.String, Options.ClaimsIssuer)); - } + context.RunClaimActions(); await Options.Events.CreatingTicket(context); return context.Ticket; diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHelper.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHelper.cs deleted file mode 100644 index cce8dcc73..000000000 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountHelper.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNetCore.Authentication.MicrosoftAccount -{ - /// - /// Contains static methods that allow to extract user's information from a - /// instance retrieved from Microsoft after a successful authentication process. - /// http://graph.microsoft.io/en-us/docs/api-reference/v1.0/resources/user - /// - public static class MicrosoftAccountHelper - { - /// - /// Gets the Microsoft Account user ID. - /// - public static string GetId(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("id"); - } - - /// - /// Gets the user's name. - /// - public static string GetDisplayName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("displayName"); - } - - /// - /// Gets the user's given name. - /// - public static string GetGivenName(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("givenName"); - } - - /// - /// Gets the user's surname. - /// - public static string GetSurname(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("surname"); - } - - /// - /// Gets the user's email address. - /// - public static string GetEmail(JObject user) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - return user.Value("mail") ?? user.Value("userPrincipalName"); - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs index 625d4baf9..1aa4009a5 100644 --- a/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.MicrosoftAccount/MicrosoftAccountOptions.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Http; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder { @@ -23,6 +25,12 @@ public MicrosoftAccountOptions() TokenEndpoint = MicrosoftAccountDefaults.TokenEndpoint; UserInformationEndpoint = MicrosoftAccountDefaults.UserInformationEndpoint; Scope.Add("https://graph.microsoft.com/user.read"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName"); + ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName"); + ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname"); + ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value("mail") ?? user.Value("userPrincipalName")); } } } diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs new file mode 100644 index 000000000..965ca5fdb --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimAction.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// Infrastructure for mapping user data from a json structure to claims on the ClaimsIdentity. + /// + public abstract class ClaimAction + { + /// + /// Create a new claim manipulation action. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + public ClaimAction(string claimType, string valueType) + { + ClaimType = claimType; + ValueType = valueType; + } + + /// + /// The value to use for Claim.Type when creating a Claim. + /// + public string ClaimType { get; } + + // The value to use for Claim.ValueType when creating a Claim. + public string ValueType { get; } + + /// + /// Exhamine the given userData json, determine if the requisite data is present, and optionally add it + /// as a new Claim on the ClaimsIdentity. + /// + /// The source data to exhamine. This value may be null. + /// The identity to add Claims to. + /// The value to use for Claim.Issuer when creating a Claim. + public abstract void Run(JObject userData, ClaimsIdentity identity, string issuer); + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs new file mode 100644 index 000000000..63da155d7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollection.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A collection of ClaimActions used when mapping user data to Claims. + /// + public class ClaimActionCollection : IEnumerable + { + private IList Actions { get; } = new List(); + + /// + /// Remove all claim actions. + /// + public void Clear() => Actions.Clear(); + + /// + /// Remove all claim actions for the given ClaimType. + /// + /// The ClaimType of maps to remove. + public void Remove(string claimType) + { + var itemsToRemove = Actions.Where(map => string.Equals(claimType, map.ClaimType, StringComparison.OrdinalIgnoreCase)).ToList(); + itemsToRemove.ForEach(map => Actions.Remove(map)); + } + + /// + /// Add a claim action to the collection. + /// + /// The claim action to add. + public void Add(ClaimAction action) + { + Actions.Add(action); + } + + public IEnumerator GetEnumerator() + { + return Actions.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Actions.GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs new file mode 100644 index 000000000..f3fee6a22 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/ClaimActionCollectionMapExtensions.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class ClaimActionCollectionMapExtensions + { + /// + /// Select a top level value from the json user data with the given key name and add it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey) + { + collection.MapJsonKey(claimType, jsonKey, ClaimValueTypes.String); + } + + /// + /// Select a top level value from the json user data with the given key name and add it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType) + { + collection.Add(new JsonKeyClaimAction(claimType, valueType, jsonKey)); + } + + /// + /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey) + { + collection.MapJsonSubKey(claimType, jsonKey, subKey, ClaimValueTypes.String); + } + + /// + /// Select a second level value from the json user data with the given top level key name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapJsonSubKey(this ClaimActionCollection collection, string claimType, string jsonKey, string subKey, string valueType) + { + collection.Add(new JsonSubKeyClaimAction(claimType, valueType, jsonKey, subKey)); + } + + /// + /// Run the given resolver to select a value from the json user data to add as a claim. + /// This no-ops if the returned value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public static void MapCustomJson(this ClaimActionCollection collection, string claimType, Func resolver) + { + collection.MapCustomJson(claimType, ClaimValueTypes.String, resolver); + } + + /// + /// Run the given resolver to select a value from the json user data to add as a claim. + /// This no-ops if the returned value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public static void MapCustomJson(this ClaimActionCollection collection, string claimType, string valueType, Func resolver) + { + collection.Add(new CustomJsonClaimAction(claimType, valueType, resolver)); + } + + /// + /// Delete all claims from the given ClaimsIdentity with the given ClaimType. + /// + /// + /// + public static void DeleteClaim(this ClaimActionCollection collection, string claimType) + { + collection.Add(new DeleteClaimAction(claimType)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs new file mode 100644 index 000000000..21a4f70e1 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/CustomJsonClaimAction.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects the value from the json user data by running the given Func resolver. + /// + public class CustomJsonClaimAction : ClaimAction + { + /// + /// Creates a new CustomJsonClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The Func that will be called to select value from the given json user data. + public CustomJsonClaimAction(string claimType, string valueType, Func resolver) + : base(claimType, valueType) + { + Resolver = resolver; + } + + /// + /// The Func that will be called to select value from the given json user data. + /// + public Func Resolver { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + if (userData == null) + { + return; + } + var value = Resolver(userData); + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs new file mode 100644 index 000000000..75167cabc --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/DeleteClaimAction.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that deletes all claims from the given ClaimsIdentity with the given ClaimType. + /// + public class DeleteClaimAction : ClaimAction + { + /// + /// Creates a new DeleteClaimAction. + /// + /// The ClaimType of Claims to delete. + public DeleteClaimAction(string claimType) + : base(claimType, ClaimValueTypes.String) + { + } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + foreach (var claim in identity.FindAll(ClaimType).ToList()) + { + identity.TryRemoveClaim(claim); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs new file mode 100644 index 000000000..e628904de --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonKeyClaimAction.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the key is not found or the value is empty. + /// + public class JsonKeyClaimAction : ClaimAction + { + /// + /// Creates a new JsonKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + public JsonKeyClaimAction(string claimType, string valueType, string jsonKey) + : base(claimType, valueType) + { + JsonKey = jsonKey; + } + + /// + /// The top level key to look for in the json user data. + /// + public string JsonKey { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = userData?.Value(JsonKey); + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs new file mode 100644 index 000000000..bc29672d0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Claims/JsonSubKeyClaimAction.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json.Linq; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Authentication.OAuth.Claims +{ + /// + /// A ClaimAction that selects a second level value from the json user data with the given top level key + /// name and second level sub key name and add it as a Claim. + /// This no-ops if the keys are not found or the value is empty. + /// + public class JsonSubKeyClaimAction : JsonKeyClaimAction + { + /// + /// Creates a new JsonSubKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + /// The second level key to look for in the json user data. + public JsonSubKeyClaimAction(string claimType, string valueType, string jsonKey, string subKey) + : base(claimType, valueType, jsonKey) + { + SubKey = subKey; + } + + /// + /// The second level key to look for in the json user data. + /// + public string SubKey { get; } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = GetValue(userData, JsonKey, SubKey); + if (!string.IsNullOrEmpty(value)) + { + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } + + // Get the given subProperty from a property. + private static string GetValue(JObject userData, string propertyName, string subProperty) + { + if (userData != null && userData.TryGetValue(propertyName, out var value)) + { + var subObject = JObject.Parse(value.ToString()); + if (subObject != null && subObject.TryGetValue(subProperty, out value)) + { + return value.ToString(); + } + } + return null; + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs index 15f9c91c7..b17d23c9b 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/Events/OAuthCreatingTicketContext.cs @@ -144,5 +144,23 @@ public TimeSpan? ExpiresIn /// This property returns null when is null. /// public ClaimsIdentity Identity => Ticket?.Principal.Identity as ClaimsIdentity; + + public void RunClaimActions() + { + RunClaimActions(User); + } + + public void RunClaimActions(JObject userData) + { + if (userData == null) + { + throw new ArgumentNullException(nameof(userData)); + } + + foreach (var action in Options.ClaimActions) + { + action.Run(userData, Identity, Options.ClaimsIssuer); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs index 1c5143842..a5c36c1c4 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthHandler.cs @@ -11,11 +11,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features.Authentication; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Linq; -using Microsoft.AspNetCore.WebUtilities; namespace Microsoft.AspNetCore.Authentication.OAuth { diff --git a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs index 9591d9c44..9bd08dfd8 100644 --- a/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OAuth/OAuthOptions.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using System.ComponentModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OAuth; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Http.Authentication; namespace Microsoft.AspNetCore.Builder @@ -55,6 +55,11 @@ public OAuthOptions() set { base.Events = value; } } + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + /// /// Gets the list of permissions to request. /// diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs new file mode 100644 index 000000000..4e349579f --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/ClaimActionCollectionUniqueExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims; + +namespace Microsoft.AspNetCore.Authentication +{ + public static class ClaimActionCollectionUniqueExtensions + { + /// + /// Selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey) + { + collection.MapUniqueJsonKey(claimType, jsonKey, ClaimValueTypes.String); + } + + /// + /// Selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + /// + /// The value to use for Claim.Type when creating a Claim. + /// The top level key to look for in the json user data. + /// The value to use for Claim.ValueType when creating a Claim. + public static void MapUniqueJsonKey(this ClaimActionCollection collection, string claimType, string jsonKey, string valueType) + { + collection.Add(new UniqueJsonKeyClaimAction(claimType, valueType, jsonKey)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs new file mode 100644 index 000000000..132885b3c --- /dev/null +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Claims/UniqueJsonKeyClaimAction.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.Authentication.OpenIdConnect.Claims +{ + /// + /// A ClaimAction that selects a top level value from the json user data with the given key name and adds it as a Claim. + /// This no-ops if the ClaimsIdentity already contains a Claim with the given ClaimType. + /// This no-ops if the key is not found or the value is empty. + /// + public class UniqueJsonKeyClaimAction : JsonKeyClaimAction + { + /// + /// Creates a new UniqueJsonKeyClaimAction. + /// + /// The value to use for Claim.Type when creating a Claim. + /// The value to use for Claim.ValueType when creating a Claim. + /// The top level key to look for in the json user data. + public UniqueJsonKeyClaimAction(string claimType, string valueType, string jsonKey) + : base(claimType, valueType, jsonKey) + { + } + + /// + public override void Run(JObject userData, ClaimsIdentity identity, string issuer) + { + var value = userData?.Value(JsonKey); + if (string.IsNullOrEmpty(value)) + { + // Not found + return; + } + + var claim = identity.FindFirst(c => string.Equals(c.Type, JsonKey, System.StringComparison.OrdinalIgnoreCase)); + if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal)) + { + // Duplicate + return; + } + + claim = identity.FindFirst(c => + { + // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set + return c.Properties.TryGetValue(JwtSecurityTokenHandler.ShortClaimTypeProperty, out var shortType) + && string.Equals(shortType, JsonKey, System.StringComparison.OrdinalIgnoreCase); + }); + if (claim != null && string.Equals(claim.Value, value, System.StringComparison.Ordinal)) + { + // Duplicate with an alternate name. + return; + } + + identity.AddClaim(new Claim(ClaimType, value, ValueType, issuer)); + } + } +} diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj index e5665f579..965a940e4 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj @@ -8,7 +8,7 @@ aspnetcore;authentication;security - + diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs index c9c513ea7..a7c20f62e 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectHandler.cs @@ -661,6 +661,14 @@ protected override async Task HandleRemoteAuthenticateAsync( { return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt, ticket); } + else + { + var identity = (ClaimsIdentity)ticket.Principal.Identity; + foreach (var action in Options.ClaimActions) + { + action.Run(null, identity, Options.ClaimsIssuer); + } + } return AuthenticateResult.Success(ticket); } @@ -727,7 +735,7 @@ protected virtual async Task RedeemAuthorizationCodeAsync( // Error handling: // 1. If the response body can't be parsed as json, throws. - // 2. If the response's status code is not in 2XX range, throw OpenIdConnectProtocolException. If the body is correct parsed, + // 2. If the response's status code is not in 2XX range, throw OpenIdConnectProtocolException. If the body is correct parsed, // pass the error information from body to the exception. OpenIdConnectMessage message; try @@ -809,29 +817,11 @@ protected virtual async Task GetUserInformationAsync(OpenIdC var identity = (ClaimsIdentity)ticket.Principal.Identity; - foreach (var claim in identity.Claims) + foreach (var action in Options.ClaimActions) { - // If this claimType is mapped by the JwtSeurityTokenHandler, then this property will be set - var shortClaimTypeName = claim.Properties.ContainsKey(JwtSecurityTokenHandler.ShortClaimTypeProperty) ? - claim.Properties[JwtSecurityTokenHandler.ShortClaimTypeProperty] : string.Empty; - - // checking if claim in the identity (generated from id_token) has the same type as a claim retrieved from userinfo endpoint - JToken value; - var isClaimIncluded = user.TryGetValue(claim.Type, out value) || user.TryGetValue(shortClaimTypeName, out value); - - // if a same claim exists (matching both type and value) both in id_token identity and userinfo response, remove the json entry from the userinfo response - if (isClaimIncluded && claim.Value.Equals(value.ToString(), StringComparison.Ordinal)) - { - if (!user.Remove(claim.Type)) - { - user.Remove(shortClaimTypeName); - } - } + action.Run(user, identity, Options.ClaimsIssuer); } - // adding remaining unique claims from userinfo endpoint to the identity - ClaimsHelper.AddClaimsToIdentity(user, identity, jwt.Issuer); - return AuthenticateResult.Success(ticket); } @@ -908,7 +898,7 @@ private void WriteNonceCookie(string nonce) /// /// the nonce that we are looking for. /// echos 'nonce' if a cookie is found that matches, null otherwise. - /// Examine of that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. + /// Examine of that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'. /// of is used to obtain the actual 'nonce'. If the nonce is found, then of is called. private string ReadNonceCookie(string nonce) { diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs index f0b26f75b..a46c2956c 100644 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Authentication; @@ -55,6 +57,30 @@ public OpenIdConnectOptions(string authenticationScheme) Events = new OpenIdConnectEvents(); Scope.Add("openid"); Scope.Add("profile"); + + ClaimActions.DeleteClaim("nonce"); + ClaimActions.DeleteClaim("aud"); + ClaimActions.DeleteClaim("azp"); + ClaimActions.DeleteClaim("acr"); + ClaimActions.DeleteClaim("amr"); + ClaimActions.DeleteClaim("iss"); + ClaimActions.DeleteClaim("iat"); + ClaimActions.DeleteClaim("nbf"); + ClaimActions.DeleteClaim("exp"); + ClaimActions.DeleteClaim("at_hash"); + ClaimActions.DeleteClaim("c_hash"); + ClaimActions.DeleteClaim("auth_time"); + ClaimActions.DeleteClaim("ipaddr"); + ClaimActions.DeleteClaim("platf"); + ClaimActions.DeleteClaim("ver"); + + // http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + ClaimActions.MapUniqueJsonKey("sub", "sub"); + ClaimActions.MapUniqueJsonKey("name", "name"); + ClaimActions.MapUniqueJsonKey("given_name", "given_name"); + ClaimActions.MapUniqueJsonKey("family_name", "family_name"); + ClaimActions.MapUniqueJsonKey("profile", "profile"); + ClaimActions.MapUniqueJsonKey("email", "email"); } /// @@ -90,6 +116,11 @@ public OpenIdConnectOptions(string authenticationScheme) /// public bool GetClaimsFromUserInfoEndpoint { get; set; } + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + /// /// Gets or sets if HTTPS is required for the metadata address or authority. /// The default is true. This should be disabled only in development environments. @@ -112,7 +143,7 @@ public OpenIdConnectOptions(string authenticationScheme) /// /// Gets or sets the that is used to ensure that the 'id_token' received - /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + /// is valid per: http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation /// /// if 'value' is null. public OpenIdConnectProtocolValidator ProtocolValidator { get; set; } = new OpenIdConnectProtocolValidator() diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Utility/ClaimsHelper.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Utility/ClaimsHelper.cs deleted file mode 100644 index 78eea68bf..000000000 --- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/Utility/ClaimsHelper.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using Newtonsoft.Json.Linq; - -namespace Microsoft.AspNetCore.Authentication.OpenIdConnect -{ - internal static class ClaimsHelper - { - public static void AddClaimsToIdentity( - JObject userInformationPayload, - ClaimsIdentity identity, - string issuer) - { - foreach (var pair in userInformationPayload) - { - var array = pair.Value as JArray; - if (array != null) - { - foreach (var item in array) - { - AddClaimsToIdentity(item, identity, pair.Key, issuer); - } - } - else - { - AddClaimsToIdentity(pair.Value, identity, pair.Key, issuer); - } - } - } - - private static void AddClaimsToIdentity(JToken item, ClaimsIdentity identity, string key, string issuer) - => identity.AddClaim(new Claim(key, item?.ToString() ?? string.Empty, GetClaimValueType(item), issuer)); - - private static string GetClaimValueType(JToken token) - { - if (token == null) - { - return JsonClaimValueTypes.JsonNull; - } - - switch (token.Type) - { - case JTokenType.Array: - return JsonClaimValueTypes.JsonArray; - - case JTokenType.Boolean: - return ClaimValueTypes.Boolean; - - case JTokenType.Date: - return ClaimValueTypes.DateTime; - - case JTokenType.Float: - return ClaimValueTypes.Double; - - case JTokenType.Integer: - { - var value = (long) token; - if (value >= int.MinValue && value <= int.MaxValue) - { - return ClaimValueTypes.Integer; - } - - return ClaimValueTypes.Integer64; - } - - case JTokenType.Object: - return JsonClaimValueTypes.Json; - - case JTokenType.String: - return ClaimValueTypes.String; - } - - // Fall back to ClaimValueTypes.String when no appropriate - // claim value type can be inferred from the claim value. - return ClaimValueTypes.String; - } - } -} diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs index 435196a1e..21c6189d7 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/Events/TwitterCreatingTicketContext.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Security.Claims; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj b/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj index 78557532c..3694159f6 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/Microsoft.AspNetCore.Authentication.Twitter.csproj @@ -11,9 +11,8 @@ - + - diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs index 20d06beaf..8481730a8 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterHandler.cs @@ -108,6 +108,11 @@ protected override async Task HandleRemoteAuthenticateAsync( protected virtual async Task CreateTicketAsync( ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token, JObject user) { + foreach (var action in Options.ClaimActions) + { + action.Run(user, identity, Options.ClaimsIssuer); + } + var context = new TwitterCreatingTicketContext(Context, Options, token.UserId, token.ScreenName, token.Token, token.TokenSecret, user) { Principal = new ClaimsPrincipal(identity), @@ -355,12 +360,6 @@ private async Task RetrieveUserDetailsAsync(AccessToken accessToken, Cl var result = JObject.Parse(responseText); - var email = result.Value("email"); - if (!string.IsNullOrEmpty(email)) - { - identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.Email, Options.ClaimsIssuer)); - } - return result; } diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs index bf54b7fbb..836dd3c0d 100644 --- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs +++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs @@ -2,8 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.ComponentModel; +using System.Security.Claims; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; using Microsoft.AspNetCore.Authentication.Twitter; using Microsoft.AspNetCore.Http; @@ -24,6 +25,8 @@ public TwitterOptions() CallbackPath = new PathString("/signin-twitter"); BackchannelTimeout = TimeSpan.FromSeconds(60); Events = new TwitterEvents(); + + ClaimActions.MapJsonKey(ClaimTypes.Email, "email", ClaimValueTypes.Email); } /// @@ -46,6 +49,11 @@ public TwitterOptions() /// public bool RetrieveUserDetails { get; set; } + /// + /// A collection of claim actions used to select values from the json user data and create Claims. + /// + public ClaimActionCollection ClaimActions { get; } = new ClaimActionCollection(); + /// /// Gets or sets the type used to secure data handled by the middleware. /// diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs index 944c322ad..2a47f1e89 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/Google/GoogleMiddlewareTests.cs @@ -653,11 +653,11 @@ public async Task ValidateAuthenticatedContext() Assert.Equal(context.AccessToken, "Test Access Token"); Assert.Equal(context.RefreshToken, "Test Refresh Token"); Assert.Equal(context.ExpiresIn, TimeSpan.FromSeconds(3600)); - Assert.Equal(GoogleHelper.GetEmail(context.User), "Test email"); - Assert.Equal(GoogleHelper.GetId(context.User), "Test User ID"); - Assert.Equal(GoogleHelper.GetName(context.User), "Test Name"); - Assert.Equal(GoogleHelper.GetFamilyName(context.User), "Test Family Name"); - Assert.Equal(GoogleHelper.GetGivenName(context.User), "Test Given Name"); + Assert.Equal(context.Identity.FindFirst(ClaimTypes.Email)?.Value, "Test email"); + Assert.Equal(context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value, "Test User ID"); + Assert.Equal(context.Identity.FindFirst(ClaimTypes.Name)?.Value, "Test Name"); + Assert.Equal(context.Identity.FindFirst(ClaimTypes.Surname)?.Value, "Test Family Name"); + Assert.Equal(context.Identity.FindFirst(ClaimTypes.GivenName)?.Value, "Test Given Name"); return Task.FromResult(0); } }, diff --git a/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj b/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj index 446469ba4..3ae22642f 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj +++ b/test/Microsoft.AspNetCore.Authentication.Test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -22,4 +22,8 @@ + + + + diff --git a/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj b/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj index 5eaa318a3..7b6b30b8c 100644 --- a/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj +++ b/test/Microsoft.AspNetCore.Authorization.Test/Microsoft.AspNetCore.Authorization.Test.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj b/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj index 0cf30727b..9c49dcd29 100644 --- a/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj +++ b/test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test/Microsoft.AspNetCore.ChunkingCookieManager.Sources.Test.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj b/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj index aa00d90e0..ced555fe9 100644 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/Microsoft.AspNetCore.CookiePolicy.Test.csproj @@ -17,4 +17,8 @@ + + + +