From 5a2eb3becdf6e25601f984542fc2f95a780314f0 Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Thu, 12 Apr 2018 15:41:27 -0700 Subject: [PATCH] Include social logins and authenticator key as part of personal download (#1745) --- src/EF/IdentityUserContext.cs | 17 +++++++++- src/Stores/IdentityUserToken.cs | 1 + .../Manage/DownloadPersonalData.cshtml.cs | 9 +++++- .../UserStoreEncryptPersonalDataTest.cs | 32 ++++++++++++++++++- .../ManagementTests.cs | 29 +++++++++++++---- 5 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/EF/IdentityUserContext.cs b/src/EF/IdentityUserContext.cs index 3e6db773a8..73d7664a05 100644 --- a/src/EF/IdentityUserContext.cs +++ b/src/EF/IdentityUserContext.cs @@ -119,6 +119,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore var storeOptions = GetStoreOptions(); var maxKeyLength = storeOptions?.MaxLengthForKeys ?? 0; var encryptPersonalData = storeOptions?.ProtectPersonalData ?? false; + PersonalDataConverter converter = null; builder.Entity(b => { @@ -135,7 +136,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore if (encryptPersonalData) { - var converter = new PersonalDataConverter(this.GetService()); + converter = new PersonalDataConverter(this.GetService()); var personalDataProps = typeof(TUser).GetProperties().Where( prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute))); foreach (var p in personalDataProps) @@ -182,6 +183,20 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore b.Property(t => t.Name).HasMaxLength(maxKeyLength); } + if (encryptPersonalData) + { + var tokenProps = typeof(TUserToken).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute))); + foreach (var p in tokenProps) + { + if (p.PropertyType != typeof(string)) + { + throw new InvalidOperationException(Resources.CanOnlyProtectStrings); + } + b.Property(typeof(string), p.Name).HasConversion(converter); + } + } + b.ToTable("AspNetUserTokens"); }); } diff --git a/src/Stores/IdentityUserToken.cs b/src/Stores/IdentityUserToken.cs index 007701b623..b03a6976b8 100644 --- a/src/Stores/IdentityUserToken.cs +++ b/src/Stores/IdentityUserToken.cs @@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Identity /// /// Gets or sets the token value. /// + [ProtectedPersonalData] public virtual string Value { get; set; } } } \ No newline at end of file diff --git a/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs b/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs index ddf0f2de14..827c8c02b4 100644 --- a/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs +++ b/src/UI/Areas/Identity/Pages/Account/Manage/DownloadPersonalData.cshtml.cs @@ -51,7 +51,6 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal // Only include personal data for download var personalData = new Dictionary(); - var personalDataProps = typeof(TUser).GetProperties().Where( prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); foreach (var p in personalDataProps) @@ -59,6 +58,14 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); } + var logins = await _userManager.GetLoginsAsync(user); + foreach (var l in logins) + { + personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); + } + + personalData.Add($"Authenticator Key", await _userManager.GetAuthenticatorKeyAsync(user)); + Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json"); return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json"); } diff --git a/test/EF.Test/UserStoreEncryptPersonalDataTest.cs b/test/EF.Test/UserStoreEncryptPersonalDataTest.cs index 5d9114cc61..ae7f604104 100644 --- a/test/EF.Test/UserStoreEncryptPersonalDataTest.cs +++ b/test/EF.Test/UserStoreEncryptPersonalDataTest.cs @@ -87,8 +87,11 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test var newName = Guid.NewGuid().ToString(); Assert.Null(await manager.FindByNameAsync(newName)); IdentityResultAssert.IsSuccess(await manager.SetPhoneNumberAsync(user, "123-456-7890")); + var login = new UserLoginInfo("loginProvider", "", "display"); + IdentityResultAssert.IsSuccess(await manager.AddLoginAsync(user, login)); Assert.Equal(user, await manager.FindByEmailAsync("hao@hao.com")); + Assert.Equal(user, await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey)); IdentityResultAssert.IsSuccess(await manager.SetUserNameAsync(user, newName)); IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user)); @@ -97,6 +100,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test DefaultKeyRing.Current = "NewPad"; Assert.NotNull(await manager.FindByNameAsync(newName)); Assert.Equal(user, await manager.FindByEmailAsync("hao@hao.com")); + Assert.Equal(user, await manager.FindByLoginAsync(login.LoginProvider, login.ProviderKey)); Assert.Equal("123-456-7890", await manager.GetPhoneNumberAsync(user)); } @@ -140,6 +144,26 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test return false; } + private bool FindAuthenticatorKeyInk(DbConnection conn, string id) + => FindTokenInk(conn, id, "[AspNetUserStore]", "AuthenticatorKey"); + + private bool FindTokenInk(DbConnection conn, string id, string loginProvider, string tokenName) + { + using (var command = conn.CreateCommand()) + { + command.CommandText = $"SELECT u.Value FROM AspNetUserTokens u WHERE u.LoginProvider = '{loginProvider}' AND u.Name = '{tokenName}' AND u.UserId = '{id}'"; + command.CommandType = System.Data.CommandType.Text; + using (var reader = command.ExecuteReader()) + { + if (reader.Read()) + { + return reader.GetString(0) == "Default:ink"; + } + } + } + return false; + } + /// /// Test. /// @@ -188,6 +212,9 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test user.PhoneNumber = "12345678"; IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user)); + IdentityResultAssert.IsSuccess(await manager.ResetAuthenticatorKeyAsync(user)); + IdentityResultAssert.IsSuccess(await manager.SetAuthenticationTokenAsync(user, "loginProvider", "token", "value")); + var conn = dbContext.Database.GetDbConnection(); conn.Open(); if (protect) @@ -197,6 +224,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test Assert.True(FindInk(conn, "UserName", guid)); Assert.True(FindInk(conn, "PersonalData1", guid)); Assert.True(FindInk(conn, "PersonalData2", guid)); + Assert.True(FindAuthenticatorKeyInk(conn, guid)); + Assert.True(FindTokenInk(conn, guid, "loginProvider", "token")); } else { @@ -205,7 +234,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test Assert.False(FindInk(conn, "UserName", guid)); Assert.False(FindInk(conn, "PersonalData1", guid)); Assert.False(FindInk(conn, "PersonalData2", guid)); - + Assert.False(FindAuthenticatorKeyInk(conn, guid)); + Assert.False(FindTokenInk(conn, guid, "loginProvider", "token")); } Assert.False(FindInk(conn, "NonPersonalData1", guid)); Assert.False(FindInk(conn, "NonPersonalData2", guid)); diff --git a/test/Identity.FunctionalTests/ManagementTests.cs b/test/Identity.FunctionalTests/ManagementTests.cs index 1bb2fd45e1..5720d6ecc9 100644 --- a/test/Identity.FunctionalTests/ManagementTests.cs +++ b/test/Identity.FunctionalTests/ManagementTests.cs @@ -196,18 +196,33 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests } } - [Fact] - public async Task CanDownloadPersonalData() + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task CanDownloadPersonalData(bool twoFactor, bool social) { using (StartLog(out var loggerFactory)) { // Arrange - var client = ServerFactory.CreateDefaultClient(loggerFactory); + var server = ServerFactory.CreateServer(loggerFactory, builder => + builder.ConfigureTestServices(s => s.SetupTestThirdPartyLogin())); + + var client = ServerFactory.CreateDefaultClient(server); var userName = $"{Guid.NewGuid()}@example.com"; - var password = $"!Test.Password1$"; + var guid = Guid.NewGuid(); + var email = userName; - var index = await UserStories.RegisterNewUserAsync(client, userName, password); + var index = social + ? await UserStories.RegisterNewUserWithSocialLoginAsync(client, userName, email) + : await UserStories.RegisterNewUserAsync(client, email, "!TestPassword1"); + + if (twoFactor) + { + await UserStories.EnableTwoFactorAuthentication(index); + } // Act & Assert var jsonData = await UserStories.DownloadPersonalData(index, userName); @@ -217,7 +232,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests Assert.Contains($"\"EmailConfirmed\":\"False\"", jsonData); Assert.Contains($"\"PhoneNumber\":\"null\"", jsonData); Assert.Contains($"\"PhoneNumberConfirmed\":\"False\"", jsonData); - Assert.Contains($"\"TwoFactorEnabled\":\"False\"", jsonData); + Assert.Contains($"\"TwoFactorEnabled\":\"{twoFactor}\"", jsonData); + Assert.Equal(twoFactor, jsonData.Contains($"\"Authenticator Key\":\"")); + Assert.Equal(social, jsonData.Contains($"\"Contoso external login provider key\":\"{userName}\"")); } }