mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[SM-220] Move identity specific files to identity (#2279)
This commit is contained in:
@ -1,89 +0,0 @@
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
|
||||
public class PasswordlessSignInManager<TUser> : SignInManager<TUser> where TUser : class
|
||||
{
|
||||
public const string PasswordlessSignInPurpose = "PasswordlessSignIn";
|
||||
|
||||
private readonly IMailService _mailService;
|
||||
|
||||
public PasswordlessSignInManager(UserManager<TUser> userManager,
|
||||
IHttpContextAccessor contextAccessor,
|
||||
IUserClaimsPrincipalFactory<TUser> claimsFactory,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
ILogger<SignInManager<TUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<TUser> confirmation,
|
||||
IMailService mailService)
|
||||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||
{
|
||||
_mailService = mailService;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(string email, string returnUrl)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
var token = await UserManager.GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||
PasswordlessSignInPurpose);
|
||||
await _mailService.SendPasswordlessSignInAsync(returnUrl, token, email);
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(TUser user, string token, bool isPersistent)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var attempt = await CheckPasswordlessSignInAsync(user, token);
|
||||
return attempt.Succeeded ?
|
||||
await SignInOrTwoFactorAsync(user, isPersistent, bypassTwoFactor: true) : attempt;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(string email, string token, bool isPersistent)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
return await PasswordlessSignInAsync(user, token, isPersistent);
|
||||
}
|
||||
|
||||
public virtual async Task<SignInResult> CheckPasswordlessSignInAsync(TUser user, string token)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var error = await PreSignInCheck(user);
|
||||
if (error != null)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
if (await UserManager.VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||
PasswordlessSignInPurpose, token))
|
||||
{
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
Logger.LogWarning(2, "User {userId} failed to provide the correct token.",
|
||||
await UserManager.GetUserIdAsync(user));
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
|
||||
public class ReadOnlyDatabaseIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ReadOnlyDatabaseIdentityUserStore(
|
||||
IUserService userService,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public override async Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(normalizedEmail);
|
||||
return user?.ToIdentityUser(await _userService.TwoFactorIsEnabledAsync(user));
|
||||
}
|
||||
|
||||
public override async Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (!Guid.TryParse(userId, out var userIdGuid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userIdGuid);
|
||||
return user?.ToIdentityUser(await _userService.TwoFactorIsEnabledAsync(user));
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
|
||||
public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public ReadOnlyEnvIdentityUserStore(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var usersCsv = _configuration["adminSettings:admins"];
|
||||
if (!CoreHelpers.SettingHasValue(usersCsv))
|
||||
{
|
||||
return Task.FromResult<IdentityUser>(null);
|
||||
}
|
||||
|
||||
var users = usersCsv.ToLowerInvariant().Split(',');
|
||||
var usersDict = new Dictionary<string, string>();
|
||||
foreach (var u in users)
|
||||
{
|
||||
var parts = u.Split(':');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var email = parts[0].Trim();
|
||||
var stamp = parts[1].Trim();
|
||||
usersDict.Add(email, stamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
var email = parts[0].Trim();
|
||||
usersDict.Add(email, email);
|
||||
}
|
||||
}
|
||||
|
||||
var userStamp = usersDict.ContainsKey(normalizedEmail) ? usersDict[normalizedEmail] : null;
|
||||
if (userStamp == null)
|
||||
{
|
||||
return Task.FromResult<IdentityUser>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult(new IdentityUser
|
||||
{
|
||||
Id = normalizedEmail,
|
||||
Email = normalizedEmail,
|
||||
NormalizedEmail = normalizedEmail,
|
||||
EmailConfirmed = true,
|
||||
UserName = normalizedEmail,
|
||||
NormalizedUserName = normalizedEmail,
|
||||
SecurityStamp = userStamp
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return FindByEmailAsync(userId, cancellationToken);
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
|
||||
public abstract class ReadOnlyIdentityUserStore :
|
||||
IUserStore<IdentityUser>,
|
||||
IUserEmailStore<IdentityUser>,
|
||||
IUserSecurityStampStore<IdentityUser>
|
||||
{
|
||||
public void Dispose() { }
|
||||
|
||||
public Task<IdentityResult> CreateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> DeleteAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public abstract Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
public abstract Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return await FindByEmailAsync(normalizedUserName, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> GetEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<bool> GetEmailConfirmedAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.EmailConfirmed);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetUserIdAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Id);
|
||||
}
|
||||
|
||||
public Task<string> GetUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task SetEmailAsync(IdentityUser user, string email,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.NormalizedEmail = normalizedEmail;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
user.NormalizedUserName = normalizedName;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetUserNameAsync(IdentityUser user, string userName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> UpdateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.SecurityStamp);
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
using Bit.Core.Settings;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class ApiClient : Client
|
||||
{
|
||||
public ApiClient(
|
||||
GlobalSettings globalSettings,
|
||||
string id,
|
||||
int refreshTokenSlidingDays,
|
||||
int accessTokenLifetimeHours,
|
||||
string[] scopes = null)
|
||||
{
|
||||
ClientId = id;
|
||||
AllowedGrantTypes = new[] { GrantType.ResourceOwnerPassword, GrantType.AuthorizationCode };
|
||||
RefreshTokenExpiration = TokenExpiration.Sliding;
|
||||
RefreshTokenUsage = TokenUsage.ReUse;
|
||||
SlidingRefreshTokenLifetime = 86400 * refreshTokenSlidingDays;
|
||||
AbsoluteRefreshTokenLifetime = 0; // forever
|
||||
UpdateAccessTokenClaimsOnRefresh = true;
|
||||
AccessTokenLifetime = 3600 * accessTokenLifetimeHours;
|
||||
AllowOfflineAccess = true;
|
||||
|
||||
RequireConsent = false;
|
||||
RequirePkce = true;
|
||||
RequireClientSecret = false;
|
||||
if (id == "web")
|
||||
{
|
||||
RedirectUris = new[] { $"{globalSettings.BaseServiceUri.Vault}/sso-connector.html" };
|
||||
PostLogoutRedirectUris = new[] { globalSettings.BaseServiceUri.Vault };
|
||||
AllowedCorsOrigins = new[] { globalSettings.BaseServiceUri.Vault };
|
||||
}
|
||||
else if (id == "desktop")
|
||||
{
|
||||
RedirectUris = new[] { "bitwarden://sso-callback" };
|
||||
PostLogoutRedirectUris = new[] { "bitwarden://logged-out" };
|
||||
}
|
||||
else if (id == "connector")
|
||||
{
|
||||
var connectorUris = new List<string>();
|
||||
for (var port = 8065; port <= 8070; port++)
|
||||
{
|
||||
connectorUris.Add(string.Format("http://localhost:{0}", port));
|
||||
}
|
||||
RedirectUris = connectorUris.Append("bwdc://sso-callback").ToList();
|
||||
PostLogoutRedirectUris = connectorUris.Append("bwdc://logged-out").ToList();
|
||||
}
|
||||
else if (id == "browser")
|
||||
{
|
||||
RedirectUris = new[] { $"{globalSettings.BaseServiceUri.Vault}/sso-connector.html" };
|
||||
PostLogoutRedirectUris = new[] { globalSettings.BaseServiceUri.Vault };
|
||||
AllowedCorsOrigins = new[] { globalSettings.BaseServiceUri.Vault };
|
||||
}
|
||||
else if (id == "cli")
|
||||
{
|
||||
var cliUris = new List<string>();
|
||||
for (var port = 8065; port <= 8070; port++)
|
||||
{
|
||||
cliUris.Add(string.Format("http://localhost:{0}", port));
|
||||
}
|
||||
RedirectUris = cliUris;
|
||||
PostLogoutRedirectUris = cliUris;
|
||||
}
|
||||
else if (id == "mobile")
|
||||
{
|
||||
RedirectUris = new[] { "bitwarden://sso-callback" };
|
||||
PostLogoutRedirectUris = new[] { "bitwarden://logged-out" };
|
||||
}
|
||||
|
||||
if (scopes == null)
|
||||
{
|
||||
scopes = new string[] { "api" };
|
||||
}
|
||||
AllowedScopes = scopes;
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class ApiResources
|
||||
{
|
||||
public static IEnumerable<ApiResource> GetApiResources()
|
||||
{
|
||||
return new List<ApiResource>
|
||||
{
|
||||
new ApiResource("api", new string[] {
|
||||
JwtClaimTypes.Name,
|
||||
JwtClaimTypes.Email,
|
||||
JwtClaimTypes.EmailVerified,
|
||||
"sstamp", // security stamp
|
||||
"premium",
|
||||
"device",
|
||||
"orgowner",
|
||||
"orgadmin",
|
||||
"orgmanager",
|
||||
"orguser",
|
||||
"orgcustom",
|
||||
"providerprovideradmin",
|
||||
"providerserviceuser",
|
||||
}),
|
||||
new ApiResource("internal", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.push", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.licensing", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.organization", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.provider", new string[] { JwtClaimTypes.Subject }),
|
||||
new ApiResource("api.installation", new string[] { JwtClaimTypes.Subject }),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class ApiScopes
|
||||
{
|
||||
public static IEnumerable<ApiScope> GetApiScopes()
|
||||
{
|
||||
return new List<ApiScope>
|
||||
{
|
||||
new ApiScope("api", "API Access"),
|
||||
new ApiScope("api.push", "API Push Access"),
|
||||
new ApiScope("api.licensing", "API Licensing Access"),
|
||||
new ApiScope("api.organization", "API Organization Access"),
|
||||
new ApiScope("api.installation", "API Installation Access"),
|
||||
new ApiScope("internal", "Internal Access")
|
||||
};
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using IdentityServer4.Stores.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
// ref: https://raw.githubusercontent.com/IdentityServer/IdentityServer4/3.1.3/src/IdentityServer4/src/Stores/Default/DefaultAuthorizationCodeStore.cs
|
||||
public class AuthorizationCodeStore : DefaultGrantStore<AuthorizationCode>, IAuthorizationCodeStore
|
||||
{
|
||||
public AuthorizationCodeStore(
|
||||
IPersistedGrantStore store,
|
||||
IPersistentGrantSerializer serializer,
|
||||
IHandleGenerationService handleGenerationService,
|
||||
ILogger<DefaultAuthorizationCodeStore> logger)
|
||||
: base(IdentityServerConstants.PersistedGrantTypes.AuthorizationCode, store, serializer,
|
||||
handleGenerationService, logger)
|
||||
{ }
|
||||
|
||||
public Task<string> StoreAuthorizationCodeAsync(AuthorizationCode code)
|
||||
{
|
||||
return CreateItemAsync(code, code.ClientId, code.Subject.GetSubjectId(), code.SessionId,
|
||||
code.Description, code.CreationTime, code.Lifetime);
|
||||
}
|
||||
|
||||
public Task<AuthorizationCode> GetAuthorizationCodeAsync(string code)
|
||||
{
|
||||
return GetItemAsync(code);
|
||||
}
|
||||
|
||||
public Task RemoveAuthorizationCodeAsync(string code)
|
||||
{
|
||||
// return RemoveItemAsync(code);
|
||||
|
||||
// We don't want to delete authorization codes during validation.
|
||||
// We'll rely on the authorization code lifecycle for short term validation and the
|
||||
// DatabaseExpiredGrantsJob to clean up old authorization codes.
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
@ -1,622 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger<ResourceOwnerPasswordValidator> _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IPolicyRepository policyRepository,
|
||||
IUserRepository userRepository,
|
||||
ICaptchaValidationService captchaValidationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_deviceRepository = deviceRepository;
|
||||
_deviceService = deviceService;
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_policyRepository = policyRepository;
|
||||
_userRepository = userRepository;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
}
|
||||
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
var isBot = (validatorContext.CaptchaResponse?.IsBot ?? false);
|
||||
if (isBot)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Login attempt for {0} detected as a captcha bot with score {1}.",
|
||||
request.UserName, validatorContext.CaptchaResponse.Score);
|
||||
}
|
||||
|
||||
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
|
||||
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
||||
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
||||
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||
|
||||
var valid = await ValidateContextAsync(context, validatorContext);
|
||||
var user = validatorContext.User;
|
||||
if (!valid)
|
||||
{
|
||||
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
|
||||
}
|
||||
if (!valid || isBot)
|
||||
{
|
||||
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
var (isTwoFactorRequired, requires2FABecauseNewDevice, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request);
|
||||
if (isTwoFactorRequired)
|
||||
{
|
||||
// Just defaulting it
|
||||
var twoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType))
|
||||
{
|
||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context, requires2FABecauseNewDevice);
|
||||
return;
|
||||
}
|
||||
|
||||
BeforeVerifyTwoFactor(user, twoFactorProviderType, requires2FABecauseNewDevice);
|
||||
|
||||
var verified = await VerifyTwoFactor(user, twoFactorOrganization,
|
||||
twoFactorProviderType, twoFactorToken);
|
||||
|
||||
AfterVerifyTwoFactor(user, twoFactorProviderType, requires2FABecauseNewDevice);
|
||||
|
||||
if ((!verified || isBot) && twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||
{
|
||||
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice);
|
||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||
return;
|
||||
}
|
||||
else if ((!verified || isBot) && twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||
{
|
||||
// Delay for brute force.
|
||||
await Task.Delay(2000);
|
||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context, requires2FABecauseNewDevice);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
twoFactorRequest = false;
|
||||
twoFactorRemember = false;
|
||||
twoFactorToken = null;
|
||||
}
|
||||
|
||||
// Returns true if can finish validation process
|
||||
if (await IsValidAuthTypeAsync(user, request.GrantType))
|
||||
{
|
||||
var device = await SaveDeviceAsync(user, request);
|
||||
if (device == null)
|
||||
{
|
||||
await BuildErrorResultAsync("No device information provided.", false, context, user);
|
||||
return;
|
||||
}
|
||||
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSsoResult(context, new Dictionary<string, object>
|
||||
{{
|
||||
"ErrorModel", new ErrorResponseModel("SSO authentication is required.")
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
|
||||
|
||||
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
|
||||
{
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);
|
||||
|
||||
var claims = new List<Claim>();
|
||||
|
||||
if (device != null)
|
||||
{
|
||||
claims.Add(new Claim("device", device.Identifier));
|
||||
}
|
||||
|
||||
var customResponse = new Dictionary<string, object>();
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
||||
{
|
||||
customResponse.Add("PrivateKey", user.PrivateKey);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.Key))
|
||||
{
|
||||
customResponse.Add("Key", user.Key);
|
||||
}
|
||||
|
||||
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
|
||||
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
|
||||
customResponse.Add("Kdf", (byte)user.Kdf);
|
||||
customResponse.Add("KdfIterations", user.KdfIterations);
|
||||
|
||||
if (sendRememberToken)
|
||||
{
|
||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
|
||||
customResponse.Add("TwoFactorToken", token);
|
||||
}
|
||||
|
||||
await ResetFailedAuthDetailsAsync(user);
|
||||
await SetSuccessResult(context, user, claims, customResponse);
|
||||
}
|
||||
|
||||
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context, bool requires2FABecauseNewDevice)
|
||||
{
|
||||
var providerKeys = new List<byte>();
|
||||
var providers = new Dictionary<string, Dictionary<string, object>>();
|
||||
|
||||
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
|
||||
if (organization?.GetTwoFactorProviders() != null)
|
||||
{
|
||||
enabledProviders.AddRange(organization.GetTwoFactorProviders().Where(
|
||||
p => organization.TwoFactorProviderIsEnabled(p.Key)));
|
||||
}
|
||||
|
||||
if (user.GetTwoFactorProviders() != null)
|
||||
{
|
||||
foreach (var p in user.GetTwoFactorProviders())
|
||||
{
|
||||
if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user))
|
||||
{
|
||||
enabledProviders.Add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledProviders.Any())
|
||||
{
|
||||
if (!requires2FABecauseNewDevice)
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
var emailProvider = new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
||||
Enabled = true
|
||||
};
|
||||
enabledProviders.Add(new KeyValuePair<TwoFactorProviderType, TwoFactorProvider>(
|
||||
TwoFactorProviderType.Email, emailProvider));
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = emailProvider
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var provider in enabledProviders)
|
||||
{
|
||||
providerKeys.Add((byte)provider.Key);
|
||||
var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);
|
||||
providers.Add(((byte)provider.Key).ToString(), infoDict);
|
||||
}
|
||||
|
||||
SetTwoFactorResult(context,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "TwoFactorProviders", providers.Keys },
|
||||
{ "TwoFactorProviders2", providers }
|
||||
});
|
||||
|
||||
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
|
||||
{
|
||||
// Send email now if this is their only 2FA method
|
||||
await _userService.SendTwoFactorEmailAsync(user, requires2FABecauseNewDevice);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
await _eventService.LogUserEventAsync(user.Id,
|
||||
twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn);
|
||||
}
|
||||
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
_logger.LogWarning(Constants.BypassFiltersEventId,
|
||||
string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".",
|
||||
$" {_currentContext.IpAddress}"));
|
||||
}
|
||||
|
||||
await Task.Delay(2000); // Delay for brute force.
|
||||
SetErrorResult(context,
|
||||
new Dictionary<string, object>
|
||||
{{
|
||||
"ErrorModel", new ErrorResponseModel(message)
|
||||
}});
|
||||
}
|
||||
|
||||
protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse);
|
||||
|
||||
protected abstract void SetSsoResult(T context, Dictionary<string, object> customResponse);
|
||||
|
||||
protected abstract Task SetSuccessResult(T context, User user, List<Claim> claims,
|
||||
Dictionary<string, object> customResponse);
|
||||
|
||||
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
||||
|
||||
private async Task<Tuple<bool, bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (request.GrantType == "client_credentials")
|
||||
{
|
||||
// Do not require MFA for api key logins
|
||||
return new Tuple<bool, bool, Organization>(false, false, null);
|
||||
}
|
||||
|
||||
var individualRequired = _userManager.SupportsUserTwoFactor &&
|
||||
await _userManager.GetTwoFactorEnabledAsync(user) &&
|
||||
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
|
||||
|
||||
Organization firstEnabledOrg = null;
|
||||
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
||||
.ToList();
|
||||
if (orgs.Any())
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));
|
||||
if (twoFactorOrgs.Any())
|
||||
{
|
||||
var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
firstEnabledOrg = userOrgs.FirstOrDefault(
|
||||
o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
var requires2FA = individualRequired || firstEnabledOrg != null;
|
||||
var requires2FABecauseNewDevice = !requires2FA
|
||||
&&
|
||||
await _userService.Needs2FABecauseNewDeviceAsync(
|
||||
user,
|
||||
GetDeviceFromRequest(request)?.Identifier,
|
||||
request.GrantType);
|
||||
|
||||
requires2FA = requires2FA || requires2FABecauseNewDevice;
|
||||
|
||||
return new Tuple<bool, bool, Organization>(requires2FA, requires2FABecauseNewDevice, firstEnabledOrg);
|
||||
}
|
||||
|
||||
private async Task<bool> IsValidAuthTypeAsync(User user, string grantType)
|
||||
{
|
||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
||||
{
|
||||
// Already using SSO to authorize, finish successfully
|
||||
// Or login via api key, skip SSO requirement
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is user apart of any orgs? Use cache for initial checks.
|
||||
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
||||
.ToList();
|
||||
if (orgs.Any())
|
||||
{
|
||||
// Get all org abilities
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
// Parse all user orgs that are enabled and have the ability to use sso
|
||||
var ssoOrgs = orgs.Where(o => OrgCanUseSso(orgAbilities, o.Id));
|
||||
if (ssoOrgs.Any())
|
||||
{
|
||||
// Parse users orgs and determine if require sso policy is enabled
|
||||
var userOrgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id,
|
||||
OrganizationUserStatusType.Confirmed);
|
||||
foreach (var userOrg in userOrgs.Where(o => o.Enabled && o.UseSso))
|
||||
{
|
||||
var orgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(userOrg.OrganizationId,
|
||||
PolicyType.RequireSso);
|
||||
// Owners and Admins are exempt from this policy
|
||||
if (orgPolicy != null && orgPolicy.Enabled &&
|
||||
userOrg.Type != OrganizationUserType.Owner && userOrg.Type != OrganizationUserType.Admin)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default - continue validation process
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||
{
|
||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
||||
}
|
||||
|
||||
private bool OrgCanUseSso(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||
{
|
||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].UseSso;
|
||||
}
|
||||
|
||||
private Device GetDeviceFromRequest(ValidatedRequest request)
|
||||
{
|
||||
var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString();
|
||||
var deviceType = request.Raw["DeviceType"]?.ToString();
|
||||
var deviceName = request.Raw["DeviceName"]?.ToString();
|
||||
var devicePushToken = request.Raw["DevicePushToken"]?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) ||
|
||||
string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Device
|
||||
{
|
||||
Identifier = deviceIdentifier,
|
||||
Name = deviceName,
|
||||
Type = type,
|
||||
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
|
||||
};
|
||||
}
|
||||
|
||||
private void BeforeVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
|
||||
{
|
||||
if (type == TwoFactorProviderType.Email && requires2FABecauseNewDevice)
|
||||
{
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
[TwoFactorProviderType.Email] = new TwoFactorProvider
|
||||
{
|
||||
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void AfterVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
|
||||
{
|
||||
if (type == TwoFactorProviderType.Email && requires2FABecauseNewDevice)
|
||||
{
|
||||
user.ClearTwoFactorProviders();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
|
||||
string token)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
case TwoFactorProviderType.Email:
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
case TwoFactorProviderType.Remember:
|
||||
if (type != TwoFactorProviderType.Remember &&
|
||||
!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(type), token);
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
if (!organization?.TwoFactorProviderIsEnabled(type) ?? true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
|
||||
TwoFactorProviderType type, TwoFactorProvider provider)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
case TwoFactorProviderType.Email:
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
if (!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(type));
|
||||
if (type == TwoFactorProviderType.Duo)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["Host"] = provider.MetaData["Host"],
|
||||
["Signature"] = token
|
||||
};
|
||||
}
|
||||
else if (type == TwoFactorProviderType.WebAuthn)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<Dictionary<string, object>>(token);
|
||||
}
|
||||
else if (type == TwoFactorProviderType.Email)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["Email"] = token
|
||||
};
|
||||
}
|
||||
else if (type == TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["Nfc"] = (bool)provider.MetaData["Nfc"]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["Host"] = provider.MetaData["Host"],
|
||||
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request) =>
|
||||
(await GetKnownDeviceAsync(user, request)) != default;
|
||||
|
||||
protected async Task<Device> GetKnownDeviceAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id);
|
||||
}
|
||||
|
||||
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
var device = GetDeviceFromRequest(request);
|
||||
if (device != null)
|
||||
{
|
||||
var existingDevice = await GetKnownDeviceAsync(user, request);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
device.UserId = user.Id;
|
||||
await _deviceService.SaveAsync(device);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
|
||||
{
|
||||
var deviceType = device.Type.GetType().GetMember(device.Type.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||
if (!_globalSettings.DisableEmailNewDevice)
|
||||
{
|
||||
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
||||
_currentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
return existingDevice;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task ResetFailedAuthDetailsAsync(User user)
|
||||
{
|
||||
// Early escape if db hit not necessary
|
||||
if (user == null || user.FailedLoginCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
user.FailedLoginCount = 0;
|
||||
user.RevisionDate = DateTime.UtcNow;
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
|
||||
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
user.FailedLoginCount = ++user.FailedLoginCount;
|
||||
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
|
||||
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
|
||||
{
|
||||
if (twoFactorInvalid)
|
||||
{
|
||||
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
|
||||
{
|
||||
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
||||
var failedLoginCount = user?.FailedLoginCount ?? 0;
|
||||
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
|
||||
}
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class ClientStore : IClientStore
|
||||
{
|
||||
private readonly IInstallationRepository _installationRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly StaticClientStore _staticClientStore;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
|
||||
public ClientStore(
|
||||
IInstallationRepository installationRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
GlobalSettings globalSettings,
|
||||
StaticClientStore staticClientStore,
|
||||
ILicensingService licensingService,
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository)
|
||||
{
|
||||
_installationRepository = installationRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_userRepository = userRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_staticClientStore = staticClientStore;
|
||||
_licensingService = licensingService;
|
||||
_currentContext = currentContext;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
}
|
||||
|
||||
public async Task<Client> FindClientByIdAsync(string clientId)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && clientId.StartsWith("installation."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out Guid id))
|
||||
{
|
||||
var installation = await _installationRepository.GetByIdAsync(id);
|
||||
if (installation != null)
|
||||
{
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"installation.{installation.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(installation.Key.Sha256()) },
|
||||
AllowedScopes = new string[] { "api.push", "api.licensing", "api.installation" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = installation.Enabled,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, installation.Id.ToString())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_globalSettings.SelfHosted && clientId.StartsWith("internal.") &&
|
||||
CoreHelpers.SettingHasValue(_globalSettings.InternalIdentityKey))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1)
|
||||
{
|
||||
var id = idParts[1];
|
||||
if (!string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"internal.{id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(_globalSettings.InternalIdentityKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "internal" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = true,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, id)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (clientId.StartsWith("organization."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(id);
|
||||
if (org != null)
|
||||
{
|
||||
var orgApiKey = (await _organizationApiKeyRepository
|
||||
.GetManyByOrganizationIdTypeAsync(org.Id, OrganizationApiKeyType.Default))
|
||||
.First();
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"organization.{org.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(orgApiKey.ApiKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "api.organization" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
Enabled = org.Enabled && org.UseApi,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, org.Id.ToString())
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (clientId.StartsWith("user."))
|
||||
{
|
||||
var idParts = clientId.Split('.');
|
||||
if (idParts.Length > 1 && Guid.TryParse(idParts[1], out var id))
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user != null)
|
||||
{
|
||||
var claims = new Collection<ClientClaim>()
|
||||
{
|
||||
new ClientClaim(JwtClaimTypes.Subject, user.Id.ToString()),
|
||||
new ClientClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external")
|
||||
};
|
||||
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
||||
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
||||
var isPremium = await _licensingService.ValidateUserPremiumAsync(user);
|
||||
foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))
|
||||
{
|
||||
var upperValue = claim.Value.ToUpperInvariant();
|
||||
var isBool = upperValue == "TRUE" || upperValue == "FALSE";
|
||||
claims.Add(isBool ?
|
||||
new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean) :
|
||||
new ClientClaim(claim.Key, claim.Value)
|
||||
);
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = clientId,
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(user.ApiKey.Sha256()) },
|
||||
AllowedScopes = new string[] { "api" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
ClientClaimsPrefix = null,
|
||||
Claims = claims
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _staticClientStore.ApiClients.ContainsKey(clientId) ?
|
||||
_staticClientStore.ApiClients[clientId] : null;
|
||||
}
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||
ICustomTokenRequestValidator
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
|
||||
public CustomTokenRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IPolicyRepository policyRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
ICaptchaValidationService captchaValidationService)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
|
||||
userRepository, captchaValidationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
||||
{
|
||||
string[] allowedGrantTypes = { "authorization_code", "client_credentials" };
|
||||
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
await ValidateAsync(context, context.Result.ValidatedRequest,
|
||||
new CustomValidatorRequestContext { KnownDevice = true });
|
||||
}
|
||||
|
||||
protected async override Task<bool> ValidateContextAsync(CustomTokenRequestValidationContext context,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
var email = context.Result.ValidatedRequest.Subject?.GetDisplayName()
|
||||
?? context.Result.ValidatedRequest.ClientClaims?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
validatorContext.User = await _userManager.FindByEmailAsync(email);
|
||||
}
|
||||
return validatorContext.User != null;
|
||||
}
|
||||
|
||||
protected override async Task SetSuccessResult(CustomTokenRequestValidationContext context, User user,
|
||||
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result.CustomResponse = customResponse;
|
||||
if (claims?.Any() ?? false)
|
||||
{
|
||||
context.Result.ValidatedRequest.Client.AlwaysSendClientClaims = true;
|
||||
context.Result.ValidatedRequest.Client.ClientClaimsPrefix = string.Empty;
|
||||
foreach (var claim in claims)
|
||||
{
|
||||
context.Result.ValidatedRequest.ClientClaims.Add(claim);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.Result.CustomResponse == null || user.MasterPassword != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// KeyConnector responses below
|
||||
|
||||
// Apikey login
|
||||
if (context.Result.ValidatedRequest.GrantType == "client_credentials")
|
||||
{
|
||||
if (user.UsesKeyConnector)
|
||||
{
|
||||
// KeyConnectorUrl is configured in the CLI client, we just need to tell the client to use it
|
||||
context.Result.CustomResponse["ApiUseKeyConnector"] = true;
|
||||
context.Result.CustomResponse["ResetMasterPassword"] = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// SSO login
|
||||
var organizationClaim = context.Result.ValidatedRequest.Subject?.FindFirst(c => c.Type == "organizationId");
|
||||
if (organizationClaim?.Value != null)
|
||||
{
|
||||
var organizationId = new Guid(organizationClaim.Value);
|
||||
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
|
||||
var ssoConfigData = ssoConfig.GetData();
|
||||
|
||||
if (ssoConfigData is { KeyConnectorEnabled: true } && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl))
|
||||
{
|
||||
context.Result.CustomResponse["KeyConnectorUrl"] = ssoConfigData.KeyConnectorUrl;
|
||||
// Prevent clients redirecting to set-password
|
||||
context.Result.CustomResponse["ResetMasterPassword"] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SetTwoFactorResult(CustomTokenRequestValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result.Error = "invalid_grant";
|
||||
context.Result.ErrorDescription = "Two factor required.";
|
||||
context.Result.IsError = true;
|
||||
context.Result.CustomResponse = customResponse;
|
||||
}
|
||||
|
||||
protected override void SetSsoResult(CustomTokenRequestValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result.Error = "invalid_grant";
|
||||
context.Result.ErrorDescription = "Single Sign on required.";
|
||||
context.Result.IsError = true;
|
||||
context.Result.CustomResponse = customResponse;
|
||||
}
|
||||
|
||||
protected override void SetErrorResult(CustomTokenRequestValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result.Error = "invalid_grant";
|
||||
context.Result.IsError = true;
|
||||
context.Result.CustomResponse = customResponse;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class CustomValidatorRequestContext
|
||||
{
|
||||
public User User { get; set; }
|
||||
public bool KnownDevice { get; set; }
|
||||
public CaptchaResponse CaptchaResponse { get; set; }
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using Bit.Core.Settings;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class OidcIdentityClient : Client
|
||||
{
|
||||
public OidcIdentityClient(GlobalSettings globalSettings)
|
||||
{
|
||||
ClientId = "oidc-identity";
|
||||
RequireClientSecret = true;
|
||||
RequirePkce = true;
|
||||
ClientSecrets = new List<Secret> { new Secret(globalSettings.OidcIdentityClientKey.Sha256()) };
|
||||
AllowedScopes = new string[]
|
||||
{
|
||||
IdentityServerConstants.StandardScopes.OpenId,
|
||||
IdentityServerConstants.StandardScopes.Profile
|
||||
};
|
||||
AllowedGrantTypes = GrantTypes.Code;
|
||||
Enabled = true;
|
||||
RedirectUris = new List<string> { $"{globalSettings.BaseServiceUri.Identity}/signin-oidc" };
|
||||
RequireConsent = false;
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
using Bit.Core.Repositories;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
using Grant = Bit.Core.Entities.Grant;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class PersistedGrantStore : IPersistedGrantStore
|
||||
{
|
||||
private readonly IGrantRepository _grantRepository;
|
||||
|
||||
public PersistedGrantStore(
|
||||
IGrantRepository grantRepository)
|
||||
{
|
||||
_grantRepository = grantRepository;
|
||||
}
|
||||
|
||||
public async Task<PersistedGrant> GetAsync(string key)
|
||||
{
|
||||
var grant = await _grantRepository.GetByKeyAsync(key);
|
||||
if (grant == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pGrant = ToPersistedGrant(grant);
|
||||
return pGrant;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PersistedGrant>> GetAllAsync(PersistedGrantFilter filter)
|
||||
{
|
||||
var grants = await _grantRepository.GetManyAsync(filter.SubjectId, filter.SessionId,
|
||||
filter.ClientId, filter.Type);
|
||||
var pGrants = grants.Select(g => ToPersistedGrant(g));
|
||||
return pGrants;
|
||||
}
|
||||
|
||||
public async Task RemoveAllAsync(PersistedGrantFilter filter)
|
||||
{
|
||||
await _grantRepository.DeleteManyAsync(filter.SubjectId, filter.SessionId, filter.ClientId, filter.Type);
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(string key)
|
||||
{
|
||||
await _grantRepository.DeleteByKeyAsync(key);
|
||||
}
|
||||
|
||||
public async Task StoreAsync(PersistedGrant pGrant)
|
||||
{
|
||||
var grant = ToGrant(pGrant);
|
||||
await _grantRepository.SaveAsync(grant);
|
||||
}
|
||||
|
||||
private Grant ToGrant(PersistedGrant pGrant)
|
||||
{
|
||||
return new Grant
|
||||
{
|
||||
Key = pGrant.Key,
|
||||
Type = pGrant.Type,
|
||||
SubjectId = pGrant.SubjectId,
|
||||
SessionId = pGrant.SessionId,
|
||||
ClientId = pGrant.ClientId,
|
||||
Description = pGrant.Description,
|
||||
CreationDate = pGrant.CreationTime,
|
||||
ExpirationDate = pGrant.Expiration,
|
||||
ConsumedDate = pGrant.ConsumedTime,
|
||||
Data = pGrant.Data
|
||||
};
|
||||
}
|
||||
|
||||
private PersistedGrant ToPersistedGrant(Grant grant)
|
||||
{
|
||||
return new PersistedGrant
|
||||
{
|
||||
Key = grant.Key,
|
||||
Type = grant.Type,
|
||||
SubjectId = grant.SubjectId,
|
||||
SessionId = grant.SessionId,
|
||||
ClientId = grant.ClientId,
|
||||
Description = grant.Description,
|
||||
CreationTime = grant.CreationDate,
|
||||
Expiration = grant.ExpirationDate,
|
||||
ConsumedTime = grant.ConsumedDate,
|
||||
Data = grant.Data
|
||||
};
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class ProfileService : IProfileService
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public ProfileService(
|
||||
IUserService userService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
ILicensingService licensingService,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_userService = userService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_licensingService = licensingService;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||
{
|
||||
var existingClaims = context.Subject.Claims;
|
||||
var newClaims = new List<Claim>();
|
||||
|
||||
var user = await _userService.GetUserByPrincipalAsync(context.Subject);
|
||||
if (user != null)
|
||||
{
|
||||
var isPremium = await _licensingService.ValidateUserPremiumAsync(user);
|
||||
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
||||
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
||||
foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))
|
||||
{
|
||||
var upperValue = claim.Value.ToUpperInvariant();
|
||||
var isBool = upperValue == "TRUE" || upperValue == "FALSE";
|
||||
newClaims.Add(isBool ?
|
||||
new Claim(claim.Key, claim.Value, ClaimValueTypes.Boolean) :
|
||||
new Claim(claim.Key, claim.Value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// filter out any of the new claims
|
||||
var existingClaimsToKeep = existingClaims
|
||||
.Where(c => !c.Type.StartsWith("org") &&
|
||||
(newClaims.Count == 0 || !newClaims.Any(nc => nc.Type == c.Type)))
|
||||
.ToList();
|
||||
|
||||
newClaims.AddRange(existingClaimsToKeep);
|
||||
if (newClaims.Any())
|
||||
{
|
||||
context.IssuedClaims.AddRange(newClaims);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task IsActiveAsync(IsActiveContext context)
|
||||
{
|
||||
var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == "sstamp");
|
||||
var user = await _userService.GetUserByPrincipalAsync(context.Subject);
|
||||
|
||||
if (user != null && securityTokenClaim != null)
|
||||
{
|
||||
context.IsActive = string.Equals(user.SecurityStamp, securityTokenClaim.Value,
|
||||
StringComparison.InvariantCultureIgnoreCase);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.IsActive = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
|
||||
IResourceOwnerPasswordValidator
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
public ResourceOwnerPasswordValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IPolicyRepository policyRepository,
|
||||
ICaptchaValidationService captchaValidationService,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IUserRepository userRepository)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
|
||||
userRepository, captchaValidationService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
||||
{
|
||||
if (!AuthEmailHeaderIsValid(context))
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant,
|
||||
"Auth-Email header invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
||||
var validatorContext = new CustomValidatorRequestContext
|
||||
{
|
||||
User = user,
|
||||
KnownDevice = await KnownDeviceAsync(user, context.Request)
|
||||
};
|
||||
string bypassToken = null;
|
||||
if (!validatorContext.KnownDevice &&
|
||||
_captchaValidationService.RequireCaptchaValidation(_currentContext, user))
|
||||
{
|
||||
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(captchaResponse))
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync(
|
||||
captchaResponse, _currentContext.IpAddress, user);
|
||||
if (!validatorContext.CaptchaResponse.Success)
|
||||
{
|
||||
await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null);
|
||||
return;
|
||||
}
|
||||
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||
}
|
||||
|
||||
await ValidateAsync(context, context.Request, validatorContext);
|
||||
if (context.Result.CustomResponse != null && bypassToken != null)
|
||||
{
|
||||
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
|
||||
}
|
||||
}
|
||||
|
||||
protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(context.UserName) || validatorContext.User == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var authRequestId = context.Request.Raw["AuthRequest"]?.ToString()?.ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(authRequestId) && Guid.TryParse(authRequestId, out var authRequestGuid))
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestGuid);
|
||||
if (authRequest != null)
|
||||
{
|
||||
var requestAge = DateTime.UtcNow - authRequest.CreationDate;
|
||||
if (requestAge < TimeSpan.FromHours(1) && !authRequest.AuthenticationDate.HasValue &&
|
||||
CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
|
||||
{
|
||||
authRequest.AuthenticationDate = DateTime.UtcNow;
|
||||
await _authRequestRepository.ReplaceAsync(authRequest);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
|
||||
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
||||
identityProvider: "bitwarden",
|
||||
claims: claims.Count > 0 ? claims : null,
|
||||
customResponse: customResponse);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override void SetTwoFactorResult(ResourceOwnerPasswordValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.",
|
||||
customResponse);
|
||||
}
|
||||
|
||||
protected override void SetSsoResult(ResourceOwnerPasswordValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Sso authentication required.",
|
||||
customResponse);
|
||||
}
|
||||
|
||||
protected override void SetErrorResult(ResourceOwnerPasswordValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse);
|
||||
}
|
||||
|
||||
private bool AuthEmailHeaderIsValid(ResourceOwnerPasswordValidationContext context)
|
||||
{
|
||||
if (!_currentContext.HttpContext.Request.Headers.ContainsKey("Auth-Email"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var authEmailHeader = _currentContext.HttpContext.Request.Headers["Auth-Email"];
|
||||
var authEmailDecoded = CoreHelpers.Base64UrlDecodeString(authEmailHeader);
|
||||
|
||||
if (authEmailDecoded != context.UserName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (System.Exception e) when (e is System.InvalidOperationException || e is System.FormatException)
|
||||
{
|
||||
// Invalid B64 encoding
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class StaticClientStore
|
||||
{
|
||||
public StaticClientStore(GlobalSettings globalSettings)
|
||||
{
|
||||
ApiClients = new List<Client>
|
||||
{
|
||||
new ApiClient(globalSettings, BitwardenClient.Mobile, 90, 1),
|
||||
new ApiClient(globalSettings, BitwardenClient.Web, 30, 1),
|
||||
new ApiClient(globalSettings, BitwardenClient.Browser, 30, 1),
|
||||
new ApiClient(globalSettings, BitwardenClient.Desktop, 30, 1),
|
||||
new ApiClient(globalSettings, BitwardenClient.Cli, 30, 1),
|
||||
new ApiClient(globalSettings, BitwardenClient.DirectoryConnector, 30, 24)
|
||||
}.ToDictionary(c => c.ClientId);
|
||||
}
|
||||
|
||||
public IDictionary<string, Client> ApiClients { get; private set; }
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Services;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
|
||||
public class CustomCorsPolicyService : ICorsPolicyService
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public CustomCorsPolicyService(GlobalSettings globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public Task<bool> IsOriginAllowedAsync(string origin)
|
||||
{
|
||||
return Task.FromResult(CoreHelpers.IsCorsOriginAllowed(origin, _globalSettings));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user