Include social logins and authenticator key as part of personal download (#1745)

This commit is contained in:
Hao Kung 2018-04-12 15:41:27 -07:00 committed by GitHub
parent 956e76f6cf
commit 5a2eb3becd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 9 deletions

View File

@ -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<TUser>(b =>
{
@ -135,7 +136,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
if (encryptPersonalData)
{
var converter = new PersonalDataConverter(this.GetService<IPersonalDataProtector>());
converter = new PersonalDataConverter(this.GetService<IPersonalDataProtector>());
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");
});
}

View File

@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Identity
/// <summary>
/// Gets or sets the token value.
/// </summary>
[ProtectedPersonalData]
public virtual string Value { get; set; }
}
}

View File

@ -51,7 +51,6 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
// Only include personal data for download
var personalData = new Dictionary<string, string>();
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");
}

View File

@ -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", "<key>", "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;
}
/// <summary>
/// Test.
/// </summary>
@ -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));

View File

@ -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}\""));
}
}