1
0
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:
Oscar Hinton
2022-09-27 18:30:37 +02:00
committed by GitHub
parent ea0087ee6f
commit c11a179332
25 changed files with 91 additions and 124 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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