diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json index 87de63259f..31a8ebad04 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/en.json @@ -9,6 +9,7 @@ "InvalidUserNameOrPassword": "Invalid username or password!", "LoginIsNotAllowed": "You are not allowed to login! You need to confirm your email/phone number.", "InvalidUsername": "Invalid username or password!", + "InvalidAuthenticatorCode": "Invalid authenticator code!", "TheTargetUserIsNotLinkedToYou": "The target user is not linked to you!" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json index 8a90e54172..b5c072c7e6 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json @@ -8,6 +8,7 @@ "InvalidUserNameOrPassword": "Kullanıcı adı ya da şifre geçersiz!", "LoginIsNotAllowed": "Giriş yapamazsınız! E-posta adresinizi ya da telefon numaranızı doğrulamanız gerekiyor.", "InvalidUsername": "Kullanıcı adı ya da şifre geçersiz!", + "InvalidAuthenticatorCode": "Geçersiz kimlik doğrulama kodu!", "TheTargetUserIsNotLinkedToYou": "Hedef kullanıcı sizinle bağlantılı değil!" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json index 0c5779717d..7b90b802e7 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/zh-Hans.json @@ -9,6 +9,7 @@ "InvalidUserNameOrPassword": "用户名或密码错误!", "LoginIsNotAllowed": "无法登录!你需要验证邮箱地址/手机号.", "InvalidUsername": "用户名或密码错误!", + "InvalidAuthenticatorCode": "验证码无效!", "TheTargetUserIsNotLinkedToYou": "目标用户未和你有关联!" } } diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs index e024976548..4d7d049949 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Resources; using System.Security.Claims; using System.Threading.Tasks; using IdentityModel; @@ -63,115 +64,160 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity [UnitOfWork] public virtual async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { - var clientId = context.Request?.Client?.ClientId; - using var scope = ServiceScopeFactory.CreateScope(); - - await ReplaceEmailToUsernameOfInputIfNeeds(context); - - IdentityUser user = null; - - async Task SetSuccessResultAsync() + using (var scope = ServiceScopeFactory.CreateScope()) { - var sub = await UserManager.GetUserIdAsync(user); + await ReplaceEmailToUsernameOfInputIfNeeds(context); - Logger.LogInformation("Credentials validated for username: {username}", context.UserName); + IdentityUser user = null; - var additionalClaims = new List(); - - await AddCustomClaimsAsync(additionalClaims, user, context); + if (AbpIdentityOptions.ExternalLoginProviders.Any()) + { + foreach (var externalLoginProviderInfo in AbpIdentityOptions.ExternalLoginProviders.Values) + { + var externalLoginProvider = (IExternalLoginProvider) scope.ServiceProvider + .GetRequiredService(externalLoginProviderInfo.Type); - context.Result = new GrantValidationResult( - sub, - OidcConstants.AuthenticationMethods.Password, - additionalClaims.ToArray() - ); + if (await externalLoginProvider.TryAuthenticateAsync(context.UserName, context.Password)) + { + user = await UserManager.FindByNameAsync(context.UserName); + if (user == null) + { + user = await externalLoginProvider.CreateUserAsync(context.UserName, externalLoginProviderInfo.Name); + } + else + { + await externalLoginProvider.UpdateUserAsync(user, externalLoginProviderInfo.Name); + } - await IdentitySecurityLogManager.SaveAsync( - new IdentitySecurityLogContext - { - Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, - Action = IdentityServerSecurityLogActionConsts.LoginSucceeded, - UserName = context.UserName, - ClientId = clientId + await SetSuccessResultAsync(context, user); + return; + } } - ); - } + } - if (AbpIdentityOptions.ExternalLoginProviders.Any()) - { - foreach (var externalLoginProviderInfo in AbpIdentityOptions.ExternalLoginProviders.Values) + user = await UserManager.FindByNameAsync(context.UserName); + string errorDescription; + if (user != null) { - var externalLoginProvider = (IExternalLoginProvider) scope.ServiceProvider - .GetRequiredService(externalLoginProviderInfo.Type); - - if (await externalLoginProvider.TryAuthenticateAsync(context.UserName, context.Password)) + await IdentityOptions.SetAsync(); + var result = await SignInManager.CheckPasswordSignInAsync(user, context.Password, true); + if (result.Succeeded) { - user = await UserManager.FindByNameAsync(context.UserName); - if (user == null) + if (await IsTfaEnabledAsync(user)) { - user = await externalLoginProvider.CreateUserAsync(context.UserName, externalLoginProviderInfo.Name); + await HandleTwoFactorLoginAsync(context, user); } else { - await externalLoginProvider.UpdateUserAsync(user, externalLoginProviderInfo.Name); + await SetSuccessResultAsync(context, user); } - - await SetSuccessResultAsync(); return; } + + if (result.IsLockedOut) + { + Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", context.UserName); + errorDescription = Localizer["UserLockedOut"]; + } + else if (result.IsNotAllowed) + { + Logger.LogInformation("Authentication failed for username: {username}, reason: not allowed", context.UserName); + errorDescription = Localizer["LoginIsNotAllowed"]; + } + else + { + Logger.LogInformation("Authentication failed for username: {username}, reason: invalid credentials", context.UserName); + errorDescription = Localizer["InvalidUserNameOrPassword"]; + } + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = result.ToIdentitySecurityLogAction(), + UserName = context.UserName, + ClientId = await FindClientIdAsync(context) + }); + } + else + { + Logger.LogInformation("No user found matching username: {username}", context.UserName); + errorDescription = Localizer["InvalidUsername"]; + + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = IdentityServerSecurityLogActionConsts.LoginInvalidUserName, + UserName = context.UserName, + ClientId = await FindClientIdAsync(context) + }); } + + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, errorDescription); } + } - user = await UserManager.FindByNameAsync(context.UserName); - string errorDescription; - if (user != null) + protected virtual async Task HandleTwoFactorLoginAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user) + { + var twoFactorProvider = context.Request?.Raw?["TwoFactorProvider"]; + var twoFactorCode = context.Request?.Raw?["TwoFactorCode"]; + if (!twoFactorProvider.IsNullOrWhiteSpace() && !twoFactorCode.IsNullOrWhiteSpace()) { - await IdentityOptions.SetAsync(); - var result = await SignInManager.CheckPasswordSignInAsync(user, context.Password, true); - if (result.Succeeded) + var providers = await UserManager.GetValidTwoFactorProvidersAsync(user); + if (providers.Contains(twoFactorProvider) && await UserManager.VerifyTwoFactorTokenAsync(user, twoFactorProvider, twoFactorCode)) { - await SetSuccessResultAsync(); + await SetSuccessResultAsync(context, user); return; } - else if (result.IsLockedOut) - { - Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", context.UserName); - errorDescription = Localizer["UserLockedOut"]; - } - else if (result.IsNotAllowed) - { - Logger.LogInformation("Authentication failed for username: {username}, reason: not allowed", context.UserName); - errorDescription = Localizer["LoginIsNotAllowed"]; - } - else - { - Logger.LogInformation("Authentication failed for username: {username}, reason: invalid credentials", context.UserName); - errorDescription = Localizer["InvalidUserNameOrPassword"]; - } - await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext - { - Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, - Action = result.ToIdentitySecurityLogAction(), - UserName = context.UserName, - ClientId = clientId - }); + Logger.LogInformation("Authentication failed for username: {username}, reason: InvalidAuthenticatorCode", context.UserName); + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, Localizer["InvalidAuthenticatorCode"]); } else { - Logger.LogInformation("No user found matching username: {username}", context.UserName); - errorDescription = Localizer["InvalidUsername"]; + Logger.LogInformation("Authentication failed for username: {username}, reason: RequiresTwoFactor", context.UserName); + var twoFactorToken = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, nameof(SignInResult.RequiresTwoFactor)); + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, nameof(SignInResult.RequiresTwoFactor), + new Dictionary() + { + {"userId", user.Id}, + {"twoFactorToken", twoFactorToken} + }); - await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext() + await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext { Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, - Action = IdentityServerSecurityLogActionConsts.LoginInvalidUserName, + Action = IdentityServerSecurityLogActionConsts.LoginRequiresTwoFactor, UserName = context.UserName, - ClientId = clientId + ClientId = await FindClientIdAsync(context) }); } + } + + protected virtual async Task SetSuccessResultAsync(ResourceOwnerPasswordValidationContext context, IdentityUser user) + { + var sub = await UserManager.GetUserIdAsync(user); + + Logger.LogInformation("Credentials validated for username: {username}", context.UserName); + + var additionalClaims = new List(); + + await AddCustomClaimsAsync(additionalClaims, user, context); + + context.Result = new GrantValidationResult( + sub, + OidcConstants.AuthenticationMethods.Password, + additionalClaims.ToArray() + ); - context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, errorDescription); + await IdentitySecurityLogManager.SaveAsync( + new IdentitySecurityLogContext + { + Identity = IdentityServerSecurityLogIdentityConsts.IdentityServer, + Action = IdentityServerSecurityLogActionConsts.LoginSucceeded, + UserName = context.UserName, + ClientId = await FindClientIdAsync(context) + } + ); } protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds(ResourceOwnerPasswordValidationContext context) @@ -196,6 +242,16 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity context.UserName = userByEmail.UserName; } + protected virtual Task FindClientIdAsync(ResourceOwnerPasswordValidationContext context) + { + return Task.FromResult(context.Request?.Client?.ClientId); + } + + protected virtual async Task IsTfaEnabledAsync(IdentityUser user) + => UserManager.SupportsUserTwoFactor && + await UserManager.GetTwoFactorEnabledAsync(user) && + (await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; + protected virtual Task AddCustomClaimsAsync(List customClaims, IdentityUser user, ResourceOwnerPasswordValidationContext context) { if (user.TenantId.HasValue)