diff --git a/src/AspNet.Security.OAuth.Apple/AppleAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Apple/AppleAuthenticationHandler.cs index cd385e691..be9c3c0ab 100644 --- a/src/AspNet.Security.OAuth.Apple/AppleAuthenticationHandler.cs +++ b/src/AspNet.Security.OAuth.Apple/AppleAuthenticationHandler.cs @@ -133,10 +133,19 @@ protected virtual IEnumerable ExtractClaimsFromToken([NotNull] string tok { var securityToken = Options.SecurityTokenHandler.ReadJsonWebToken(token); - return new List(securityToken.Claims) + var claims = new List(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) { @@ -161,11 +170,6 @@ protected virtual IEnumerable 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; } diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs b/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs index dd2151cae..dcb53ec73 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs +++ b/test/AspNet.Security.OAuth.Providers.Tests/Apple/AppleTests.cs @@ -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; @@ -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, "johnny.appleseed@apple.local"), + 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, "ussckefuz6@privaterelay.appleid.com"), + 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 { } diff --git a/test/AspNet.Security.OAuth.Providers.Tests/Apple/bundle.json b/test/AspNet.Security.OAuth.Providers.Tests/Apple/bundle.json index 93c25acad..163b584bc 100644 --- a/test/AspNet.Security.OAuth.Providers.Tests/Apple/bundle.json +++ b/test/AspNet.Security.OAuth.Providers.Tests/Apple/bundle.json @@ -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" @@ -39,8 +40,13 @@ "email_verified", "exp", "iat", + "is_private_email", "iss", - "sub" + "nonce", + "nonce_supported", + "real_user_status", + "sub", + "transfer_sub" ] } }, @@ -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" } ] @@ -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" } @@ -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" }