Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a3e4fe5
Add shopify provider (#326)
sbiaudet Jul 21, 2019
6807cf1
Shopify - Fix TokenEndpoint format (#330)
sbiaudet Jul 22, 2019
86f0878
Gitlab (#329)
twsl Jul 23, 2019
81d383e
incorrect LoginPath
mgroves Sep 3, 2019
a652881
Basic Apple provider
martincostello Jun 6, 2019
c78b5e5
Implement Apple provider
martincostello Jun 8, 2019
b4b76d0
Enable Sign In with Apple
martincostello Jun 8, 2019
62d4e79
Update tests
martincostello Jun 8, 2019
06dfc7b
Enable token lifetime validation
martincostello Jun 8, 2019
4a9f230
Add null annotations
martincostello Jun 9, 2019
2c6a89c
Improve exception handling
martincostello Jun 9, 2019
cf3705a
Extend integration tests
martincostello Jun 9, 2019
c65e54f
Move expiry period to options
martincostello Jun 9, 2019
58e642c
Add ClientSecretExpiresAfter validation
martincostello Jun 9, 2019
93965e3
Add tests for options validation
martincostello Jun 9, 2019
fc95923
Add unit tests for client secret
martincostello Jun 9, 2019
38e71ab
Make KeyId required
martincostello Jun 9, 2019
0a55575
Fix test
martincostello Jun 9, 2019
991356d
Fix Linux and macOS secret generation
martincostello Jun 9, 2019
b660372
Add password option for pfx files
martincostello Jun 9, 2019
938e9ad
Fix flaky test
martincostello Jun 9, 2019
56abbbf
Add UsePrivateKey() method
martincostello Jun 9, 2019
d2e2f24
Bump System.IdentityModel.Tokens.Jwt
martincostello Jun 9, 2019
0d2f184
Set response_mode to form_post
martincostello Sep 8, 2019
2dcfb65
Use latest C# version
martincostello Sep 8, 2019
d083871
Retrieve user details after sign-in
martincostello Sep 8, 2019
88c8faa
Update branding
martincostello Sep 15, 2019
221bfeb
Access events via options
martincostello Sep 15, 2019
8ee2f76
Resolve logging TODO
martincostello Sep 15, 2019
15078f4
Comment out Apple option
martincostello Sep 20, 2019
bb83201
Merge branch 'dev' into Sign-In-With-Apple-300
martincostello Sep 20, 2019
99e3185
Update Sign in with Apple provider for ASP.NET Core 3.0
martincostello Sep 20, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Retrieve user details after sign-in
Get the user's name and email address, if available, as claims after signing in with an Apple ID. These details are only available the first time the user signs in; if they are not persisted they cannot currently be obtained again.
  • Loading branch information
martincostello committed Sep 20, 2019
commit d083871be5d1d18f52b88d5b2c4ec9875d24fa46
81 changes: 67 additions & 14 deletions src/AspNet.Security.OAuth.Apple/AppleAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;

namespace AspNet.Security.OAuth.Apple
{
Expand Down Expand Up @@ -101,7 +102,12 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(
await Events.ValidateIdToken(validateIdContext);
}

identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, GetNameIdentifier(idToken)));
var tokenClaims = ExtractClaimsFromToken(idToken);

foreach (var claim in tokenClaims)
{
identity.AddClaim(claim);
}

var principal = new ClaimsPrincipal(identity);

Expand All @@ -124,6 +130,55 @@ protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code,
return await base.ExchangeCodeAsync(code, redirectUri);
}

/// <summary>
/// Extracts the claims from the token received from the token endpoint.
/// </summary>
/// <param name="token">The token to extract the claims from.</param>
/// <returns>
/// An <see cref="IEnumerable{Claim}"/> containing the claims extracted from the token.
/// </returns>
protected virtual IEnumerable<Claim> ExtractClaimsFromToken([NotNull] string token)
{
try
{
var securityToken = _tokenHandler.ReadJwtToken(token);

return new List<Claim>(securityToken.Claims)
{
new Claim(ClaimTypes.NameIdentifier, securityToken.Subject, ClaimValueTypes.String, ClaimsIssuer),
};
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to parse JWT for claims from Apple ID token.", ex);
}
}

/// <summary>
/// Extracts the claims from the user received from the authorization endpoint.
/// </summary>
/// <param name="user">The user object to extract the claims from.</param>
/// <returns>
/// An <see cref="IEnumerable{Claim}"/> containing the claims extracted from the user information.
/// </returns>
protected virtual IEnumerable<Claim> ExtractClaimsFromUser([NotNull] JObject user)
{
var claims = new List<Claim>();

if (user.TryGetValue("name", out var name))
{
claims.Add(new Claim(ClaimTypes.GivenName, name.Value<string>("firstName"), ClaimValueTypes.String, ClaimsIssuer));
claims.Add(new Claim(ClaimTypes.Surname, name.Value<string>("lastName"), ClaimValueTypes.String, ClaimsIssuer));
}

if (user.TryGetValue("email", out var email))
{
claims.Add(new Claim(ClaimTypes.Email, email.Value<string>(), ClaimValueTypes.String, ClaimsIssuer));
}

return claims;
}

/// <inheritdoc />
protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
{
Expand All @@ -149,19 +204,6 @@ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync
return await HandleRemoteAuthenticateAsync(parameters);
}

private string GetNameIdentifier(string token)
{
try
{
var userToken = _tokenHandler.ReadJwtToken(token);
return userToken.Subject;
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to parse JWT from Apple ID token.", ex);
}
}

private async Task<HandleRequestResult> HandleRemoteAuthenticateAsync(
[NotNull] Dictionary<string, StringValues> parameters)
{
Expand Down Expand Up @@ -276,6 +318,17 @@ private async Task<HandleRequestResult> HandleRemoteAuthenticateAsync(
properties.StoreTokens(authTokens);
}

if (parameters.TryGetValue("user", out var userJson))
{
var user = JObject.Parse(userJson);
var userClaims = ExtractClaimsFromUser(user);

foreach (var claim in userClaims)
{
identity.AddClaim(claim);
}
}

var ticket = await CreateTicketAsync(identity, properties, tokens);

if (ticket != null)
Expand Down
18 changes: 18 additions & 0 deletions test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
*/

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
Expand All @@ -27,6 +29,13 @@ public AppleTests(ITestOutputHelper outputHelper)

public override string DefaultScheme => AppleAuthenticationDefaults.AuthenticationScheme;

protected override HttpMethod RedirectMethod => HttpMethod.Post;

protected override IDictionary<string, string> RedirectParameters => new Dictionary<string, string>()
{
["user"] = @"{""name"":{""firstName"":""Johnny"",""lastName"":""Appleseed""},""email"":""[email protected]""}",
};

protected internal override void RegisterAuthentication(AuthenticationBuilder builder)
{
IdentityModelEventSource.ShowPII = true;
Expand All @@ -39,7 +48,10 @@ protected internal override void RegisterAuthentication(AuthenticationBuilder bu
}

[Theory]
[InlineData(ClaimTypes.Email, "[email protected]")]
[InlineData(ClaimTypes.GivenName, "Johnny")]
[InlineData(ClaimTypes.NameIdentifier, "001883.fcc77ba97500402389df96821ad9c790.1517")]
[InlineData(ClaimTypes.Surname, "Appleseed")]
public async Task Can_Sign_In_Using_Apple_With_Client_Secret(string claimType, string claimValue)
{
// Arrange
Expand All @@ -64,7 +76,10 @@ void ConfigureServices(IServiceCollection services)
}

[Theory]
[InlineData(ClaimTypes.Email, "[email protected]")]
[InlineData(ClaimTypes.GivenName, "Johnny")]
[InlineData(ClaimTypes.NameIdentifier, "001883.fcc77ba97500402389df96821ad9c790.1517")]
[InlineData(ClaimTypes.Surname, "Appleseed")]
public async Task Can_Sign_In_Using_Apple_With_Private_Key(string claimType, string claimValue)
{
// Arrange
Expand Down Expand Up @@ -98,7 +113,10 @@ void ConfigureServices(IServiceCollection services)
}

[Theory]
[InlineData(ClaimTypes.Email, "[email protected]")]
[InlineData(ClaimTypes.GivenName, "Johnny")]
[InlineData(ClaimTypes.NameIdentifier, "001883.fcc77ba97500402389df96821ad9c790.1517")]
[InlineData(ClaimTypes.Surname, "Appleseed")]
public async Task Can_Sign_In_Using_Apple_With_No_Token_Validation(string claimType, string claimValue)
{
// Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -17,6 +18,10 @@ namespace AspNet.Security.OAuth.Infrastructure
/// </summary>
internal class LoopbackRedirectHandler : DelegatingHandler
{
public HttpMethod RedirectMethod { get; set; } = HttpMethod.Get;

public IDictionary<string, string> RedirectParameters { get; set; } = new Dictionary<string, string>();

public string RedirectUri { get; set; }

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Expand All @@ -28,8 +33,40 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
!string.Equals(result.Headers.Location?.Host, "localhost", StringComparison.OrdinalIgnoreCase))
{
var uri = BuildLoopbackUri(result);
HttpContent content = null;

if (RedirectMethod == HttpMethod.Post)
{
var queryString = HttpUtility.ParseQueryString(result.Headers.Location.Query);
string state = queryString["state"];

var parameters = new Dictionary<string, string>()
{
["code"] = "a6ed8e7f-471f-44f1-903b-65946475f351",
["state"] = state,
};

if (RedirectParameters?.Count > 0)
{
foreach (var parameter in RedirectParameters)
{
parameters[parameter.Key] = parameter.Value;
}
}

content = new FormUrlEncodedContent(parameters);
}
else
{
uri = BuildLoopbackUri(result);
}

var redirectRequest = new HttpRequestMessage(RedirectMethod, uri);

var redirectRequest = new HttpRequestMessage(request.Method, uri);
if (content != null)
{
redirectRequest.Content = content;
}

// Forward on the headers and cookies
foreach (var header in result.Headers)
Expand Down
19 changes: 17 additions & 2 deletions test/AspNet.Security.OAuth.Providers.Tests/OAuthTests`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ protected OAuthTests()
.ThrowsOnMissingRegistration()
.RegisterBundle(Path.Combine(GetType().Name.Replace("Tests", string.Empty), "bundle.json"));

LoopbackRedirectHandler = new LoopbackRedirectHandler { RedirectUri = RedirectUri };
LoopbackRedirectHandler = new LoopbackRedirectHandler
{
RedirectMethod = RedirectMethod,
RedirectParameters = RedirectParameters,
RedirectUri = RedirectUri,
};
}

/// <summary>
Expand All @@ -58,6 +63,16 @@ protected OAuthTests()
/// </summary>
public abstract string DefaultScheme { get; }

/// <summary>
/// Gets the optional redirect HTTP method to use for OAuth flows.
/// </summary>
protected virtual HttpMethod RedirectMethod => HttpMethod.Get;

/// <summary>
/// Gets the optional additional parameters for the redirect request with OAuth flows.
/// </summary>
protected virtual IDictionary<string, string> RedirectParameters => new Dictionary<string, string>();

/// <summary>
/// Gets the optional redirect URI to use for OAuth flows.
/// </summary>
Expand Down Expand Up @@ -112,7 +127,7 @@ protected HttpClient CreateBackchannel(AuthenticationBuilder builder)
public DelegatingHandler LoopbackRedirectHandler { get; set; }

/// <summary>
/// Run the ChannelAsync for authentication
/// Run the ChannelAsync for authentication
/// </summary>
/// <param name="context">The HTTP context</param>
protected internal virtual Task ChallengeAsync(HttpContext context) => context.ChallengeAsync();
Expand Down