Skip to content

Commit

Permalink
Extract email from ID token not user parameter
Browse files Browse the repository at this point in the history
Use the verified ID token JWT as the source of the email claim rather than the user parameter in the callback.
  • Loading branch information
martincostello committed Aug 31, 2022
1 parent f454732 commit a0fe95e
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 10 deletions.
16 changes: 10 additions & 6 deletions src/AspNet.Security.OAuth.Apple/AppleAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,19 @@ protected virtual IEnumerable<Claim> ExtractClaimsFromToken([NotNull] string tok
{
var securityToken = Options.SecurityTokenHandler.ReadJsonWebToken(token);

return new List<Claim>(securityToken.Claims)
var claims = new List<Claim>(securityToken.Claims)
{
new Claim(ClaimTypes.NameIdentifier, securityToken.Subject, ClaimValueTypes.String, ClaimsIssuer),
};

var emailClaim = claims.Find((p) => string.Equals(p.Type, "email", StringComparison.Ordinal));

if (emailClaim is not null)
{
claims.Add(new Claim(ClaimTypes.Email, emailClaim.Value ?? string.Empty, ClaimValueTypes.String, ClaimsIssuer));
}

return claims;
}
catch (Exception ex)
{
Expand All @@ -161,11 +170,6 @@ protected virtual IEnumerable<Claim> ExtractClaimsFromUser([NotNull] JsonElement
claims.Add(new Claim(ClaimTypes.Surname, name.GetString("lastName") ?? string.Empty, ClaimValueTypes.String, ClaimsIssuer));
}

if (user.TryGetProperty("email", out var email))
{
claims.Add(new Claim(ClaimTypes.Email, email.GetString() ?? string.Empty, ClaimValueTypes.String, ClaimsIssuer));
}

return claims;
}

Expand Down
88 changes: 88 additions & 0 deletions test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* for more information concerning the license and the contributors participating to this project.
*/

using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.IdentityModel.Logging;
Expand Down Expand Up @@ -465,6 +468,91 @@ public async Task BuildChallengeUrl_Generates_Correct_Url(bool usePkce)
}
}

[Fact]
public void Regenerate_Test_Jwts()
{
using var rsa = RSA.Create();
var parameters = rsa.ExportParameters(true);

var webKey = new
{
kty = JsonWebAlgorithmsKeyTypes.RSA,
kid = "AIDOPK1",
use = "sig",
alg = SecurityAlgorithms.RsaSha256,
n = Base64UrlEncoder.Encode(parameters.Modulus),
e = Base64UrlEncoder.Encode(parameters.Exponent),
};

var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256)
{
CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
};

var audience = "com.martincostello.signinwithapple.test.client";
var issuer = "https://appleid.apple.com";
var expires = DateTimeOffset.FromUnixTimeSeconds(1587212159).UtcDateTime;

var iat = new Claim(JwtRegisteredClaimNames.Iat, "1587211559");
var sub = new Claim(JwtRegisteredClaimNames.Sub, "001883.fcc77ba97500402389df96821ad9c790.1517");
var atHash = new Claim(JwtRegisteredClaimNames.AtHash, "eOy0y7XVexdkzc7uuDZiCQ");
var emailVerified = new Claim("email_verified", "true");
var authTime = new Claim(JwtRegisteredClaimNames.AuthTime, "1587211556");
var nonceSupported = new Claim("nonce_supported", "true");

var claimsForPublicEmail = new Claim[]
{
iat,
sub,
atHash,
new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
emailVerified,
authTime,
nonceSupported,
};

var publicEmailToken = new JwtSecurityToken(
issuer,
audience,
claimsForPublicEmail,
expires: expires,
signingCredentials: signingCredentials);

var claimsForPrivateEmail = new Claim[]
{
iat,
sub,
atHash,
new Claim(JwtRegisteredClaimNames.Email, "[email protected]"),
emailVerified,
authTime,
nonceSupported,
new Claim("is_private_email", "true"),
};

var privateEmailToken = new JwtSecurityToken(
issuer,
audience,
claimsForPrivateEmail,
expires: expires,
signingCredentials: signingCredentials);

var publicEmailIdToken = new JwtSecurityTokenHandler().WriteToken(publicEmailToken);
var privateEmailIdToken = new JwtSecurityTokenHandler().WriteToken(privateEmailToken);
var serializedRsaPublicKey = JsonSerializer.Serialize(webKey, new JsonSerializerOptions() { WriteIndented = true });

