ProtectPersonalData + extensiblity support (#1562)
This commit is contained in:
parent
47d610b0cc
commit
9ecbefcf21
|
|
@ -1,14 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace IdentitySample.DefaultUI.Data
|
||||
{
|
||||
public class ApplicationUser : IdentityUser
|
||||
{
|
||||
[ProtectedPersonalData]
|
||||
public string Name { get; set; }
|
||||
[PersonalData]
|
||||
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));
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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()
|
||||
=> 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)
|
||||
{
|
||||
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>
|
||||
<comment>Error when the IdentityBuilder.RoleType was not specified</comment>
|
||||
</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>
|
||||
|
|
@ -13,5 +13,11 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// properties used as keys, i.e. UserId, LoginProvider, ProviderKey.
|
||||
/// </summary>
|
||||
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 bool _disposed;
|
||||
private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create();
|
||||
|
||||
private IServiceProvider _services;
|
||||
|
||||
/// <summary>
|
||||
/// The cancellation token used to cancel operations.
|
||||
|
|
@ -99,6 +99,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
}
|
||||
}
|
||||
|
||||
_services = services;
|
||||
if (services != null)
|
||||
{
|
||||
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>
|
||||
|
|
@ -527,7 +540,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// <returns>
|
||||
/// The <see cref="Task"/> that represents the asynchronous operation, containing the user matching the specified <paramref name="userName"/> if it exists.
|
||||
/// </returns>
|
||||
public virtual Task<TUser> FindByNameAsync(string userName)
|
||||
public virtual async Task<TUser> FindByNameAsync(string userName)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (userName == null)
|
||||
|
|
@ -535,7 +548,28 @@ namespace Microsoft.AspNetCore.Identity
|
|||
throw new ArgumentNullException(nameof(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>
|
||||
|
|
@ -578,6 +612,17 @@ namespace Microsoft.AspNetCore.Identity
|
|||
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>
|
||||
/// Updates the normalized user name for the specified <paramref name="user"/>.
|
||||
/// </summary>
|
||||
|
|
@ -586,6 +631,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
public virtual async Task UpdateNormalizedUserNameAsync(TUser user)
|
||||
{
|
||||
var normalizedName = NormalizeKey(await GetUserNameAsync(user));
|
||||
normalizedName = ProtectPersonalData(normalizedName);
|
||||
await Store.SetNormalizedUserNameAsync(user, normalizedName, CancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -1352,7 +1398,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// <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.
|
||||
/// </returns>
|
||||
public virtual Task<TUser> FindByEmailAsync(string email)
|
||||
public virtual async Task<TUser> FindByEmailAsync(string email)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
var store = GetEmailStore();
|
||||
|
|
@ -1360,7 +1406,29 @@ namespace Microsoft.AspNetCore.Identity
|
|||
{
|
||||
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>
|
||||
|
|
@ -1374,7 +1442,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
if (store != null)
|
||||
{
|
||||
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.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Converters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
|
@ -96,16 +96,16 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
|||
/// </summary>
|
||||
public DbSet<TUserToken> UserTokens { get; set; }
|
||||
|
||||
private int GetMaxLengthForKeys()
|
||||
{
|
||||
// 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>()
|
||||
private StoreOptions GetStoreOptions() => this.GetService<IDbContextOptions>()
|
||||
.Extensions.OfType<CoreOptionsExtension>()
|
||||
.FirstOrDefault()?.ApplicationServiceProvider
|
||||
?.GetService<IOptions<IdentityOptions>>()
|
||||
?.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>
|
||||
|
|
@ -116,7 +116,9 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
|||
/// </param>
|
||||
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 =>
|
||||
{
|
||||
|
|
@ -131,6 +133,21 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
|||
b.Property(u => u.Email).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<TUserLogin>().WithOne().HasForeignKey(ul => ul.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
|
||||
= 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>
|
||||
/// AddEntityFrameworkStores can only be called with a role that derives from IdentityRole<TKey>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -117,6 +117,10 @@
|
|||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore
|
|||
IUserTwoFactorStore<TUser>,
|
||||
IUserAuthenticationTokenStore<TUser>,
|
||||
IUserAuthenticatorKeyStore<TUser>,
|
||||
IUserTwoFactorRecoveryCodeStore<TUser>
|
||||
IUserTwoFactorRecoveryCodeStore<TUser>,
|
||||
IProtectedUserStore<TUser>
|
||||
where TUser : IdentityUser<TKey>
|
||||
where TContext : DbContext
|
||||
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="TRoleClaim">The type representing a role claim.</typeparam>
|
||||
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 TRole : IdentityRole<TKey>
|
||||
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>
|
||||
/// <returns>Task</returns>
|
||||
[Fact]
|
||||
public async Task CanFindUsersViaUserQuerable()
|
||||
public async virtual Task CanFindUsersViaUserQuerable()
|
||||
{
|
||||
if (ShouldSkipDbTests())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mgr = CreateManager();
|
||||
if (mgr.SupportsQueryableUsers)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -57,11 +57,13 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// <summary>
|
||||
/// Gets or sets the primary key for this user.
|
||||
/// </summary>
|
||||
[PersonalData]
|
||||
public virtual TKey Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user name for this user.
|
||||
/// </summary>
|
||||
[ProtectedPersonalData]
|
||||
public virtual string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -72,6 +74,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// <summary>
|
||||
/// Gets or sets the email address for this user.
|
||||
/// </summary>
|
||||
[ProtectedPersonalData]
|
||||
public virtual string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -83,6 +86,7 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// Gets or sets a flag indicating if a user has confirmed their email address.
|
||||
/// </summary>
|
||||
/// <value>True if the email address has been confirmed, otherwise false.</value>
|
||||
[PersonalData]
|
||||
public virtual bool EmailConfirmed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -103,18 +107,21 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// <summary>
|
||||
/// Gets or sets a telephone number for the user.
|
||||
/// </summary>
|
||||
[ProtectedPersonalData]
|
||||
public virtual string PhoneNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a flag indicating if a user has confirmed their telephone address.
|
||||
/// </summary>
|
||||
/// <value>True if the telephone number has been confirmed, otherwise false.</value>
|
||||
[PersonalData]
|
||||
public virtual bool PhoneNumberConfirmed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a flag indicating if two factor authentication is enabled for this user.
|
||||
/// </summary>
|
||||
/// <value>True if 2fa is enabled, otherwise false.</value>
|
||||
[PersonalData]
|
||||
public virtual bool TwoFactorEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -140,8 +147,6 @@ namespace Microsoft.AspNetCore.Identity
|
|||
/// Returns the username for this user.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return UserName;
|
||||
}
|
||||
=> UserName;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</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>
|
||||
@if (Model.RequirePassword)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
|
@ -43,12 +44,13 @@ namespace Microsoft.AspNetCore.Identity.UI.Pages.Account.Manage.Internal
|
|||
|
||||
// Only include personal data for download
|
||||
var personalData = new Dictionary<string, string>();
|
||||
personalData.Add("UserId", await _userManager.GetUserIdAsync(user));
|
||||
personalData.Add("UserName", await _userManager.GetUserNameAsync(user));
|
||||
personalData.Add("Email", await _userManager.GetEmailAsync(user));
|
||||
personalData.Add("EmailConfirmed", (await _userManager.IsEmailConfirmedAsync(user)).ToString());
|
||||
personalData.Add("PhoneNumber", await _userManager.GetPhoneNumberAsync(user));
|
||||
personalData.Add("PhoneNumberConfirmed", (await _userManager.IsPhoneNumberConfirmedAsync(user)).ToString());
|
||||
|
||||
var personalDataProps = typeof(TUser).GetProperties().Where(
|
||||
prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute)));
|
||||
foreach (var p in personalDataProps)
|
||||
{
|
||||
personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null");
|
||||
}
|
||||
|
||||
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json");
|
||||
return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@
|
|||
<p>
|
||||
<strong>Deleting this data will permanently remove your account, and this cannot be recovered.</strong>
|
||||
</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>
|
||||
</form>
|
||||
<p>
|
||||
<a asp-page="DeletePersonalData" class="btn btn-default">Delete</a>
|
||||
<a id="delete" asp-page="DeletePersonalData" class="btn btn-default">Delete</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -25,9 +26,9 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
|||
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>();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,20 +24,15 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
|||
where TRole : IdentityRole<TKey>, new()
|
||||
where TKey : IEquatable<TKey>
|
||||
{
|
||||
private readonly ScratchDatabaseFixture _fixture;
|
||||
protected readonly ScratchDatabaseFixture _fixture;
|
||||
|
||||
protected SqlStoreTestBase(ScratchDatabaseFixture 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 =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
|
|
@ -50,6 +45,16 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
|
|||
.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()
|
||||
{
|
||||
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);
|
||||
|
||||
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();
|
||||
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
|
||||
var builder = services.AddIdentity<IdentityUser<string>, IdentityRole<string>>().AddEntityFrameworkStores<TestDbContext>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
public DefaultUIContext WithAuthenticatedUser() =>
|
||||
new DefaultUIContext(this) { UserAuthenticated = true };
|
||||
|
||||
public DefaultUIContext WithAnonymousUser() =>
|
||||
new DefaultUIContext(this) { UserAuthenticated = false };
|
||||
|
||||
public DefaultUIContext WithSocialLoginEnabled() =>
|
||||
new DefaultUIContext(this) { ContosoLoginEnabled = true };
|
||||
|
||||
|
|
@ -49,6 +52,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
get => GetValue<bool>(nameof(UserAuthenticated));
|
||||
set => SetValue(nameof(UserAuthenticated), value);
|
||||
}
|
||||
|
||||
public bool ExistingUser
|
||||
{
|
||||
get => GetValue<bool>(nameof(ExistingUser));
|
||||
|
|
|
|||
|
|
@ -52,5 +52,42 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
var email = emails.SentEmails[1];
|
||||
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 IHtmlFormElement _updateProfileForm;
|
||||
private readonly IHtmlElement _confirmEmailButton;
|
||||
public static readonly string Path = "/";
|
||||
|
||||
public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
|
||||
: base(client, manage, context)
|
||||
|
|
@ -53,5 +54,13 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
|||
|
||||
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());
|
||||
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"));
|
||||
}
|
||||
|
||||
[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]
|
||||
public void UserManagerWillUseTokenProviderInstanceOverDefaults()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue