Skip to content

Commit 23904f4

Browse files
authored
VCST-4570: Error handling for duplicated emails in DB (#2981)
1 parent 1905cff commit 23904f4

File tree

5 files changed

+98
-13
lines changed

5 files changed

+98
-13
lines changed

src/VirtoCommerce.Platform.Security/CustomUserManager.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using VirtoCommerce.Platform.Core.Security;
1313
using VirtoCommerce.Platform.Core.Security.Events;
1414
using VirtoCommerce.Platform.Security.Caching;
15+
using VirtoCommerce.Platform.Security.Exceptions;
1516
using VirtoCommerce.Platform.Security.Model;
1617
using VirtoCommerce.Platform.Security.Repositories;
1718

@@ -63,7 +64,17 @@ public override Task<ApplicationUser> FindByEmailAsync(string email)
6364
var cacheKey = CacheKey.With(GetType(), nameof(FindByEmailAsync), email);
6465
return _memoryCache.GetOrCreateExclusiveAsync(cacheKey, async cacheEntry =>
6566
{
66-
var user = await base.FindByEmailAsync(email);
67+
ApplicationUser user;
68+
69+
try
70+
{
71+
user = await base.FindByEmailAsync(email);
72+
}
73+
catch (InvalidOperationException ex) when (ex.StackTrace?.Contains("SingleOrDefault") == true)
74+
{
75+
throw new DuplicateEmailException(email, ex);
76+
}
77+
6778
if (user is not null)
6879
{
6980
await LoadUserDetailsAsync(user);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using VirtoCommerce.Platform.Core.Exceptions;
3+
4+
namespace VirtoCommerce.Platform.Security.Exceptions
5+
{
6+
public class DuplicateEmailException : PlatformException
7+
{
8+
public DuplicateEmailException(string email)
9+
: base($"Multiple accounts are associated with the email '{email}'.")
10+
{
11+
}
12+
13+
public DuplicateEmailException(string email, Exception innerException)
14+
: base($"Multiple accounts are associated with the email '{email}'.", innerException)
15+
{
16+
}
17+
}
18+
}

src/VirtoCommerce.Platform.Security/OpenIddict/SecurityErrorDescriber.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,12 @@ public static class SecurityErrorDescriber
6767
Code = nameof(UnsupportedGrantType).ToSnakeCase(),
6868
ErrorDescription = "The specified grant type is not supported."
6969
};
70+
71+
public static TokenResponse DuplicateEmailLoginAttempt() => new()
72+
{
73+
Error = Errors.InvalidGrant,
74+
Code = nameof(DuplicateEmailLoginAttempt).ToSnakeCase(),
75+
ErrorDescription = "Multiple accounts are associated with this email. Please use your username to login."
76+
};
7077
}
7178
}

src/VirtoCommerce.Platform.Web/Controllers/Api/AuthorizationController.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using VirtoCommerce.Platform.Core.Security.Events;
2121
using VirtoCommerce.Platform.Core.Security.ExternalSignIn;
2222
using VirtoCommerce.Platform.Security.Authorization;
23+
using VirtoCommerce.Platform.Security.Exceptions;
2324
using VirtoCommerce.Platform.Security.Extensions;
2425
using VirtoCommerce.Platform.Security.Model.OpenIddict;
2526
using VirtoCommerce.Platform.Security.OpenIddict;
@@ -134,10 +135,18 @@ public async Task<ActionResult> Exchange()
134135

135136
var user = await _userManager.FindByNameAsync(openIdConnectRequest.Username);
136137

137-
// Allows signin to back office by either username (login) or email if IdentityOptions.User.RequireUniqueEmail is True.
138+
// Allows signin to back office by either username (login) or email if IdentityOptions.User.RequireUniqueEmail is True.
138139
if (user is null && _identityOptions.User.RequireUniqueEmail)
139140
{
140-
user = await _userManager.FindByEmailAsync(openIdConnectRequest.Username);
141+
try
142+
{
143+
user = await _userManager.FindByEmailAsync(openIdConnectRequest.Username);
144+
}
145+
catch (DuplicateEmailException)
146+
{
147+
await delayedResponse.FailAsync();
148+
return BadRequest(SecurityErrorDescriber.DuplicateEmailLoginAttempt());
149+
}
141150
}
142151

143152
if (user is null)
@@ -153,7 +162,16 @@ public async Task<ActionResult> Exchange()
153162
}
154163

155164
// Validate the username/password parameters and ensure the account is not locked out.
156-
context.SignInResult = await _signInManager.CheckPasswordSignInAsync(user, openIdConnectRequest.Password, lockoutOnFailure: true);
165+
try
166+
{
167+
context.SignInResult = await _signInManager.CheckPasswordSignInAsync(user, openIdConnectRequest.Password, lockoutOnFailure: true);
168+
}
169+
catch (DuplicateEmailException)
170+
{
171+
await delayedResponse.FailAsync();
172+
return BadRequest(SecurityErrorDescriber.DuplicateEmailLoginAttempt());
173+
}
174+
157175
context.User = user.CloneTyped();
158176

159177
foreach (var requestValidator in _requestValidators)
@@ -172,7 +190,16 @@ public async Task<ActionResult> Exchange()
172190
// Create a new authentication ticket.
173191
var ticket = await CreateTicketAsync(user, context);
174192

175-
await SetLastLoginDate(user);
193+
try
194+
{
195+
await SetLastLoginDate(user);
196+
}
197+
catch (DuplicateEmailException)
198+
{
199+
await delayedResponse.FailAsync();
200+
return BadRequest(SecurityErrorDescriber.DuplicateEmailLoginAttempt());
201+
}
202+
176203
await _eventPublisher.Publish(new UserLoginEvent(user));
177204

178205
await delayedResponse.SucceedAsync();

src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using VirtoCommerce.Platform.Core.Security;
2020
using VirtoCommerce.Platform.Core.Security.Events;
2121
using VirtoCommerce.Platform.Core.Security.Search;
22+
using VirtoCommerce.Platform.Security.Exceptions;
2223
using VirtoCommerce.Platform.Security.Extensions;
2324
using VirtoCommerce.Platform.Security.ExternalSignIn;
2425
using VirtoCommerce.Platform.Web.Model.Security;
@@ -137,10 +138,18 @@ public async Task<ActionResult<SignInResult>> Login([FromBody] LoginRequest requ
137138

138139
var user = await UserManager.FindByNameAsync(request.UserName);
139140

140-
// Allows signin to back office by either username (login) or email if IdentityOptions.User.RequireUniqueEmail is True.
141+
// Allows signin to back office by either username (login) or email if IdentityOptions.User.RequireUniqueEmail is True.
141142
if (user == null && _identityOptions.User.RequireUniqueEmail)
142143
{
143-
user = await UserManager.FindByEmailAsync(request.UserName);
144+
try
145+
{
146+
user = await UserManager.FindByEmailAsync(request.UserName);
147+
}
148+
catch (DuplicateEmailException)
149+
{
150+
await delayedResponse.FailAsync();
151+
return Ok(SignInResult.Failed);
152+
}
144153
}
145154

146155
if (user == null)
@@ -417,11 +426,16 @@ public async Task<ActionResult<ApplicationUser>> GetUserById([FromRoute] string
417426
[Authorize(PlatformPermissions.SecurityQuery)]
418427
public async Task<ActionResult<ApplicationUser>> GetUserByEmail([FromRoute] string email)
419428
{
420-
var result = await UserManager.FindByEmailAsync(email);
421-
422-
result = ReduceUserDetails(result);
423-
424-
return Ok(result);
429+
try
430+
{
431+
var result = await UserManager.FindByEmailAsync(email);
432+
result = ReduceUserDetails(result);
433+
return Ok(result);
434+
}
435+
catch (DuplicateEmailException ex)
436+
{
437+
return BadRequest(new { message = ex.Message });
438+
}
425439
}
426440

427441
/// <summary>
@@ -664,7 +678,15 @@ public async Task<ActionResult> RequestPasswordReset(string loginOrEmail)
664678

665679
if (user == null && _identityOptions.User.RequireUniqueEmail)
666680
{
667-
user = await UserManager.FindByEmailAsync(loginOrEmail);
681+
try
682+
{
683+
user = await UserManager.FindByEmailAsync(loginOrEmail);
684+
}
685+
catch (DuplicateEmailException)
686+
{
687+
await delayedResponse.FailAsync();
688+
return Ok();
689+
}
668690
}
669691

670692
// Return 200 to prevent potential user name/email harvesting

0 commit comments

Comments
 (0)