ProtectPersonalData + extensiblity support (#1562)
This commit is contained in:
parent
47d610b0cc
commit
9ecbefcf21
|
|
@ -1,14 +1,12 @@
|
||||||
using System;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
|
|
||||||
namespace IdentitySample.DefaultUI.Data
|
namespace IdentitySample.DefaultUI.Data
|
||||||
{
|
{
|
||||||
public class ApplicationUser : IdentityUser
|
public class ApplicationUser : IdentityUser
|
||||||
{
|
{
|
||||||
|
[ProtectedPersonalData]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
[PersonalData]
|
||||||
public int Age { get; set; }
|
public int Age { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation of <see cref="IPersonalDataProtector"/> that uses <see cref="ILookupProtectorKeyRing"/>
|
||||||
|
/// and <see cref="ILookupProtector"/> to protect data with a payload format of {keyId}:{protectedData}
|
||||||
|
/// </summary>
|
||||||
|
public class DefaultPersonalDataProtector : IPersonalDataProtector
|
||||||
|
{
|
||||||
|
private readonly ILookupProtectorKeyRing _keyRing;
|
||||||
|
private readonly ILookupProtector _encryptor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyRing"></param>
|
||||||
|
/// <param name="protector"></param>
|
||||||
|
public DefaultPersonalDataProtector(ILookupProtectorKeyRing keyRing, ILookupProtector protector)
|
||||||
|
{
|
||||||
|
_keyRing = keyRing;
|
||||||
|
_encryptor = protector;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unprotect the data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The data to unprotect.</param>
|
||||||
|
/// <returns>The unprotected data.</returns>
|
||||||
|
public virtual string Unprotect(string data)
|
||||||
|
{
|
||||||
|
var split = data.IndexOf(':');
|
||||||
|
if (split == -1 || split == data.Length-1)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Malformed data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyId = data.Substring(0, split);
|
||||||
|
return _encryptor.Unprotect(keyId, data.Substring(split + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Protect the data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The data to protect.</param>
|
||||||
|
/// <returns>The protected data.</returns>
|
||||||
|
public virtual string Protect(string data)
|
||||||
|
{
|
||||||
|
var current = _keyRing.CurrentKeyId;
|
||||||
|
return current + ":" + _encryptor.Protect(current, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to protect/unprotect lookups with a specific key.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILookupProtector
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Protect the data using the specified key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyId">The key to use.</param>
|
||||||
|
/// <param name="data">The data to protect.</param>
|
||||||
|
/// <returns>The protected data.</returns>
|
||||||
|
string Protect(string keyId, string data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unprotect the data using the specified key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyId">The key to use.</param>
|
||||||
|
/// <param name="data">The data to unprotect.</param>
|
||||||
|
/// <returns>The original data.</returns>
|
||||||
|
string Unprotect(string keyId, string data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstraction used to manage named keys used to protect lookups.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILookupProtectorKeyRing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the current key id.
|
||||||
|
/// </summary>
|
||||||
|
string CurrentKeyId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a specific key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyId">The id of the key to fetch.</param>
|
||||||
|
/// <returns>The key ring.</returns>
|
||||||
|
string this[string keyId] { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return all of the key ids.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>All of the key ids.</returns>
|
||||||
|
IEnumerable<string> GetAllKeyIds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an abstraction used for personal data encryption.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPersonalDataProtector
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Protect the data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The data to protect.</param>
|
||||||
|
/// <returns>The protected data.</returns>
|
||||||
|
string Protect(string data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unprotect the data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
/// <returns>The unprotected data.</returns>
|
||||||
|
string Unprotect(string data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marker interface used to signal that the store supports the <see cref="StoreOptions.ProtectPersonalData"/> flag.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TUser">The type that represents a user.</typeparam>
|
||||||
|
public interface IProtectedUserStore<TUser> : IUserStore<TUser> where TUser : class
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
@ -184,6 +184,21 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return AddScoped(typeof(IRoleValidator<>).MakeGenericType(RoleType), typeof(TRole));
|
return AddScoped(typeof(IRoleValidator<>).MakeGenericType(RoleType), typeof(TRole));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an <see cref="ILookupProtector"/> and <see cref="ILookupProtectorKeyRing"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TProtector">The personal data protector type.</typeparam>
|
||||||
|
/// <typeparam name="TKeyRing">The personal data protector key ring type.</typeparam>
|
||||||
|
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
|
||||||
|
public virtual IdentityBuilder AddPersonalDataProtection<TProtector, TKeyRing>()
|
||||||
|
where TProtector : class,ILookupProtector
|
||||||
|
where TKeyRing : class, ILookupProtectorKeyRing
|
||||||
|
{
|
||||||
|
Services.AddSingleton<IPersonalDataProtector, DefaultPersonalDataProtector>();
|
||||||
|
Services.AddSingleton<ILookupProtector, TProtector>();
|
||||||
|
Services.AddSingleton<ILookupProtectorKeyRing, TKeyRing>();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a <see cref="IRoleStore{TRole}"/> for the <seealso cref="RoleType"/>.
|
/// Adds a <see cref="IRoleStore{TRole}"/> for the <seealso cref="RoleType"/>.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to indicate that a something is considered personal data.
|
||||||
|
/// </summary>
|
||||||
|
public class PersonalDataAttribute : Attribute
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
@ -682,6 +682,34 @@ namespace Microsoft.Extensions.Identity.Core
|
||||||
internal static string FormatNoRoleType()
|
internal static string FormatNoRoleType()
|
||||||
=> GetString("NoRoleType");
|
=> GetString("NoRoleType");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IProtectedUserStore<TUser> which is required when ProtectPersonalData = true.
|
||||||
|
/// </summary>
|
||||||
|
internal static string StoreNotIProtectedUserStore
|
||||||
|
{
|
||||||
|
get => GetString("StoreNotIProtectedUserStore");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Store does not implement IProtectedUserStore<TUser> which is required when ProtectPersonalData = true.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatStoreNotIProtectedUserStore()
|
||||||
|
=> GetString("StoreNotIProtectedUserStore");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No IPersonalDataProtector service was registered, this is required when ProtectPersonalData = true.
|
||||||
|
/// </summary>
|
||||||
|
internal static string NoPersonalDataProtector
|
||||||
|
{
|
||||||
|
get => GetString("NoPersonalDataProtector");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No IPersonalDataProtector service was registered, this is required when ProtectPersonalData = true.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatNoPersonalDataProtector()
|
||||||
|
=> GetString("NoPersonalDataProtector");
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to indicate that a something is considered personal data and should be protected.
|
||||||
|
/// </summary>
|
||||||
|
public class ProtectedPersonalDataAttribute : PersonalDataAttribute
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
@ -309,4 +309,12 @@
|
||||||
<value>No RoleType was specified, try AddRoles<TRole>().</value>
|
<value>No RoleType was specified, try AddRoles<TRole>().</value>
|
||||||
<comment>Error when the IdentityBuilder.RoleType was not specified</comment>
|
<comment>Error when the IdentityBuilder.RoleType was not specified</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="StoreNotIProtectedUserStore" xml:space="preserve">
|
||||||
|
<value>Store does not implement IProtectedUserStore<TUser> which is required when ProtectPersonalData = true.</value>
|
||||||
|
<comment>Error when the store does not implement this interface</comment>
|
||||||
|
</data>
|
||||||
|
<data name="NoPersonalDataProtector" xml:space="preserve">
|
||||||
|
<value>No IPersonalDataProtector service was registered, this is required when ProtectPersonalData = true.</value>
|
||||||
|
<comment>Error when there is no IPersonalDataProtector</comment>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -13,5 +13,11 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// properties used as keys, i.e. UserId, LoginProvider, ProviderKey.
|
/// properties used as keys, i.e. UserId, LoginProvider, ProviderKey.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxLengthForKeys { get; set; }
|
public int MaxLengthForKeys { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If set to true, the store must protect all personally identifying data for a user.
|
||||||
|
/// This will be enforced by requiring the store to implement <see cref="IProtectedUserStore{TUser}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProtectPersonalData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
private TimeSpan _defaultLockout = TimeSpan.Zero;
|
private TimeSpan _defaultLockout = TimeSpan.Zero;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
|
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
|
||||||
|
private IServiceProvider _services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The cancellation token used to cancel operations.
|
/// The cancellation token used to cancel operations.
|
||||||
|
|
@ -99,6 +99,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_services = services;
|
||||||
if (services != null)
|
if (services != null)
|
||||||
{
|
{
|
||||||
foreach (var providerName in Options.Tokens.ProviderMap.Keys)
|
foreach (var providerName in Options.Tokens.ProviderMap.Keys)
|
||||||
|
|
@ -113,6 +114,18 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Options.Stores.ProtectPersonalData)
|
||||||
|
{
|
||||||
|
if (!(Store is IProtectedUserStore<TUser>))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(Resources.StoreNotIProtectedUserStore);
|
||||||
|
}
|
||||||
|
if (services.GetService<ILookupProtector>() == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(Resources.NoPersonalDataProtector);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -527,7 +540,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userName"/> if it exists.
|
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userName"/> if it exists.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public virtual Task<TUser> FindByNameAsync(string userName)
|
public virtual async Task<TUser> FindByNameAsync(string userName)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
if (userName == null)
|
if (userName == null)
|
||||||
|
|
@ -535,7 +548,28 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
throw new ArgumentNullException(nameof(userName));
|
throw new ArgumentNullException(nameof(userName));
|
||||||
}
|
}
|
||||||
userName = NormalizeKey(userName);
|
userName = NormalizeKey(userName);
|
||||||
return Store.FindByNameAsync(userName, CancellationToken);
|
|
||||||
|
var user = await Store.FindByNameAsync(userName, CancellationToken);
|
||||||
|
|
||||||
|
// Need to potentially check all keys
|
||||||
|
if (user == null && Options.Stores.ProtectPersonalData)
|
||||||
|
{
|
||||||
|
var keyRing = _services.GetService<ILookupProtectorKeyRing>();
|
||||||
|
var protector = _services.GetService<ILookupProtector>();
|
||||||
|
if (keyRing != null && protector != null)
|
||||||
|
{
|
||||||
|
foreach (var key in keyRing.GetAllKeyIds())
|
||||||
|
{
|
||||||
|
var oldKey = protector.Protect(key, userName);
|
||||||
|
user = await Store.FindByNameAsync(oldKey, CancellationToken);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -578,6 +612,17 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
return (KeyNormalizer == null) ? key : KeyNormalizer.Normalize(key);
|
return (KeyNormalizer == null) ? key : KeyNormalizer.Normalize(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ProtectPersonalData(string data)
|
||||||
|
{
|
||||||
|
if (Options.Stores.ProtectPersonalData)
|
||||||
|
{
|
||||||
|
var keyRing = _services.GetService<ILookupProtectorKeyRing>();
|
||||||
|
var protector = _services.GetService<ILookupProtector>();
|
||||||
|
return protector.Protect(keyRing.CurrentKeyId, data);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the normalized user name for the specified <paramref name="user"/>.
|
/// Updates the normalized user name for the specified <paramref name="user"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -586,6 +631,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
public virtual async Task UpdateNormalizedUserNameAsync(TUser user)
|
public virtual async Task UpdateNormalizedUserNameAsync(TUser user)
|
||||||
{
|
{
|
||||||
var normalizedName = NormalizeKey(await GetUserNameAsync(user));
|
var normalizedName = NormalizeKey(await GetUserNameAsync(user));
|
||||||
|
normalizedName = ProtectPersonalData(normalizedName);
|
||||||
await Store.SetNormalizedUserNameAsync(user, normalizedName, CancellationToken);
|
await Store.SetNormalizedUserNameAsync(user, normalizedName, CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1352,7 +1398,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// The task object containing the results of the asynchronous lookup operation, the user, if any, associated with a normalized value of the specified email address.
|
/// The task object containing the results of the asynchronous lookup operation, the user, if any, associated with a normalized value of the specified email address.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public virtual Task<TUser> FindByEmailAsync(string email)
|
public virtual async Task<TUser> FindByEmailAsync(string email)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
var store = GetEmailStore();
|
var store = GetEmailStore();
|
||||||
|
|
@ -1360,7 +1406,29 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(email));
|
throw new ArgumentNullException(nameof(email));
|
||||||
}
|
}
|
||||||
return store.FindByEmailAsync(NormalizeKey(email), CancellationToken);
|
|
||||||
|
email = NormalizeKey(email);
|
||||||
|
var user = await store.FindByEmailAsync(email, CancellationToken);
|
||||||
|
|
||||||
|
// Need to potentially check all keys
|
||||||
|
if (user == null && Options.Stores.ProtectPersonalData)
|
||||||
|
{
|
||||||
|
var keyRing = _services.GetService<ILookupProtectorKeyRing>();
|
||||||
|
var protector = _services.GetService<ILookupProtector>();
|
||||||
|
if (keyRing != null && protector != null)
|
||||||
|
{
|
||||||
|
foreach (var key in keyRing.GetAllKeyIds())
|
||||||
|
{
|
||||||
|
var oldKey = protector.Protect(key, email);
|
||||||
|
user = await store.FindByEmailAsync(oldKey, CancellationToken);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1374,7 +1442,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
if (store != null)
|
if (store != null)
|
||||||
{
|
{
|
||||||
var email = await GetEmailAsync(user);
|
var email = await GetEmailAsync(user);
|
||||||
await store.SetNormalizedEmailAsync(user, NormalizeKey(email), CancellationToken);
|
await store.SetNormalizedEmailAsync(user, ProtectPersonalData(NormalizeKey(email)), CancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.Converters;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
|
@ -96,16 +96,16 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<TUserToken> UserTokens { get; set; }
|
public DbSet<TUserToken> UserTokens { get; set; }
|
||||||
|
|
||||||
private int GetMaxLengthForKeys()
|
private StoreOptions GetStoreOptions() => this.GetService<IDbContextOptions>()
|
||||||
{
|
|
||||||
// Need to get the actual application service provider, fallback will cause
|
|
||||||
// options to not work since IEnumerable<IConfigureOptions> don't flow across providers
|
|
||||||
var options = this.GetService<IDbContextOptions>()
|
|
||||||
.Extensions.OfType<CoreOptionsExtension>()
|
.Extensions.OfType<CoreOptionsExtension>()
|
||||||
.FirstOrDefault()?.ApplicationServiceProvider
|
.FirstOrDefault()?.ApplicationServiceProvider
|
||||||
?.GetService<IOptions<IdentityOptions>>()
|
?.GetService<IOptions<IdentityOptions>>()
|
||||||
?.Value?.Stores;
|
?.Value?.Stores;
|
||||||
return options != null ? options.MaxLengthForKeys : 0;
|
|
||||||
|
private class PersonalDataConverter : ValueConverter<string, string>
|
||||||
|
{
|
||||||
|
public PersonalDataConverter(IPersonalDataProtector protector) : base(s => protector.Protect(s), s => protector.Unprotect(s), default)
|
||||||
|
{ }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -116,7 +116,9 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
/// </param>
|
/// </param>
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
var maxKeyLength = GetMaxLengthForKeys();
|
var storeOptions = GetStoreOptions();
|
||||||
|
var maxKeyLength = storeOptions?.MaxLengthForKeys ?? 0;
|
||||||
|
var encryptPersonalData = storeOptions?.ProtectPersonalData ?? false;
|
||||||
|
|
||||||
builder.Entity<TUser>(b =>
|
builder.Entity<TUser>(b =>
|
||||||
{
|
{
|
||||||
|
|
@ -131,6 +133,21 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
b.Property(u => u.Email).HasMaxLength(256);
|
b.Property(u => u.Email).HasMaxLength(256);
|
||||||
b.Property(u => u.NormalizedEmail).HasMaxLength(256);
|
b.Property(u => u.NormalizedEmail).HasMaxLength(256);
|
||||||
|
|
||||||
|
if (encryptPersonalData)
|
||||||
|
{
|
||||||
|
var converter = new PersonalDataConverter(this.GetService<IPersonalDataProtector>());
|
||||||
|
var personalDataProps = typeof(TUser).GetProperties().Where(
|
||||||
|
prop => Attribute.IsDefined(prop, typeof(ProtectedPersonalDataAttribute)));
|
||||||
|
foreach (var p in personalDataProps)
|
||||||
|
{
|
||||||
|
if (p.PropertyType != typeof(string))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(Resources.CanOnlyProtectStrings);
|
||||||
|
}
|
||||||
|
b.Property(typeof(string), p.Name).HasConversion(converter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
|
b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();
|
||||||
b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
|
b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();
|
||||||
b.HasMany<TUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();
|
b.HasMany<TUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,20 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
private static readonly ResourceManager _resourceManager
|
private static readonly ResourceManager _resourceManager
|
||||||
= new ResourceManager("Microsoft.AspNetCore.Identity.EntityFrameworkCore.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
= new ResourceManager("Microsoft.AspNetCore.Identity.EntityFrameworkCore.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [ProtectedPersonalData] only works strings by default.
|
||||||
|
/// </summary>
|
||||||
|
internal static string CanOnlyProtectStrings
|
||||||
|
{
|
||||||
|
get => GetString("CanOnlyProtectStrings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// [ProtectedPersonalData] only works strings by default.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatCanOnlyProtectStrings()
|
||||||
|
=> GetString("CanOnlyProtectStrings");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey>.
|
/// AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,10 @@
|
||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
|
<data name="CanOnlyProtectStrings" xml:space="preserve">
|
||||||
|
<value>[ProtectedPersonalData] only works strings by default.</value>
|
||||||
|
<comment>error when attribute is used on a non string property.</comment>
|
||||||
|
</data>
|
||||||
<data name="NotIdentityRole" xml:space="preserve">
|
<data name="NotIdentityRole" xml:space="preserve">
|
||||||
<value>AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey>.</value>
|
<value>AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey>.</value>
|
||||||
<comment>error when the role does not derive from IdentityRole</comment>
|
<comment>error when the role does not derive from IdentityRole</comment>
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
IUserTwoFactorStore<TUser>,
|
IUserTwoFactorStore<TUser>,
|
||||||
IUserAuthenticationTokenStore<TUser>,
|
IUserAuthenticationTokenStore<TUser>,
|
||||||
IUserAuthenticatorKeyStore<TUser>,
|
IUserAuthenticatorKeyStore<TUser>,
|
||||||
IUserTwoFactorRecoveryCodeStore<TUser>
|
IUserTwoFactorRecoveryCodeStore<TUser>,
|
||||||
|
IProtectedUserStore<TUser>
|
||||||
where TUser : IdentityUser<TKey>
|
where TUser : IdentityUser<TKey>
|
||||||
where TContext : DbContext
|
where TContext : DbContext
|
||||||
where TKey : IEquatable<TKey>
|
where TKey : IEquatable<TKey>
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
||||||
/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
|
/// <typeparam name="TUserToken">The type representing a user token.</typeparam>
|
||||||
/// <typeparam name="TRoleClaim">The type representing a role claim.</typeparam>
|
/// <typeparam name="TRoleClaim">The type representing a role claim.</typeparam>
|
||||||
public class UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
|
public class UserStore<TUser, TRole, TContext, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim> :
|
||||||
UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>
|
UserStoreBase<TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TUserToken, TRoleClaim>,
|
||||||
|
IProtectedUserStore<TUser>
|
||||||
where TUser : IdentityUser<TKey>
|
where TUser : IdentityUser<TKey>
|
||||||
where TRole : IdentityRole<TKey>
|
where TRole : IdentityRole<TKey>
|
||||||
where TContext : DbContext
|
where TContext : DbContext
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
// <auto-generated />
|
||||||
|
namespace Microsoft.AspNetCore.Diagnostics.Identity.Service
|
||||||
|
{
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Resources;
|
||||||
|
|
||||||
|
internal static class Strings
|
||||||
|
{
|
||||||
|
private static readonly ResourceManager _resourceManager
|
||||||
|
= new ResourceManager("Microsoft.AspNetCore.Diagnostics.Identity.Service.Strings", typeof(Strings).GetTypeInfo().Assembly);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Developer certificate
|
||||||
|
/// </summary>
|
||||||
|
internal static string CertificateErrorPage_Title
|
||||||
|
{
|
||||||
|
get => GetString("CertificateErrorPage_Title");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Developer certificate
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatCertificateErrorPage_Title()
|
||||||
|
=> GetString("CertificateErrorPage_Title");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create certificate
|
||||||
|
/// </summary>
|
||||||
|
internal static string CreateCertificate
|
||||||
|
{
|
||||||
|
get => GetString("CreateCertificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create certificate
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatCreateCertificate()
|
||||||
|
=> GetString("CreateCertificate");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Done
|
||||||
|
/// </summary>
|
||||||
|
internal static string CreateCertificateDone
|
||||||
|
{
|
||||||
|
get => GetString("CreateCertificateDone");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Done
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatCreateCertificateDone()
|
||||||
|
=> GetString("CreateCertificateDone");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Certificate creation failed.
|
||||||
|
/// </summary>
|
||||||
|
internal static string CreateCertificateFailed
|
||||||
|
{
|
||||||
|
get => GetString("CreateCertificateFailed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Certificate creation failed.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatCreateCertificateFailed()
|
||||||
|
=> GetString("CreateCertificateFailed");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Certificate creation succeeded. Try refreshing the page.
|
||||||
|
/// </summary>
|
||||||
|
internal static string CreateCertificateRefresh
|
||||||
|
{
|
||||||
|
get => GetString("CreateCertificateRefresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Certificate creation succeeded. Try refreshing the page.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatCreateCertificateRefresh()
|
||||||
|
=> GetString("CreateCertificateRefresh");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creating developer certificate...
|
||||||
|
/// </summary>
|
||||||
|
internal static string CreateCertificateRunning
|
||||||
|
{
|
||||||
|
get => GetString("CreateCertificateRunning");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creating developer certificate...
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatCreateCertificateRunning()
|
||||||
|
=> GetString("CreateCertificateRunning");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identity requires a certificate to sign tokens. You can create a developer certificate by clicking the Create Certificate button to generate a developer certificate for you automatically. This will create a self-signed certificate with subject IdentityService.Development and will add it to your current user personal store.
|
||||||
|
/// Alternatively, you can create this certificate manually with the instructions given in the following link:
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
internal static string ManualCertificateGenerationInfo
|
||||||
|
{
|
||||||
|
get => GetString("ManualCertificateGenerationInfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identity requires a certificate to sign tokens. You can create a developer certificate by clicking the Create Certificate button to generate a developer certificate for you automatically. This will create a self-signed certificate with subject IdentityService.Development and will add it to your current user personal store.
|
||||||
|
/// Alternatively, you can create this certificate manually with the instructions given in the following link:
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatManualCertificateGenerationInfo()
|
||||||
|
=> GetString("ManualCertificateGenerationInfo");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// https://go.microsoft.com/fwlink/?linkid=848037
|
||||||
|
/// </summary>
|
||||||
|
internal static string ManualCertificateGenerationInfoLink
|
||||||
|
{
|
||||||
|
get => GetString("ManualCertificateGenerationInfoLink");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// https://go.microsoft.com/fwlink/?linkid=848037
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatManualCertificateGenerationInfoLink()
|
||||||
|
=> GetString("ManualCertificateGenerationInfoLink");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The developer certificate is missing or invalid
|
||||||
|
/// </summary>
|
||||||
|
internal static string MissingOrInvalidCertificate
|
||||||
|
{
|
||||||
|
get => GetString("MissingOrInvalidCertificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The developer certificate is missing or invalid
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMissingOrInvalidCertificate()
|
||||||
|
=> GetString("MissingOrInvalidCertificate");
|
||||||
|
|
||||||
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
|
{
|
||||||
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.Assert(value != null);
|
||||||
|
|
||||||
|
if (formatterNames != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < formatterNames.Length; i++)
|
||||||
|
{
|
||||||
|
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Diagnostics.Identity.Service {
|
|
||||||
using System;
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
|
||||||
/// </summary>
|
|
||||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
|
||||||
// class via a tool like ResGen or Visual Studio.
|
|
||||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
|
||||||
// with the /str option, or rebuild your VS project.
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
|
||||||
internal class Strings {
|
|
||||||
|
|
||||||
private static global::System.Resources.ResourceManager resourceMan;
|
|
||||||
|
|
||||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
|
||||||
|
|
||||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
|
||||||
internal Strings() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the cached ResourceManager instance used by this class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
|
||||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
|
||||||
get {
|
|
||||||
if (object.ReferenceEquals(resourceMan, null)) {
|
|
||||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.Diagnostics.Identity.Service.Strings", typeof(Strings).Assembly);
|
|
||||||
resourceMan = temp;
|
|
||||||
}
|
|
||||||
return resourceMan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Overrides the current thread's CurrentUICulture property for all
|
|
||||||
/// resource lookups using this strongly typed resource class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
|
||||||
internal static global::System.Globalization.CultureInfo Culture {
|
|
||||||
get {
|
|
||||||
return resourceCulture;
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
resourceCulture = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Developer certificate.
|
|
||||||
/// </summary>
|
|
||||||
internal static string CertificateErrorPage_Title {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("CertificateErrorPage_Title", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Create certificate.
|
|
||||||
/// </summary>
|
|
||||||
internal static string CreateCertificate {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("CreateCertificate", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Done.
|
|
||||||
/// </summary>
|
|
||||||
internal static string CreateCertificateDone {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("CreateCertificateDone", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Certificate creation failed..
|
|
||||||
/// </summary>
|
|
||||||
internal static string CreateCertificateFailed {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("CreateCertificateFailed", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Certificate creation succeeded. Try refreshing the page..
|
|
||||||
/// </summary>
|
|
||||||
internal static string CreateCertificateRefresh {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("CreateCertificateRefresh", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Creating developer certificate....
|
|
||||||
/// </summary>
|
|
||||||
internal static string CreateCertificateRunning {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("CreateCertificateRunning", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Identity requires a certificate to sign tokens. You can create a developer certificate by clicking the Create Certificate button to generate a developer certificate for you automatically. This will create a self-signed certificate with subject IdentityService.Development and will add it to your current user personal store.
|
|
||||||
/// Alternatively, you can create this certificate manually with the instructions given in the following link:
|
|
||||||
/// .
|
|
||||||
/// </summary>
|
|
||||||
internal static string ManualCertificateGenerationInfo {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ManualCertificateGenerationInfo", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to https://go.microsoft.com/fwlink/?linkid=848037.
|
|
||||||
/// </summary>
|
|
||||||
internal static string ManualCertificateGenerationInfoLink {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ManualCertificateGenerationInfoLink", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to The developer certificate is missing or invalid.
|
|
||||||
/// </summary>
|
|
||||||
internal static string MissingOrInvalidCertificate {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("MissingOrInvalidCertificate", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1020,12 +1020,13 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Task</returns>
|
/// <returns>Task</returns>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanFindUsersViaUserQuerable()
|
public async virtual Task CanFindUsersViaUserQuerable()
|
||||||
{
|
{
|
||||||
if (ShouldSkipDbTests())
|
if (ShouldSkipDbTests())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mgr = CreateManager();
|
var mgr = CreateManager();
|
||||||
if (mgr.SupportsQueryableUsers)
|
if (mgr.SupportsQueryableUsers)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -57,11 +57,13 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the primary key for this user.
|
/// Gets or sets the primary key for this user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[PersonalData]
|
||||||
public virtual TKey Id { get; set; }
|
public virtual TKey Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the user name for this user.
|
/// Gets or sets the user name for this user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ProtectedPersonalData]
|
||||||
public virtual string UserName { get; set; }
|
public virtual string UserName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -72,6 +74,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the email address for this user.
|
/// Gets or sets the email address for this user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ProtectedPersonalData]
|
||||||
public virtual string Email { get; set; }
|
public virtual string Email { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -83,6 +86,7 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// Gets or sets a flag indicating if a user has confirmed their email address.
|
/// Gets or sets a flag indicating if a user has confirmed their email address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>True if the email address has been confirmed, otherwise false.</value>
|
/// <value>True if the email address has been confirmed, otherwise false.</value>
|
||||||
|
[PersonalData]
|
||||||
public virtual bool EmailConfirmed { get; set; }
|
public virtual bool EmailConfirmed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -103,18 +107,21 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a telephone number for the user.
|
/// Gets or sets a telephone number for the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ProtectedPersonalData]
|
||||||
public virtual string PhoneNumber { get; set; }
|
public virtual string PhoneNumber { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a flag indicating if a user has confirmed their telephone address.
|
/// Gets or sets a flag indicating if a user has confirmed their telephone address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>True if the telephone number has been confirmed, otherwise false.</value>
|
/// <value>True if the telephone number has been confirmed, otherwise false.</value>
|
||||||
|
[PersonalData]
|
||||||
public virtual bool PhoneNumberConfirmed { get; set; }
|
public virtual bool PhoneNumberConfirmed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a flag indicating if two factor authentication is enabled for this user.
|
/// Gets or sets a flag indicating if two factor authentication is enabled for this user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>True if 2fa is enabled, otherwise false.</value>
|
/// <value>True if 2fa is enabled, otherwise false.</value>
|
||||||
|
[PersonalData]
|
||||||
public virtual bool TwoFactorEnabled { get; set; }
|
public virtual bool TwoFactorEnabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -140,8 +147,6 @@ namespace Microsoft.AspNetCore.Identity
|
||||||
/// Returns the username for this user.
|
/// Returns the username for this user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
=> UserName;
|
||||||
return UserName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<form method="post" class="form-group">
|
<form id="delete-user" method="post" class="form-group">
|
||||||
<div asp-validation-summary="All" class="text-danger"></div>
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
@if (Model.RequirePassword)
|
@if (Model.RequirePassword)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
@ -43,12 +44,13 @@ 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>();
|
||||||
personalData.Add("UserId", await _userManager.GetUserIdAsync(user));
|
|
||||||
personalData.Add("UserName", await _userManager.GetUserNameAsync(user));
|
var personalDataProps = typeof(TUser).GetProperties().Where(
|
||||||
personalData.Add("Email", await _userManager.GetEmailAsync(user));
|
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
|
||||||
personalData.Add("EmailConfirmed", (await _userManager.IsEmailConfirmedAsync(user)).ToString());
|
foreach (var p in personalDataProps)
|
||||||
personalData.Add("PhoneNumber", await _userManager.GetPhoneNumberAsync(user));
|
{
|
||||||
personalData.Add("PhoneNumberConfirmed", (await _userManager.IsPhoneNumberConfirmedAsync(user)).ToString());
|
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@
|
||||||
<p>
|
<p>
|
||||||
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
|
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
|
||||||
</p>
|
</p>
|
||||||
<form asp-page="DownloadPersonalData" method="post" class="form-group">
|
<form id="download-data" asp-page="DownloadPersonalData" method="post" class="form-group">
|
||||||
<button class="btn btn-default" type="submit">Download</button>
|
<button class="btn btn-default" type="submit">Download</button>
|
||||||
</form>
|
</form>
|
||||||
<p>
|
<p>
|
||||||
<a asp-page="DeletePersonalData" class="btn btn-default">Delete</a>
|
<a id="delete" asp-page="DeletePersonalData" class="btn btn-default">Delete</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Data.SqlClient;
|
using System.Data.SqlClient;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -25,9 +26,9 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TContext Create<TContext>(string connectionString) where TContext : DbContext
|
public static TContext Create<TContext>(string connectionString, IServiceCollection services = null) where TContext : DbContext
|
||||||
{
|
{
|
||||||
var serviceProvider = ConfigureDbServices<TContext>(connectionString).BuildServiceProvider();
|
var serviceProvider = ConfigureDbServices<TContext>(connectionString, services).BuildServiceProvider();
|
||||||
return serviceProvider.GetRequiredService<TContext>();
|
return serviceProvider.GetRequiredService<TContext>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,15 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
where TRole : IdentityRole<TKey>, new()
|
where TRole : IdentityRole<TKey>, new()
|
||||||
where TKey : IEquatable<TKey>
|
where TKey : IEquatable<TKey>
|
||||||
{
|
{
|
||||||
private readonly ScratchDatabaseFixture _fixture;
|
protected readonly ScratchDatabaseFixture _fixture;
|
||||||
|
|
||||||
protected SqlStoreTestBase(ScratchDatabaseFixture fixture)
|
protected SqlStoreTestBase(ScratchDatabaseFixture fixture)
|
||||||
{
|
{
|
||||||
_fixture = fixture;
|
_fixture = fixture;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void SetupIdentityServices(IServiceCollection services, object context)
|
protected virtual void SetupAddIdentity(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddHttpContextAccessor();
|
|
||||||
services.AddSingleton((TestDbContext)context);
|
|
||||||
services.AddLogging();
|
|
||||||
services.AddSingleton<ILogger<UserManager<TUser>>>(new TestLogger<UserManager<TUser>>());
|
|
||||||
services.AddSingleton<ILogger<RoleManager<TRole>>>(new TestLogger<RoleManager<TRole>>());
|
|
||||||
services.AddIdentity<TUser, TRole>(options =>
|
services.AddIdentity<TUser, TRole>(options =>
|
||||||
{
|
{
|
||||||
options.Password.RequireDigit = false;
|
options.Password.RequireDigit = false;
|
||||||
|
|
@ -50,6 +45,16 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
.AddEntityFrameworkStores<TestDbContext>();
|
.AddEntityFrameworkStores<TestDbContext>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void SetupIdentityServices(IServiceCollection services, object context)
|
||||||
|
{
|
||||||
|
services.AddHttpContextAccessor();
|
||||||
|
services.AddSingleton((TestDbContext)context);
|
||||||
|
services.AddLogging();
|
||||||
|
services.AddSingleton<ILogger<UserManager<TUser>>>(new TestLogger<UserManager<TUser>>());
|
||||||
|
services.AddSingleton<ILogger<RoleManager<TRole>>>(new TestLogger<RoleManager<TRole>>());
|
||||||
|
SetupAddIdentity(services);
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool ShouldSkipDbTests()
|
protected override bool ShouldSkipDbTests()
|
||||||
{
|
{
|
||||||
return TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows;
|
return TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows;
|
||||||
|
|
@ -86,9 +91,11 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
|
|
||||||
protected override Expression<Func<TUser, bool>> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName);
|
protected override Expression<Func<TUser, bool>> UserNameStartsWithPredicate(string userName) => u => u.UserName.StartsWith(userName);
|
||||||
|
|
||||||
public TestDbContext CreateContext()
|
public virtual TestDbContext CreateContext()
|
||||||
{
|
{
|
||||||
var db = DbUtil.Create<TestDbContext>(_fixture.ConnectionString);
|
var services = new ServiceCollection();
|
||||||
|
SetupAddIdentity(services);
|
||||||
|
var db = DbUtil.Create<TestDbContext>(_fixture.ConnectionString, services);
|
||||||
db.Database.EnsureCreated();
|
db.Database.EnsureCreated();
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Identity.Test;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
|
{
|
||||||
|
public class ProtectedUserStoreTest : SqlStoreTestBase<IdentityUser, IdentityRole, string>
|
||||||
|
{
|
||||||
|
private DefaultKeyRing _keyRing = new DefaultKeyRing();
|
||||||
|
|
||||||
|
public ProtectedUserStoreTest(ScratchDatabaseFixture fixture)
|
||||||
|
: base(fixture)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
protected override void SetupAddIdentity(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddIdentity<IdentityUser, IdentityRole>(options =>
|
||||||
|
{
|
||||||
|
options.Stores.ProtectPersonalData = true;
|
||||||
|
options.Password.RequireDigit = false;
|
||||||
|
options.Password.RequireLowercase = false;
|
||||||
|
options.Password.RequireNonAlphanumeric = false;
|
||||||
|
options.Password.RequireUppercase = false;
|
||||||
|
options.User.AllowedUserNameCharacters = null;
|
||||||
|
})
|
||||||
|
.AddDefaultTokenProviders()
|
||||||
|
.AddEntityFrameworkStores<TestDbContext>()
|
||||||
|
.AddPersonalDataProtection<SillyEncryptor, DefaultKeyRing>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DefaultKeyRing : ILookupProtectorKeyRing
|
||||||
|
{
|
||||||
|
public static string Current = "Default";
|
||||||
|
public string this[string keyId] => keyId;
|
||||||
|
public string CurrentKeyId => Current;
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAllKeyIds()
|
||||||
|
{
|
||||||
|
return new string[] { "Default", "NewPad" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SillyEncryptor : ILookupProtector
|
||||||
|
{
|
||||||
|
private readonly ILookupProtectorKeyRing _keyRing;
|
||||||
|
|
||||||
|
public SillyEncryptor(ILookupProtectorKeyRing keyRing) => _keyRing = keyRing;
|
||||||
|
|
||||||
|
public string Unprotect(string keyId, string data)
|
||||||
|
{
|
||||||
|
var pad = _keyRing[keyId];
|
||||||
|
if (!data.StartsWith(pad))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Didn't find pad.");
|
||||||
|
}
|
||||||
|
return data.Substring(pad.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Protect(string keyId, string data)
|
||||||
|
=> _keyRing[keyId] + data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Task</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task CanRotateKeysAndStillFind()
|
||||||
|
{
|
||||||
|
if (ShouldSkipDbTests())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var manager = CreateManager();
|
||||||
|
var name = Guid.NewGuid().ToString();
|
||||||
|
var user = CreateTestUser(name);
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, "hao@hao.com"));
|
||||||
|
var newName = Guid.NewGuid().ToString();
|
||||||
|
Assert.Null(await manager.FindByNameAsync(newName));
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.SetPhoneNumberAsync(user, "123-456-7890"));
|
||||||
|
|
||||||
|
Assert.Equal(user, await manager.FindByEmailAsync("hao@hao.com"));
|
||||||
|
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.SetUserNameAsync(user, newName));
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user));
|
||||||
|
Assert.NotNull(await manager.FindByNameAsync(newName));
|
||||||
|
Assert.Null(await manager.FindByNameAsync(name));
|
||||||
|
DefaultKeyRing.Current = "NewPad";
|
||||||
|
Assert.NotNull(await manager.FindByNameAsync(newName));
|
||||||
|
Assert.Equal(user, await manager.FindByEmailAsync("hao@hao.com"));
|
||||||
|
Assert.Equal("123-456-7890", await manager.GetPhoneNumberAsync(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InkProtector : ILookupProtector
|
||||||
|
{
|
||||||
|
public InkProtector() { }
|
||||||
|
|
||||||
|
public string Unprotect(string keyId, string data)
|
||||||
|
=> "ink";
|
||||||
|
|
||||||
|
public string Protect(string keyId, string data)
|
||||||
|
=> "ink";
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomUser : IdentityUser
|
||||||
|
{
|
||||||
|
[ProtectedPersonalData]
|
||||||
|
public string PersonalData1 { get; set; }
|
||||||
|
public string NonPersonalData1 { get; set; }
|
||||||
|
[ProtectedPersonalData]
|
||||||
|
public string PersonalData2 { get; set; }
|
||||||
|
public string NonPersonalData2 { get; set; }
|
||||||
|
[PersonalData]
|
||||||
|
public string SafePersonalData { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool FindInk(DbConnection conn, string column, string id)
|
||||||
|
{
|
||||||
|
using (var command = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
command.CommandText = $"SELECT u.{column} FROM AspNetUsers u WHERE u.Id = '{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>
|
||||||
|
/// <returns>Task</returns>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public async Task CustomPersonalDataPropertiesAreProtected(bool protect)
|
||||||
|
{
|
||||||
|
if (ShouldSkipDbTests())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var scratch = new ScratchDatabaseFixture())
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection().AddLogging();
|
||||||
|
services.AddIdentity<CustomUser, IdentityRole>(options =>
|
||||||
|
{
|
||||||
|
options.Stores.ProtectPersonalData = protect;
|
||||||
|
})
|
||||||
|
.AddEntityFrameworkStores<IdentityDbContext<CustomUser>>()
|
||||||
|
.AddPersonalDataProtection<InkProtector, DefaultKeyRing>();
|
||||||
|
|
||||||
|
var dbOptions = new DbContextOptionsBuilder().UseSqlServer(scratch.ConnectionString)
|
||||||
|
.UseApplicationServiceProvider(services.BuildServiceProvider())
|
||||||
|
.Options;
|
||||||
|
var dbContext = new IdentityDbContext<CustomUser>(dbOptions);
|
||||||
|
services.AddSingleton(dbContext);
|
||||||
|
dbContext.Database.EnsureCreated();
|
||||||
|
|
||||||
|
var sp = services.BuildServiceProvider();
|
||||||
|
var manager = sp.GetService<UserManager<CustomUser>>();
|
||||||
|
|
||||||
|
var guid = Guid.NewGuid().ToString();
|
||||||
|
var user = new CustomUser();
|
||||||
|
user.Id = guid;
|
||||||
|
user.UserName = guid;
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
|
||||||
|
user.Email = "test@test.com";
|
||||||
|
user.PersonalData1 = "p1";
|
||||||
|
user.PersonalData2 = "p2";
|
||||||
|
user.NonPersonalData1 = "np1";
|
||||||
|
user.NonPersonalData2 = "np2";
|
||||||
|
user.SafePersonalData = "safe";
|
||||||
|
user.PhoneNumber = "12345678";
|
||||||
|
IdentityResultAssert.IsSuccess(await manager.UpdateAsync(user));
|
||||||
|
|
||||||
|
var conn = dbContext.Database.GetDbConnection();
|
||||||
|
conn.Open();
|
||||||
|
if (protect)
|
||||||
|
{
|
||||||
|
Assert.True(FindInk(conn, "PhoneNumber", guid));
|
||||||
|
Assert.True(FindInk(conn, "Email", guid));
|
||||||
|
Assert.True(FindInk(conn, "UserName", guid));
|
||||||
|
Assert.True(FindInk(conn, "PersonalData1", guid));
|
||||||
|
Assert.True(FindInk(conn, "PersonalData2", guid));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.False(FindInk(conn, "PhoneNumber", guid));
|
||||||
|
Assert.False(FindInk(conn, "Email", guid));
|
||||||
|
Assert.False(FindInk(conn, "UserName", guid));
|
||||||
|
Assert.False(FindInk(conn, "PersonalData1", guid));
|
||||||
|
Assert.False(FindInk(conn, "PersonalData2", guid));
|
||||||
|
|
||||||
|
}
|
||||||
|
Assert.False(FindInk(conn, "NonPersonalData1", guid));
|
||||||
|
Assert.False(FindInk(conn, "NonPersonalData2", guid));
|
||||||
|
Assert.False(FindInk(conn, "SafePersonalData", guid));
|
||||||
|
|
||||||
|
conn.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InvalidUser : IdentityUser
|
||||||
|
{
|
||||||
|
[ProtectedPersonalData]
|
||||||
|
public bool PersonalData1 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ProtectedPersonalDataThrowsOnNonString()
|
||||||
|
{
|
||||||
|
if (ShouldSkipDbTests())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var scratch = new ScratchDatabaseFixture())
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection().AddLogging();
|
||||||
|
services.AddIdentity<CustomUser, IdentityRole>(options =>
|
||||||
|
{
|
||||||
|
options.Stores.ProtectPersonalData = true;
|
||||||
|
})
|
||||||
|
.AddEntityFrameworkStores<IdentityDbContext<CustomUser>>()
|
||||||
|
.AddPersonalDataProtection<InkProtector, DefaultKeyRing>();
|
||||||
|
var dbOptions = new DbContextOptionsBuilder().UseSqlServer(scratch.ConnectionString)
|
||||||
|
.UseApplicationServiceProvider(services.BuildServiceProvider())
|
||||||
|
.Options;
|
||||||
|
var dbContext = new IdentityDbContext<InvalidUser>(dbOptions);
|
||||||
|
var e = Assert.Throws<InvalidOperationException>(() => dbContext.Database.EnsureCreated());
|
||||||
|
Assert.Equal("[ProtectedPersonalData] only works strings by default.", e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Skipped because encryption causes this to fail.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Task</returns>
|
||||||
|
[Fact]
|
||||||
|
public override Task CanFindUsersViaUserQuerable()
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,5 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
||||||
// This used to throw
|
// This used to throw
|
||||||
var builder = services.AddIdentity<IdentityUser<string>, IdentityRole<string>>().AddEntityFrameworkStores<TestDbContext>();
|
var builder = services.AddIdentity<IdentityUser<string>, IdentityRole<string>>().AddEntityFrameworkStores<TestDbContext>();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
||||||
public DefaultUIContext WithAuthenticatedUser() =>
|
public DefaultUIContext WithAuthenticatedUser() =>
|
||||||
new DefaultUIContext(this) { UserAuthenticated = true };
|
new DefaultUIContext(this) { UserAuthenticated = true };
|
||||||
|
|
||||||
|
public DefaultUIContext WithAnonymousUser() =>
|
||||||
|
new DefaultUIContext(this) { UserAuthenticated = false };
|
||||||
|
|
||||||
public DefaultUIContext WithSocialLoginEnabled() =>
|
public DefaultUIContext WithSocialLoginEnabled() =>
|
||||||
new DefaultUIContext(this) { ContosoLoginEnabled = true };
|
new DefaultUIContext(this) { ContosoLoginEnabled = true };
|
||||||
|
|
||||||
|
|
@ -49,6 +52,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
||||||
get => GetValue<bool>(nameof(UserAuthenticated));
|
get => GetValue<bool>(nameof(UserAuthenticated));
|
||||||
set => SetValue(nameof(UserAuthenticated), value);
|
set => SetValue(nameof(UserAuthenticated), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ExistingUser
|
public bool ExistingUser
|
||||||
{
|
{
|
||||||
get => GetValue<bool>(nameof(ExistingUser));
|
get => GetValue<bool>(nameof(ExistingUser));
|
||||||
|
|
|
||||||
|
|
@ -52,5 +52,42 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
||||||
var email = emails.SentEmails[1];
|
var email = emails.SentEmails[1];
|
||||||
await UserStories.ConfirmEmailAsync(email, client);
|
await UserStories.ConfirmEmailAsync(email, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanDownloadPersonalData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = ServerFactory.CreateDefaultClient();
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
|
||||||
|
var index = await UserStories.RegisterNewUserAsync(client, userName, password);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var jsonData = await UserStories.DownloadPersonalData(index, userName);
|
||||||
|
Assert.Contains($"\"Id\":\"", jsonData);
|
||||||
|
Assert.Contains($"\"UserName\":\"{userName}\"", jsonData);
|
||||||
|
Assert.Contains($"\"Email\":\"{userName}\"", jsonData);
|
||||||
|
Assert.Contains($"\"EmailConfirmed\":\"False\"", jsonData);
|
||||||
|
Assert.Contains($"\"PhoneNumber\":\"null\"", jsonData);
|
||||||
|
Assert.Contains($"\"PhoneNumberConfirmed\":\"False\"", jsonData);
|
||||||
|
Assert.Contains($"\"TwoFactorEnabled\":\"False\"", jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CanDeleteUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = ServerFactory.CreateDefaultClient();
|
||||||
|
|
||||||
|
var userName = $"{Guid.NewGuid()}@example.com";
|
||||||
|
var password = $"!Test.Password1$";
|
||||||
|
|
||||||
|
var index = await UserStories.RegisterNewUserAsync(client, userName, password);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await UserStories.DeleteUser(index, password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AngleSharp.Dom.Html;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
||||||
|
{
|
||||||
|
public class DeleteUser : DefaultUIPage
|
||||||
|
{
|
||||||
|
private readonly IHtmlFormElement _deleteForm;
|
||||||
|
|
||||||
|
public DeleteUser(HttpClient client, IHtmlDocument deleteUser, DefaultUIContext context)
|
||||||
|
: base(client, deleteUser, context)
|
||||||
|
{
|
||||||
|
_deleteForm = HtmlAssert.HasForm("#delete-user", deleteUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FunctionalTests.Index> Delete(string password)
|
||||||
|
{
|
||||||
|
var loggedIn = await SendDeleteForm(password);
|
||||||
|
|
||||||
|
var deleteLocation = ResponseAssert.IsRedirect(loggedIn);
|
||||||
|
Assert.Equal(Index.Path, deleteLocation.ToString());
|
||||||
|
var indexResponse = await Client.GetAsync(deleteLocation);
|
||||||
|
var index = await ResponseAssert.IsHtmlDocumentAsync(indexResponse);
|
||||||
|
return new FunctionalTests.Index(Client, index, Context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> SendDeleteForm(string password)
|
||||||
|
{
|
||||||
|
return await Client.SendAsync(_deleteForm, new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
["Input_Password"] = password
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
||||||
private readonly IHtmlAnchorElement _personalDataLink;
|
private readonly IHtmlAnchorElement _personalDataLink;
|
||||||
private readonly IHtmlFormElement _updateProfileForm;
|
private readonly IHtmlFormElement _updateProfileForm;
|
||||||
private readonly IHtmlElement _confirmEmailButton;
|
private readonly IHtmlElement _confirmEmailButton;
|
||||||
|
public static readonly string Path = "/";
|
||||||
|
|
||||||
public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
|
public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
|
||||||
: base(client, manage, context)
|
: base(client, manage, context)
|
||||||
|
|
@ -53,5 +54,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
||||||
|
|
||||||
return new Index(Client, manage, Context);
|
return new Index(Client, manage, Context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PersonalData> ClickPersonalDataLinkAsync()
|
||||||
|
{
|
||||||
|
var goToPersonalData = await Client.GetAsync(_personalDataLink.Href);
|
||||||
|
var personalData = await ResponseAssert.IsHtmlDocumentAsync(goToPersonalData);
|
||||||
|
return new PersonalData(Client, personalData, Context);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AngleSharp.Dom.Html;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
||||||
|
{
|
||||||
|
public class PersonalData : DefaultUIPage
|
||||||
|
{
|
||||||
|
private readonly IHtmlAnchorElement _deleteLink;
|
||||||
|
private readonly IHtmlFormElement _downloadForm;
|
||||||
|
|
||||||
|
public PersonalData(HttpClient client, IHtmlDocument personalData, DefaultUIContext context)
|
||||||
|
: base(client, personalData, context)
|
||||||
|
{
|
||||||
|
_deleteLink = HtmlAssert.HasLink("#delete", personalData);
|
||||||
|
_downloadForm = HtmlAssert.HasForm("#download-data", personalData);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<DeleteUser> ClickDeleteLinkAsync()
|
||||||
|
{
|
||||||
|
var goToDelete = await Client.GetAsync(_deleteLink.Href);
|
||||||
|
var delete = await ResponseAssert.IsHtmlDocumentAsync(goToDelete);
|
||||||
|
return new DeleteUser(Client, delete, Context.WithAnonymousUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<HttpResponseMessage> SubmitDownloadForm()
|
||||||
|
{
|
||||||
|
return await Client.SendAsync(_downloadForm, new Dictionary<string, string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -135,5 +135,21 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
||||||
var resetPassword = await ResetPassword.CreateAsync(link, client, new DefaultUIContext().WithExistingUser());
|
var resetPassword = await ResetPassword.CreateAsync(link, client, new DefaultUIContext().WithExistingUser());
|
||||||
return await resetPassword.SendNewPasswordAsync(email, newPassword);
|
return await resetPassword.SendNewPasswordAsync(email, newPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static async Task<Index> DeleteUser(Index index, string password)
|
||||||
|
{
|
||||||
|
var manage = await index.ClickManageLinkAsync();
|
||||||
|
var personalData = await manage.ClickPersonalDataLinkAsync();
|
||||||
|
var deleteUser = await personalData.ClickDeleteLinkAsync();
|
||||||
|
return await deleteUser.Delete(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<string> DownloadPersonalData(Index index, string userName)
|
||||||
|
{
|
||||||
|
var manage = await index.ClickManageLinkAsync();
|
||||||
|
var personalData = await manage.ClickPersonalDataLinkAsync();
|
||||||
|
var download = await personalData.SubmitDownloadForm();
|
||||||
|
return await download.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -698,6 +698,86 @@ namespace Microsoft.AspNetCore.Identity.Test
|
||||||
Assert.ThrowsAsync<NotImplementedException>(() => manager.GenerateUserTokenAsync(new TestUser(), "A", "purpose"));
|
Assert.ThrowsAsync<NotImplementedException>(() => manager.GenerateUserTokenAsync(new TestUser(), "A", "purpose"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UserManagerThrowsIfStoreDoesNotSupportProtection()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection()
|
||||||
|
.AddLogging();
|
||||||
|
services.AddIdentity<TestUser, TestRole>(o => o.Stores.ProtectPersonalData = true)
|
||||||
|
.AddUserStore<NoopUserStore>();
|
||||||
|
var e = Assert.Throws<InvalidOperationException>(() => services.BuildServiceProvider().GetService<UserManager<TestUser>>());
|
||||||
|
Assert.Contains("Store does not implement IProtectedUserStore", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UserManagerThrowsIfMissingPersonalDataProtection()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection()
|
||||||
|
.AddLogging();
|
||||||
|
services.AddIdentity<TestUser, TestRole>(o => o.Stores.ProtectPersonalData = true)
|
||||||
|
.AddUserStore<ProtectedStore>();
|
||||||
|
var e = Assert.Throws<InvalidOperationException>(() => services.BuildServiceProvider().GetService<UserManager<TestUser>>());
|
||||||
|
Assert.Contains("No IPersonalDataProtector service was registered", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProtectedStore : IProtectedUserStore<TestUser>
|
||||||
|
{
|
||||||
|
public Task<IdentityResult> CreateAsync(TestUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IdentityResult> DeleteAsync(TestUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TestUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TestUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetNormalizedUserNameAsync(TestUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetUserIdAsync(TestUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetUserNameAsync(TestUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetNormalizedUserNameAsync(TestUser user, string normalizedName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetUserNameAsync(TestUser user, string userName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IdentityResult> UpdateAsync(TestUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UserManagerWillUseTokenProviderInstanceOverDefaults()
|
public void UserManagerWillUseTokenProviderInstanceOverDefaults()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue