Skip to content

Commit c0b5ecc

Browse files
Ensure email claim is set (#411)
Set the ClaimTypes.Email claim from the email in the JWT if present and an email is not received as a parameter with the sign in.
1 parent c349496 commit c0b5ecc

File tree

4 files changed

+96
-1
lines changed

4 files changed

+96
-1
lines changed

src/AspNet.Security.OAuth.Apple/AppleAuthenticationOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ public AppleAuthenticationOptions()
3030

3131
Scope.Add("name");
3232
Scope.Add("email");
33+
34+
// Add a custom claim action that maps the email claim from the ID token if
35+
// it was not otherwise provided in the user endpoint response.
36+
// See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/issues/407
37+
ClaimActions.Add(new AppleEmailClaimAction(this));
3338
}
3439

3540
/// <summary>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
3+
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
4+
* for more information concerning the license and the contributors participating to this project.
5+
*/
6+
7+
using System;
8+
using System.Security.Claims;
9+
using System.Text.Json;
10+
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
11+
12+
namespace AspNet.Security.OAuth.Apple
13+
{
14+
internal sealed class AppleEmailClaimAction : ClaimAction
15+
{
16+
private readonly AppleAuthenticationOptions _options;
17+
18+
internal AppleEmailClaimAction(AppleAuthenticationOptions options)
19+
: base(ClaimTypes.Email, ClaimValueTypes.String)
20+
{
21+
_options = options;
22+
}
23+
24+
public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
25+
{
26+
if (!identity.HasClaim((p) => string.Equals(p.Type, ClaimType, StringComparison.OrdinalIgnoreCase)))
27+
{
28+
var emailClaim = identity.FindFirst("email");
29+
30+
if (!string.IsNullOrEmpty(emailClaim?.Value))
31+
{
32+
identity.AddClaim(new Claim(ClaimType, emailClaim.Value, ValueType, _options.ClaimsIssuer));
33+
}
34+
}
35+
}
36+
}
37+
}

test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public AppleTests(ITestOutputHelper outputHelper)
3333

3434
protected override HttpMethod RedirectMethod => HttpMethod.Post;
3535

36-
protected override IDictionary<string, string> RedirectParameters => new Dictionary<string, string>()
36+
protected override IDictionary<string, string> RedirectParameters { get; } = new Dictionary<string, string>()
3737
{
3838
["user"] = @"{""name"":{""firstName"":""Johnny"",""lastName"":""Appleseed""},""email"":""[email protected]""}",
3939
};
@@ -111,6 +111,46 @@ static void ConfigureServices(IServiceCollection services)
111111
AssertClaim(claims, claimType, claimValue);
112112
}
113113

114+
[Theory]
115+
[InlineData("at_hash", "eOy0y7XVexdkzc7uuDZiCQ")]
116+
[InlineData("aud", "com.martincostello.signinwithapple.test.client")]
117+
[InlineData("auth_time", "1587211556")]
118+
[InlineData("email", "[email protected]")]
119+
[InlineData("email_verified", "true")]
120+
[InlineData("exp", "1587212159")]
121+
[InlineData("iat", "1587211559")]
122+
[InlineData("iss", "https://appleid.apple.com")]
123+
[InlineData("is_private_email", "true")]
124+
[InlineData("nonce_supported", "true")]
125+
[InlineData("sub", "001883.fcc77ba97500402389df96821ad9c790.1517")]
126+
[InlineData(ClaimTypes.Email, "[email protected]")]
127+
[InlineData(ClaimTypes.NameIdentifier, "001883.fcc77ba97500402389df96821ad9c790.1517")]
128+
public async Task Can_Sign_In_Using_Apple_And_Receive_Claims_From_Id_Token(string claimType, string claimValue)
129+
{
130+
// Arrange
131+
static void ConfigureServices(IServiceCollection services)
132+
{
133+
services.AddSingleton<JwtSecurityTokenHandler, FrozenJwtSecurityTokenHandler>();
134+
services.PostConfigureAll<AppleAuthenticationOptions>((options) =>
135+
{
136+
options.ClientSecret = "my-client-secret";
137+
options.GenerateClientSecret = false;
138+
options.TokenEndpoint = "https://appleid.apple.local/auth/token/email";
139+
options.ValidateTokens = false;
140+
});
141+
}
142+
143+
RedirectParameters.Clear(); // Simulate second sign in where user data is not returned
144+
145+
using var server = CreateTestServer(ConfigureServices);
146+
147+
// Act
148+
var claims = await AuthenticateUserAsync(server);
149+
150+
// Assert
151+
AssertClaim(claims, claimType, claimValue);
152+
}
153+
114154
[Theory]
115155
[InlineData(ClaimTypes.Email, "[email protected]")]
116156
[InlineData(ClaimTypes.GivenName, "Johnny")]

test/AspNet.Security.OAuth.Providers.Tests/Apple/bundle.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@
3232
"token_type": "bearer"
3333
}
3434
},
35+
{
36+
"comment": "https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens",
37+
"uri": "https://appleid.apple.local/auth/token/email",
38+
"method": "POST",
39+
"contentFormat": "json",
40+
"contentJson": {
41+
"access_token": "secret-access-token",
42+
"expires_in": "300",
43+
"id_token": "eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCIsImV4cCI6MTU4NzIxMjE1OSwiaWF0IjoxNTg3MjExNTU5LCJzdWIiOiIwMDE4ODMuZmNjNzdiYTk3NTAwNDAyMzg5ZGY5NjgyMWFkOWM3OTAuMTUxNyIsImF0X2hhc2giOiJlT3kweTdYVmV4ZGt6Yzd1dURaaUNRIiwiZW1haWwiOiJ1c3Nja2VmdXo2QHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTg3MjExNTU2LCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.ZPUgcJlCneXLNZiFDraKpWVtFPSyoxkWgrMlTZ8tM3IBBXOmQFbb75OBQC-JbZHciry96y-sy33O_fF8gaudmInH1EorDIsfryafNd0POD-8pJWY9PiGrGx50c_1DLIIIsYEm0p-JEIfQpzJ-lIWpz9ujv4ChmZx-t3PzPzzZOVlC0q1pATqJaxhY_ntL_u98BZnfAKxzqEhb5q-1TmhtHFaEtAtsd2gGm6PTaM5N-2HXQ8Bh_BlJMH3u_KakFNJRhaezlVIlLtmgxM4VjrxUeIqba-fwBlfGXPonA_xZIHg71ZujJSlYJp3yWW3Kjsb4rUUUff7yEQF5A1LVnghwA",
44+
"refresh_token": "secret-refresh-token",
45+
"token_type": "bearer"
46+
}
47+
},
3548
{
3649
"uri": "https://appleid.apple.local/auth/keys/none",
3750
"method": "GET",

0 commit comments

Comments
 (0)