Include social logins and authenticator key as part of personal download (#1745)
This commit is contained in:
parent
956e76f6cf
commit
5a2eb3becd
|
|
@ -119,6 +119,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
var storeOptions = GetStoreOptions();
|
var storeOptions = GetStoreOptions();
|
||||||
var maxKeyLength = storeOptions?.MaxLengthForKeys ?? 0;
|
var maxKeyLength = storeOptions?.MaxLengthForKeys ?? 0;
|
||||||
var encryptPersonalData = storeOptions?.ProtectPersonalData ?? false;
|
var encryptPersonalData = storeOptions?.ProtectPersonalData ?? false;
|
||||||
|
PersonalDataConverter converter = null;
|
||||||
|
|
||||||
builder.Entity<TUser>(b =>
|
builder.Entity<TUser>(b =>
|
||||||
{
|
{
|
||||||
|
|
@ -135,7 +136,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
|
|
||||||
if (encryptPersonalData)
|
if (encryptPersonalData)
|
||||||
{
|
{
|
||||||
var converter = new PersonalDataConverter(this.GetService<IPersonalDataProtector>());
|
converter = new PersonalDataConverter(this.GetService<IPersonalDataProtector>());
|
||||||
var personalDataProps = typeof(TUser).GetProperties().Where(
|
var personalDataProps = typeof(TUser).GetProperties().Where(
|
||||||
prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute)));
|
prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute)));
|
||||||
foreach (var p in personalDataProps)
|
foreach (var p in personalDataProps)
|
||||||
|
|
@ -182,6 +183,20 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
b.Property(t => t.Name).HasMaxLength(maxKeyLength);
|
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");
|
b.ToTable("AspNetUserTokens");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the token value.
|
/// Gets or sets the token value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ProtectedPersonalData]
|
||||||
public virtual string Value { get; set; }
|
public virtual string Value { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +51,6 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
|
||||||
|
|
||||||
// Only include personal data for download
|
// Only include personal data for download
|
||||||
var personalData = new Dictionary<string, string>();
|
var personalData = new Dictionary<string, string>();
|
||||||
|
|
||||||
var personalDataProps = typeof(TUser).GetProperties().Where(
|
var personalDataProps = typeof(TUser).GetProperties().Where(
|
||||||
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
|
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
|
||||||
foreach (var p in personalDataProps)
|
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");
|
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");
|
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
|
||||||
return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");
|
return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,11 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
var newName = Guid.NewGuid().ToString();
|
var newName = Guid.NewGuid().ToString();
|
||||||
Assert.Null(await manager.FindByNameAsync(newName));
|
Assert.Null(await manager.FindByNameAsync(newName));
|
||||||
IdentityResultAssert.IsSuccess(await manager.SetPhoneNumberAsync(user, "123-456-7890"));
|
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.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.SetUserNameAsync(user, newName));
|
||||||
IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user));
|
IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user));
|
||||||
|
|
@ -97,6 +100,7 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
DefaultKeyRing.Current = "NewPad";
|
DefaultKeyRing.Current = "NewPad";
|
||||||
Assert.NotNull(await manager.FindByNameAsync(newName));
|
Assert.NotNull(await manager.FindByNameAsync(newName));
|
||||||
Assert.Equal(user, await manager.FindByEmailAsync("hao@hao.com"));
|
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));
|
Assert.Equal("123-456-7890", await manager.GetPhoneNumberAsync(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,6 +144,26 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
return false;
|
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>
|
/// <summary>
|
||||||
/// Test.
|
/// Test.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -188,6 +212,9 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
user.PhoneNumber = "12345678";
|
user.PhoneNumber = "12345678";
|
||||||
IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user));
|
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();
|
var conn = dbContext.Database.GetDbConnection();
|
||||||
conn.Open();
|
conn.Open();
|
||||||
if (protect)
|
if (protect)
|
||||||
|
|
@ -197,6 +224,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
Assert.True(FindInk(conn, "UserName", guid));
|
Assert.True(FindInk(conn, "UserName", guid));
|
||||||
Assert.True(FindInk(conn, "PersonalData1", guid));
|
Assert.True(FindInk(conn, "PersonalData1", guid));
|
||||||
Assert.True(FindInk(conn, "PersonalData2", guid));
|
Assert.True(FindInk(conn, "PersonalData2", guid));
|
||||||
|
Assert.True(FindAuthenticatorKeyInk(conn, guid));
|
||||||
|
Assert.True(FindTokenInk(conn, guid, "loginProvider", "token"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -205,7 +234,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
Assert.False(FindInk(conn, "UserName", guid));
|
Assert.False(FindInk(conn, "UserName", guid));
|
||||||
Assert.False(FindInk(conn, "PersonalData1", guid));
|
Assert.False(FindInk(conn, "PersonalData1", guid));
|
||||||
Assert.False(FindInk(conn, "PersonalData2", 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, "NonPersonalData1", guid));
|
||||||
Assert.False(FindInk(conn, "NonPersonalData2", guid));
|
Assert.False(FindInk(conn, "NonPersonalData2", guid));
|
||||||
|
|
|
||||||
|
|
@ -196,18 +196,33 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task CanDownloadPersonalData()
|
[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))
|
using (StartLog(out var loggerFactory))
|
||||||
{
|
{
|
||||||
// Arrange
|
// 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 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
|
// Act & Assert
|
||||||
var jsonData = await UserStories.DownloadPersonalData(index, userName);
|
var jsonData = await UserStories.DownloadPersonalData(index, userName);
|
||||||
|
|
@ -217,7 +232,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
||||||
Assert.Contains($"\"EmailConfirmed\":\"False\"", jsonData);
|
Assert.Contains($"\"EmailConfirmed\":\"False\"", jsonData);
|
||||||
Assert.Contains($"\"PhoneNumber\":\"null\"", jsonData);
|
Assert.Contains($"\"PhoneNumber\":\"null\"", jsonData);
|
||||||
Assert.Contains($"\"PhoneNumberConfirmed\":\"False\"", 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}\""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue