Skip to content

Commit a0fe95e

Browse files
Extract email from ID token not user parameter
Use the verified ID token JWT as the source of the email claim rather than the user parameter in the callback.
1 parent f454732 commit a0fe95e

File tree

3 files changed

+108
-10
lines changed

3 files changed

+108
-10
lines changed

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,19 @@ protected virtual IEnumerable<Claim> ExtractClaimsFromToken([NotNull] string tok
133133
{
134134
var securityToken = Options.SecurityTokenHandler.ReadJsonWebToken(token);
135135

136-
return new List<Claim>(securityToken.Claims)
136+
var claims = new List<Claim>(securityToken.Claims)
137137
{
138138
new Claim(ClaimTypes.NameIdentifier, securityToken.Subject, ClaimValueTypes.String, ClaimsIssuer),
139139
};
140+
141+
var emailClaim = claims.Find((p) => string.Equals(p.Type, "email", StringComparison.Ordinal));
142+
143+
if (emailClaim is not null)
144+
{
145+
claims.Add(new Claim(ClaimTypes.Email, emailClaim.Value ?? string.Empty, ClaimValueTypes.String, ClaimsIssuer));
146+
}
147+
148+
return claims;
140149
}
141150
catch (Exception ex)
142151
{
@@ -161,11 +170,6 @@ protected virtual IEnumerable<Claim> ExtractClaimsFromUser([NotNull] JsonElement
161170
claims.Add(new Claim(ClaimTypes.Surname, name.GetString("lastName") ?? string.Empty, ClaimValueTypes.String, ClaimsIssuer));
162171
}
163172

164-
if (user.TryGetProperty("email", out var email))
165-
{
166-
claims.Add(new Claim(ClaimTypes.Email, email.GetString() ?? string.Empty, ClaimValueTypes.String, ClaimsIssuer));
167-
}
168-
169173
return claims;
170174
}
171175

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* for more information concerning the license and the contributors participating to this project.
55
*/
66

7+
using System.IdentityModel.Tokens.Jwt;
8+
using System.Security.Cryptography;
9+
using System.Text.Json;
710
using Microsoft.AspNetCore.WebUtilities;
811
using Microsoft.Extensions.DependencyInjection.Extensions;
912
using Microsoft.IdentityModel.Logging;
@@ -465,6 +468,91 @@ public async Task BuildChallengeUrl_Generates_Correct_Url(bool usePkce)
465468
}
466469
}
467470

471+
[Fact]
472+
public void Regenerate_Test_Jwts()
473+
{
474+
using var rsa = RSA.Create();
475+
var parameters = rsa.ExportParameters(true);
476+
477+
var webKey = new
478+
{
479+
kty = JsonWebAlgorithmsKeyTypes.RSA,
480+
kid = "AIDOPK1",
481+
use = "sig",
482+
alg = SecurityAlgorithms.RsaSha256,
483+
n = Base64UrlEncoder.Encode(parameters.Modulus),
484+
e = Base64UrlEncoder.Encode(parameters.Exponent),
485+
};
486+
487+
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256)
488+
{
489+
CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
490+
};
491+
492+
var audience = "com.martincostello.signinwithapple.test.client";
493+
var issuer = "https://appleid.apple.com";
494+
var expires = DateTimeOffset.FromUnixTimeSeconds(1587212159).UtcDateTime;
495+
496+
var iat = new Claim(JwtRegisteredClaimNames.Iat, "1587211559");
497+
var sub = new Claim(JwtRegisteredClaimNames.Sub, "001883.fcc77ba97500402389df96821ad9c790.1517");
498+
var atHash = new Claim(JwtRegisteredClaimNames.AtHash, "eOy0y7XVexdkzc7uuDZiCQ");
499+
var emailVerified = new Claim("email_verified", "true");
500+
var authTime = new Claim(JwtRegisteredClaimNames.AuthTime, "1587211556");
501+
var nonceSupported = new Claim("nonce_supported", "true");
502+
503+
var claimsForPublicEmail = new Claim[]
504+
{
505+
iat,
506+
sub,
507+
atHash,
508+
new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
509+
emailVerified,
510+
authTime,
511+
nonceSupported,
512+
};
513+
514+
var publicEmailToken = new JwtSecurityToken(
515+
issuer,
516+
audience,
517+
claimsForPublicEmail,
518+
expires: expires,
519+
signingCredentials: signingCredentials);
520+
521+
var claimsForPrivateEmail = new Claim[]
522+
{
523+
iat,
524+
sub,
525+
atHash,
526+
new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
527+
emailVerified,
528+
authTime,
529+
nonceSupported,
530+
new Claim("is_private_email", "true"),
531+
};
532+
533+
var privateEmailToken = new JwtSecurityToken(
534+
issuer,
535+
audience,
536+
claimsForPrivateEmail,
537+
expires: expires,
538+
signingCredentials: signingCredentials);
539+
540+
var publicEmailIdToken = new JwtSecurityTokenHandler().WriteToken(publicEmailToken);
541+
var privateEmailIdToken = new JwtSecurityTokenHandler().WriteToken(privateEmailToken);
542+
var serializedRsaPublicKey = JsonSerializer.Serialize(webKey, new JsonSerializerOptions() { WriteIndented = true });
543+
544+
// Copy the values from the test output to bundles.json if you need to regenerate the JWTs to edit the claims
545+
546+
// For https://appleid.apple.com/auth/keys
547+
OutputHelper!.WriteLine($"RSA key: {serializedRsaPublicKey}");
548+
549+
// For https://appleid.apple.com/auth/token
550+
OutputHelper!.WriteLine($"Public email JWT: {publicEmailIdToken}");
551+
552+
// For https://appleid.apple.local/auth/token/email
553+
OutputHelper!.WriteLine($"Private email JWT: {privateEmailIdToken}");
554+
}
555+
468556
private sealed class CustomAppleAuthenticationEvents : AppleAuthenticationEvents
469557
{
470558
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"issuer": "https://appleid.apple.com",
1111
"authorization_endpoint": "https://appleid.apple.com/auth/authorize",
1212
"token_endpoint": "https://appleid.apple.com/auth/token",
13+
"revocation_endpoint": "https://appleid.apple.com/auth/revoke",
1314
"jwks_uri": "https://appleid.apple.com/auth/keys",
1415
"response_types_supported": [
1516
"code"
@@ -39,8 +40,13 @@
3940
"email_verified",
4041
"exp",
4142
"iat",
43+
"is_private_email",
4244
"iss",
43-
"sub"
45+
"nonce",
46+
"nonce_supported",
47+
"real_user_status",
48+
"sub",
49+
"transfer_sub"
4450
]
4551
}
4652
},
@@ -56,7 +62,7 @@
5662
"kid": "AIDOPK1",
5763
"use": "sig",
5864
"alg": "RS256",
59-
"n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
65+
"n": "1VIMsu0l2vntPVynIAkok5NGPQtM2Rkrs6PZGKHrfoBoHBBAk3oIGybfshc1YBZwcKYAMSh0tMt0YC8o6FMIrY4VmABgaiInU_IZWwJVnW4uQScPixLfygQ4MGbocICKc-YbcLepReCbmBe1QImOClbG_aPNR-EttysW9gJyc1aZPmDm9nsfrWSPBN75ZjM1u01b_FcwsnwdrGplDsSUU9ULQ7ySw4s3whCGGKPE3vN1ZVkZLN-Avm69CzFvrdXrNp4qnltJ3SUYM73RGEhuNa6J2KqPDzc-VW5V0zeGv2j2PjadJ1r-69d6QIM6Oa2vNSHJxzrqwhLAEgZ_SGngyQ",
6066
"e": "AQAB"
6167
}
6268
]
@@ -70,7 +76,7 @@
7076
"contentJson": {
7177
"access_token": "secret-access-token",
7278
"expires_in": "300",
73-
"id_token": "eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCIsImV4cCI6MTU2MDAwODkxMCwiaWF0IjoxNTYwMDA4MzEwLCJzdWIiOiIwMDE4ODMuZmNjNzdiYTk3NTAwNDAyMzg5ZGY5NjgyMWFkOWM3OTAuMTUxNyIsImF0X2hhc2giOiJjN0xnNk9mSk1WQVUyUHRJVGRaeW93In0.hwLfuE0dB3mNYnDFWCd08MyJThsiRbGQmF-KX6VpGQttXRzChNgy9QWTT3vfd4bftMvlWCUlUEwCG0Os7hQUbWPknKYYIdxZGAejtCSCWYQ4PMhS_eQ5goICdLdi3ITzOG2JUmU-Vry4bPn3dJiyZ8ODGpj7MIBsVaRlfL4AlAgOKi9rp5UjVqj05M4qm512G-u-tVX7nasx3Eg-pFvS-w0CQJtVp3xIR2Ez3DRRt2roL0S6f0jNA-zb-zhOt_sFwmeqElGnQAidakUvrPTN0tORMUk_rKuohtkcY1_6uaVIsQ8NnOMl5Xszg9NzkQh5Je2Gi-qRzMxskJ0fJDCAfA",
79+
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTg3MjExNTU5Iiwic3ViIjoiMDAxODgzLmZjYzc3YmE5NzUwMDQwMjM4OWRmOTY4MjFhZDljNzkwLjE1MTciLCJhdF9oYXNoIjoiZU95MHk3WFZleGRremM3dXVEWmlDUSIsImVtYWlsIjoiam9obm55LmFwcGxlc2VlZEBhcHBsZS5sb2NhbCIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6IjE1ODcyMTE1NTYiLCJub25jZV9zdXBwb3J0ZWQiOiJ0cnVlIiwiZXhwIjoxNTg3MjEyMTU5LCJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCJ9.zu386hf3Y_3EG_OZsf-jpPKurH5HFmJ0Aal4Gnc_G-VpVoa8SvhNR_7UTbZtmQs8jOvjldPZzzXHJLWDBL_6yKIhnOntxd3G4QwIfM6PzkhiFiZXd1xHbDdx1aJ1EPnZWHPfRPtaQibda5BhenBRwAK3CPhvr7DLio54xtw-FDZgyakOHbb_2QYz0N0FBlyM5vzQEVObOKm9V2qx6hk5t7aeobOf8jOKJcx8WXWCpGQX6LOTpNnfD7Jw4Xlnb0IK6BC-agyFy_KZ5ujmB10wFnmIz9-QtvwTY4tTYpY7RigMHGIbmLS6egJTI0UhsvEHuXxaEXJ-52YGo_IIJCV6DQ",
7480
"refresh_token": "secret-refresh-token",
7581
"token_type": "bearer"
7682
}
@@ -83,7 +89,7 @@
8389
"contentJson": {
8490
"access_token": "secret-access-token",
8591
"expires_in": "300",
86-
"id_token": "eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCIsImV4cCI6MTU4NzIxMjE1OSwiaWF0IjoxNTg3MjExNTU5LCJzdWIiOiIwMDE4ODMuZmNjNzdiYTk3NTAwNDAyMzg5ZGY5NjgyMWFkOWM3OTAuMTUxNyIsImF0X2hhc2giOiJlT3kweTdYVmV4ZGt6Yzd1dURaaUNRIiwiZW1haWwiOiJ1c3Nja2VmdXo2QHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTg3MjExNTU2LCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.ZPUgcJlCneXLNZiFDraKpWVtFPSyoxkWgrMlTZ8tM3IBBXOmQFbb75OBQC-JbZHciry96y-sy33O_fF8gaudmInH1EorDIsfryafNd0POD-8pJWY9PiGrGx50c_1DLIIIsYEm0p-JEIfQpzJ-lIWpz9ujv4ChmZx-t3PzPzzZOVlC0q1pATqJaxhY_ntL_u98BZnfAKxzqEhb5q-1TmhtHFaEtAtsd2gGm6PTaM5N-2HXQ8Bh_BlJMH3u_KakFNJRhaezlVIlLtmgxM4VjrxUeIqba-fwBlfGXPonA_xZIHg71ZujJSlYJp3yWW3Kjsb4rUUUff7yEQF5A1LVnghwA",
92+
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTg3MjExNTU5Iiwic3ViIjoiMDAxODgzLmZjYzc3YmE5NzUwMDQwMjM4OWRmOTY4MjFhZDljNzkwLjE1MTciLCJhdF9oYXNoIjoiZU95MHk3WFZleGRremM3dXVEWmlDUSIsImVtYWlsIjoidXNzY2tlZnV6NkBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJhdXRoX3RpbWUiOiIxNTg3MjExNTU2Iiwibm9uY2Vfc3VwcG9ydGVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiZXhwIjoxNTg3MjEyMTU5LCJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCJ9.Xz-HeSAGEvPL0ObpZUYYexefSAPmRO9O_x2MTdbJKXuW65gluyJoRYfjzkKrnQUGEFvGUJ1qUiEIcdGs3kCo_TmSk6xH6e_loNYMI2J_7qb2i1-LOFHajNd1g1kTNGwSu2E22iE2IqecwfKpE7-a8thRFfbwuKyd6MNnm_NwMKBWr7IaekUc3Z876gtq94QlhItbBz8brQO6qTTekEigGEfa_h20WkPg3ZZVdqV8F-mJAQZXsGbVKToLi_L1AS6AiKxuHpTn04IGz1y6ezbng3STp-JzZslv85DJAJdZTieFh4s9RH0RFV_1GvfiExB8Q6COCaMFP7rnAVgc-27Uhg",
8793
"refresh_token": "secret-refresh-token",
8894
"token_type": "bearer"
8995
}

0 commit comments

Comments
 (0)