Skip to content

Commit 555b506

Browse files
authored
Identity: Add new email/confirmation flows (#8577)
1 parent 8274776 commit 555b506

File tree

59 files changed

+1465
-380
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1465
-380
lines changed

src/Identity/Core/ref/Microsoft.AspNetCore.Identity.netcoreapp3.0.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public SecurityStampValidator(Microsoft.Extensions.Options.IOptions<Microsoft.As
106106
}
107107
public partial class SignInManager<TUser> where TUser : class
108108
{
109-
public SignInManager(Microsoft.AspNetCore.Identity.UserManager<TUser> userManager, Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<TUser> claimsFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<TUser>> logger, Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes) { }
109+
public SignInManager(Microsoft.AspNetCore.Identity.UserManager<TUser> userManager, Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor, Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<TUser> claimsFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor, Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<TUser>> logger, Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes, Microsoft.AspNetCore.Identity.IUserConfirmation<TUser> confirmation) { }
110110
public Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<TUser> ClaimsFactory { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
111111
public Microsoft.AspNetCore.Http.HttpContext Context { get { throw null; } set { } }
112112
public virtual Microsoft.Extensions.Logging.ILogger Logger { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

src/Identity/Core/src/IdentityServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public static IdentityBuilder AddIdentity<TUser, TRole>(
8888
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
8989
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
9090
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
91+
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
9192
services.TryAddScoped<UserManager<TUser>>();
9293
services.TryAddScoped<SignInManager<TUser>>();
9394
services.TryAddScoped<RoleManager<TRole>>();

src/Identity/Core/src/SignInManager.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ public class SignInManager<TUser> where TUser : class
3131
/// <param name="optionsAccessor">The accessor used to access the <see cref="IdentityOptions"/>.</param>
3232
/// <param name="logger">The logger used to log messages, warnings and errors.</param>
3333
/// <param name="schemes">The scheme provider that is used enumerate the authentication schemes.</param>
34+
/// <param name="confirmation">The <see cref="IUserConfirmation{TUser}"/> used check whether a user account is confirmed.</param>
3435
public SignInManager(UserManager<TUser> userManager,
3536
IHttpContextAccessor contextAccessor,
3637
IUserClaimsPrincipalFactory<TUser> claimsFactory,
3738
IOptions<IdentityOptions> optionsAccessor,
3839
ILogger<SignInManager<TUser>> logger,
39-
IAuthenticationSchemeProvider schemes)
40+
IAuthenticationSchemeProvider schemes,
41+
IUserConfirmation<TUser> confirmation)
4042
{
4143
if (userManager == null)
4244
{
@@ -57,11 +59,13 @@ public SignInManager(UserManager<TUser> userManager,
5759
Options = optionsAccessor?.Value ?? new IdentityOptions();
5860
Logger = logger;
5961
_schemes = schemes;
62+
_confirmation = confirmation;
6063
}
6164

6265
private readonly IHttpContextAccessor _contextAccessor;
6366
private HttpContext _context;
6467
private IAuthenticationSchemeProvider _schemes;
68+
private IUserConfirmation<TUser> _confirmation;
6569

6670
/// <summary>
6771
/// Gets the <see cref="ILogger"/> used to log messages from the manager.
@@ -148,7 +152,11 @@ public virtual async Task<bool> CanSignInAsync(TUser user)
148152
Logger.LogWarning(1, "User {userId} cannot sign in without a confirmed phone number.", await UserManager.GetUserIdAsync(user));
149153
return false;
150154
}
151-
155+
if (Options.SignIn.RequireConfirmedAccount && !(await _confirmation.IsConfirmedAsync(UserManager, user)))
156+
{
157+
Logger.LogWarning(4, "User {userId} cannot sign in without a confirmed account.", await UserManager.GetUserIdAsync(user));
158+
return false;
159+
}
152160
return true;
153161
}
154162

src/Identity/Extensions.Core/ref/Microsoft.Extensions.Identity.Core.netcoreapp3.0.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public DefaultPersonalDataProtector(Microsoft.AspNetCore.Identity.ILookupProtect
2626
public virtual string Protect(string data) { throw null; }
2727
public virtual string Unprotect(string data) { throw null; }
2828
}
29+
public partial class DefaultUserConfirmation<TUser> : Microsoft.AspNetCore.Identity.IUserConfirmation<TUser> where TUser : class
30+
{
31+
public DefaultUserConfirmation() { }
32+
[System.Diagnostics.DebuggerStepThroughAttribute]
33+
public virtual System.Threading.Tasks.Task<bool> IsConfirmedAsync(Microsoft.AspNetCore.Identity.UserManager<TUser> manager, TUser user) { throw null; }
34+
}
2935
public partial class EmailTokenProvider<TUser> : Microsoft.AspNetCore.Identity.TotpSecurityStampBasedTokenProvider<TUser> where TUser : class
3036
{
3137
public EmailTokenProvider() { }
@@ -194,6 +200,10 @@ public partial interface IUserClaimStore<TUser> : Microsoft.AspNetCore.Identity.
194200
System.Threading.Tasks.Task RemoveClaimsAsync(TUser user, System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> claims, System.Threading.CancellationToken cancellationToken);
195201
System.Threading.Tasks.Task ReplaceClaimAsync(TUser user, System.Security.Claims.Claim claim, System.Security.Claims.Claim newClaim, System.Threading.CancellationToken cancellationToken);
196202
}
203+
public partial interface IUserConfirmation<TUser> where TUser : class
204+
{
205+
System.Threading.Tasks.Task<bool> IsConfirmedAsync(Microsoft.AspNetCore.Identity.UserManager<TUser> manager, TUser user);
206+
}
197207
public partial interface IUserEmailStore<TUser> : Microsoft.AspNetCore.Identity.IUserStore<TUser>, System.IDisposable where TUser : class
198208
{
199209
System.Threading.Tasks.Task<TUser> FindByEmailAsync(string normalizedEmail, System.Threading.CancellationToken cancellationToken);
@@ -396,6 +406,7 @@ public RoleValidator(Microsoft.AspNetCore.Identity.IdentityErrorDescriber errors
396406
public partial class SignInOptions
397407
{
398408
public SignInOptions() { }
409+
public bool RequireConfirmedAccount { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
399410
public bool RequireConfirmedEmail { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
400411
public bool RequireConfirmedPhoneNumber { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
401412
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Identity
7+
{
8+
/// <summary>
9+
/// Default implementation of <see cref="IUserConfirmation{TUser}"/>.
10+
/// </summary>
11+
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
12+
public class DefaultUserConfirmation<TUser> : IUserConfirmation<TUser> where TUser : class
13+
{
14+
/// <summary>
15+
/// Determines whether the specified <paramref name="user"/> is confirmed.
16+
/// </summary>
17+
/// <param name="manager">The <see cref="UserManager{TUser}"/> that can be used to retrieve user properties.</param>
18+
/// <param name="user">The user.</param>
19+
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the <see cref="IdentityResult"/> of the confirmation operation.</returns>
20+
public async virtual Task<bool> IsConfirmedAsync(UserManager<TUser> manager, TUser user)
21+
{
22+
if (!await manager.IsEmailConfirmedAsync(user))
23+
{
24+
return false;
25+
}
26+
return true;
27+
}
28+
}
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Identity
7+
{
8+
/// <summary>
9+
/// Provides an abstraction for confirmation of user accounts.
10+
/// </summary>
11+
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
12+
public interface IUserConfirmation<TUser> where TUser : class
13+
{
14+
/// <summary>
15+
/// Determines whether the specified <paramref name="user"/> is confirmed.
16+
/// </summary>
17+
/// <param name="manager">The <see cref="UserManager{TUser}"/> that can be used to retrieve user properties.</param>
18+
/// <param name="user">The user.</param>
19+
/// <returns>Whether the user is confirmed.</returns>
20+
Task<bool> IsConfirmedAsync(UserManager<TUser> manager, TUser user);
21+
}
22+
}

src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection ser
4141
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
4242
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
4343
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
44+
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
4445
// No interface for the error describer so we can add errors without rev'ing the interface
4546
services.TryAddScoped<IdentityErrorDescriber>();
4647
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();

src/Identity/Extensions.Core/src/SignInOptions.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,11 @@ public class SignInOptions
1919
/// </summary>
2020
/// <value>True if a user must have a confirmed telephone number before they can sign in, otherwise false.</value>
2121
public bool RequireConfirmedPhoneNumber { get; set; }
22+
23+
/// <summary>
24+
/// Gets or sets a flag indicating whether a confirmed <see cref="IUserConfirmation{TUser}"/> account is required to sign in. Defaults to false.
25+
/// </summary>
26+
/// <value>True if a user must have a confirmed account before they can sign in, otherwise false.</value>
27+
public bool RequireConfirmedAccount { get; set; }
2228
}
23-
}
29+
}

src/Identity/Specification.Tests/src/UserManagerSpecificationTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,34 @@ public async Task CanChangeEmail()
17101710
Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user));
17111711
}
17121712

1713+
/// <summary>
1714+
/// Test.
1715+
/// </summary>
1716+
/// <returns>Task</returns>
1717+
[Fact]
1718+
public async Task CanChangeEmailOnlyIfEmailSame()
1719+
{
1720+
if (ShouldSkipDbTests())
1721+
{
1722+
return;
1723+
}
1724+
var manager = CreateManager();
1725+
var user = CreateTestUser("foouser");
1726+
IdentityResultAssert.IsSuccess(await manager.CreateAsync(user));
1727+
var email = await manager.GetUserNameAsync(user) + "@diddly.bop";
1728+
IdentityResultAssert.IsSuccess(await manager.SetEmailAsync(user, email));
1729+
Assert.False(await manager.IsEmailConfirmedAsync(user));
1730+
var stamp = await manager.GetSecurityStampAsync(user);
1731+
var newEmail = await manager.GetUserNameAsync(user) + "@en.vec";
1732+
var token1 = await manager.GenerateChangeEmailTokenAsync(user, newEmail);
1733+
var token2 = await manager.GenerateChangeEmailTokenAsync(user, "[email protected]");
1734+
IdentityResultAssert.IsSuccess(await manager.ChangeEmailAsync(user, newEmail, token1));
1735+
Assert.True(await manager.IsEmailConfirmedAsync(user));
1736+
Assert.Equal(await manager.GetEmailAsync(user), newEmail);
1737+
Assert.NotEqual(stamp, await manager.GetSecurityStampAsync(user));
1738+
IdentityResultAssert.IsFailure(await manager.ChangeEmailAsync(user, "[email protected]", token2));
1739+
}
1740+
17131741
/// <summary>
17141742
/// Test.
17151743
/// </summary>

0 commit comments

Comments
 (0)