// Copy the values from the test output to bundles.json if you need to regenerate the JWTs to edit the claims

// For https://appleid.apple.com/auth/keys
OutputHelper!.WriteLine($"RSA key: {serializedRsaPublicKey}");

// For https://appleid.apple.com/auth/token
OutputHelper!.WriteLine($"Public email JWT: {publicEmailIdToken}");

// For https://appleid.apple.local/auth/token/email
OutputHelper!.WriteLine($"Private email JWT: {privateEmailIdToken}");
}

private sealed class CustomAppleAuthenticationEvents : AppleAuthenticationEvents
{
}
Expand Down
14 changes: 10 additions & 4 deletions test/AspNet.Security.OAuth.Providers.Tests/Apple/bundle.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"issuer": "https://appleid.apple.com",
"authorization_endpoint": "https://appleid.apple.com/auth/authorize",
"token_endpoint": "https://appleid.apple.com/auth/token",
"revocation_endpoint": "https://appleid.apple.com/auth/revoke",
"jwks_uri": "https://appleid.apple.com/auth/keys",
"response_types_supported": [
"code"
Expand Down Expand Up @@ -39,8 +40,13 @@
"email_verified",
"exp",
"iat",
"is_private_email",
"iss",
"sub"
"nonce",
"nonce_supported",
"real_user_status",
"sub",
"transfer_sub"
]
}
},
Expand All @@ -56,7 +62,7 @@
"kid": "AIDOPK1",
"use": "sig",
"alg": "RS256",
"n": "lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w",
"n": "1VIMsu0l2vntPVynIAkok5NGPQtM2Rkrs6PZGKHrfoBoHBBAk3oIGybfshc1YBZwcKYAMSh0tMt0YC8o6FMIrY4VmABgaiInU_IZWwJVnW4uQScPixLfygQ4MGbocICKc-YbcLepReCbmBe1QImOClbG_aPNR-EttysW9gJyc1aZPmDm9nsfrWSPBN75ZjM1u01b_FcwsnwdrGplDsSUU9ULQ7ySw4s3whCGGKPE3vN1ZVkZLN-Avm69CzFvrdXrNp4qnltJ3SUYM73RGEhuNa6J2KqPDzc-VW5V0zeGv2j2PjadJ1r-69d6QIM6Oa2vNSHJxzrqwhLAEgZ_SGngyQ",
"e": "AQAB"
}
]
Expand All @@ -70,7 +76,7 @@
"contentJson": {
"access_token": "secret-access-token",
"expires_in": "300",
"id_token": "eyJraWQiOiJBSURPUEsxIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCIsImV4cCI6MTU2MDAwODkxMCwiaWF0IjoxNTYwMDA4MzEwLCJzdWIiOiIwMDE4ODMuZmNjNzdiYTk3NTAwNDAyMzg5ZGY5NjgyMWFkOWM3OTAuMTUxNyIsImF0X2hhc2giOiJjN0xnNk9mSk1WQVUyUHRJVGRaeW93In0.hwLfuE0dB3mNYnDFWCd08MyJThsiRbGQmF-KX6VpGQttXRzChNgy9QWTT3vfd4bftMvlWCUlUEwCG0Os7hQUbWPknKYYIdxZGAejtCSCWYQ4PMhS_eQ5goICdLdi3ITzOG2JUmU-Vry4bPn3dJiyZ8ODGpj7MIBsVaRlfL4AlAgOKi9rp5UjVqj05M4qm512G-u-tVX7nasx3Eg-pFvS-w0CQJtVp3xIR2Ez3DRRt2roL0S6f0jNA-zb-zhOt_sFwmeqElGnQAidakUvrPTN0tORMUk_rKuohtkcY1_6uaVIsQ8NnOMl5Xszg9NzkQh5Je2Gi-qRzMxskJ0fJDCAfA",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTg3MjExNTU5Iiwic3ViIjoiMDAxODgzLmZjYzc3YmE5NzUwMDQwMjM4OWRmOTY4MjFhZDljNzkwLjE1MTciLCJhdF9oYXNoIjoiZU95MHk3WFZleGRremM3dXVEWmlDUSIsImVtYWlsIjoiam9obm55LmFwcGxlc2VlZEBhcHBsZS5sb2NhbCIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImF1dGhfdGltZSI6IjE1ODcyMTE1NTYiLCJub25jZV9zdXBwb3J0ZWQiOiJ0cnVlIiwiZXhwIjoxNTg3MjEyMTU5LCJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCJ9.zu386hf3Y_3EG_OZsf-jpPKurH5HFmJ0Aal4Gnc_G-VpVoa8SvhNR_7UTbZtmQs8jOvjldPZzzXHJLWDBL_6yKIhnOntxd3G4QwIfM6PzkhiFiZXd1xHbDdx1aJ1EPnZWHPfRPtaQibda5BhenBRwAK3CPhvr7DLio54xtw-FDZgyakOHbb_2QYz0N0FBlyM5vzQEVObOKm9V2qx6hk5t7aeobOf8jOKJcx8WXWCpGQX6LOTpNnfD7Jw4Xlnb0IK6BC-agyFy_KZ5ujmB10wFnmIz9-QtvwTY4tTYpY7RigMHGIbmLS6egJTI0UhsvEHuXxaEXJ-52YGo_IIJCV6DQ",
"refresh_token": "secret-refresh-token",
"token_type": "bearer"
}
Expand All @@ -83,7 +89,7 @@
"contentJson": {
"access_token": "secret-access-token",
"expires_in": "300",
"id_token": "eyJraWQiOiI4NkQ4OEtmIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCIsImV4cCI6MTU4NzIxMjE1OSwiaWF0IjoxNTg3MjExNTU5LCJzdWIiOiIwMDE4ODMuZmNjNzdiYTk3NTAwNDAyMzg5ZGY5NjgyMWFkOWM3OTAuMTUxNyIsImF0X2hhc2giOiJlT3kweTdYVmV4ZGt6Yzd1dURaaUNRIiwiZW1haWwiOiJ1c3Nja2VmdXo2QHByaXZhdGVyZWxheS5hcHBsZWlkLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiYXV0aF90aW1lIjoxNTg3MjExNTU2LCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.ZPUgcJlCneXLNZiFDraKpWVtFPSyoxkWgrMlTZ8tM3IBBXOmQFbb75OBQC-JbZHciry96y-sy33O_fF8gaudmInH1EorDIsfryafNd0POD-8pJWY9PiGrGx50c_1DLIIIsYEm0p-JEIfQpzJ-lIWpz9ujv4ChmZx-t3PzPzzZOVlC0q1pATqJaxhY_ntL_u98BZnfAKxzqEhb5q-1TmhtHFaEtAtsd2gGm6PTaM5N-2HXQ8Bh_BlJMH3u_KakFNJRhaezlVIlLtmgxM4VjrxUeIqba-fwBlfGXPonA_xZIHg71ZujJSlYJp3yWW3Kjsb4rUUUff7yEQF5A1LVnghwA",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOiIxNTg3MjExNTU5Iiwic3ViIjoiMDAxODgzLmZjYzc3YmE5NzUwMDQwMjM4OWRmOTY4MjFhZDljNzkwLjE1MTciLCJhdF9oYXNoIjoiZU95MHk3WFZleGRremM3dXVEWmlDUSIsImVtYWlsIjoidXNzY2tlZnV6NkBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJhdXRoX3RpbWUiOiIxNTg3MjExNTU2Iiwibm9uY2Vfc3VwcG9ydGVkIjoidHJ1ZSIsImlzX3ByaXZhdGVfZW1haWwiOiJ0cnVlIiwiZXhwIjoxNTg3MjEyMTU5LCJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1hcnRpbmNvc3RlbGxvLnNpZ25pbndpdGhhcHBsZS50ZXN0LmNsaWVudCJ9.Xz-HeSAGEvPL0ObpZUYYexefSAPmRO9O_x2MTdbJKXuW65gluyJoRYfjzkKrnQUGEFvGUJ1qUiEIcdGs3kCo_TmSk6xH6e_loNYMI2J_7qb2i1-LOFHajNd1g1kTNGwSu2E22iE2IqecwfKpE7-a8thRFfbwuKyd6MNnm_NwMKBWr7IaekUc3Z876gtq94QlhItbBz8brQO6qTTekEigGEfa_h20WkPg3ZZVdqV8F-mJAQZXsGbVKToLi_L1AS6AiKxuHpTn04IGz1y6ezbng3STp-JzZslv85DJAJdZTieFh4s9RH0RFV_1GvfiExB8Q6COCaMFP7rnAVgc-27Uhg",
"refresh_token": "secret-refresh-token",
"token_type": "bearer"
}
Expand Down

0 comments on commit a0fe95e

Please sign in to comment.