ProtectPersonalData + extensiblity support (#1562)

This commit is contained in:
Hao Kung 2018-03-08 12:13:34 -08:00 committed by GitHub
parent 47d610b0cc
commit 9ecbefcf21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1045 additions and 192 deletions

View File

@ -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; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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
{ }
}

View File

@ -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"/>.

View File

@ -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
{ }
}

View File

@ -682,6 +682,34 @@ namespace Microsoft.Extensions.Identity.Core
internal static string FormatNoRoleType()
=> GetString("NoRoleType");
/// <summary>
/// Store does not implement IProtectedUserStore&lt;TUser&gt; which is required when ProtectPersonalData = true.
/// </summary>
internal static string StoreNotIProtectedUserStore
{
get => GetString("StoreNotIProtectedUserStore");
}
/// <summary>
/// Store does not implement IProtectedUserStore&lt;TUser&gt; 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);

View File

@ -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
{ }
}

View File

@ -309,4 +309,12 @@
<value>No RoleType was specified, try AddRoles&lt;TRole&gt;().</value>
<comment>Error when the IdentityBuilder.RoleType was not specified</comment>
</data>
<data name="StoreNotIProtectedUserStore" xml:space="preserve">
<value>Store does not implement IProtectedUserStore&lt;TUser&gt; 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>

View File

@ -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; }
}
}

View File

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

View File

@ -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();

View File

@ -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&lt;TKey&gt;.
/// </summary>

View File

@ -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&lt;TKey&gt;.</value>
<comment>error when the role does not derive from IdentityRole</comment>

View File

@ -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>

View File

@ -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

View File

@ -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;
}
}
}

View File

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

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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)
{

View File

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

View File

@ -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>

View File

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

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -46,6 +46,5 @@ namespace Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test
// This used to throw
var builder = services.AddIdentity<IdentityUser<string>, IdentityRole<string>>().AddEntityFrameworkStores<TestDbContext>();
}
}
}

View File

@ -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));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
{