Files
server/src/Core/IdentityServer/BaseRequestValidator.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

623 lines
24 KiB
C#
Raw Normal View History

using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Security.Claims;
2021-03-22 23:21:43 +01:00
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;
Feature/self hosted families for enterprise (#1991) * Families for enterprise/split up organization sponsorship service (#1829) * Split OrganizationSponsorshipService into commands * Use tokenable for token validation * Use interfaces to set up for DI * Use commands over services * Move service tests to command tests * Value types can't be null * Run dotnet format * Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs Co-authored-by: Justin Baur <admin@justinbaur.com> * Fix controller tests Co-authored-by: Justin Baur <admin@justinbaur.com> * Families for enterprise/split up organization sponsorship service (#1875) * Split OrganizationSponsorshipService into commands * Use tokenable for token validation * Use interfaces to set up for DI * Use commands over services * Move service tests to command tests * Value types can't be null * Run dotnet format * Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs Co-authored-by: Justin Baur <admin@justinbaur.com> * Fix controller tests * Split create and send sponsorships * Split up create sponsorship * Add self hosted commands to dependency injection * Add field to store cloud billing sync key on self host instances * Fix typo * Fix data protector purpose of sponsorship offers * Split cloud and selfhosted sponsorship offer tokenable * Generate offer from self hosted with all necessary auth data * Add Required properties to constructor * Split up cancel sponsorship command * Split revoke sponsorship command between cloud and self hosted * Fix/f4e multiple sponsorships (#1838) * Use sponosorship from validate to redeem * Update tests * Format * Remove sponsorship service * Run dotnet format * Fix self hosted only controller attribute * Clean up file structure and fixes * Remove unneeded tokenables * Remove obsolete commands * Do not require file/class prefix if unnecessary * Update Organizaiton sprocs * Remove unnecessary models * Fix tests * Generalize LicenseService path calculation Use async file read and deserialization * Use interfaces for testability * Remove unused usings * Correct test direction * Test license reading * remove unused usings * Format Co-authored-by: Justin Baur <admin@justinbaur.com> * Improve DataProtectorTokenFactory test coverage (#1884) * Add encstring to server * Test factory Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com> * Format * Remove SymmetricKeyProtectedString Not needed * Set ForcInvalid Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com> * Feature/self f4e/api keys (#1896) * Add in ApiKey * Work on API Key table * Work on apikey table * Fix response model * Work on information for UI * Work on last sync date * Work on sync status * Work on auth * Work on tokenable * Work on merge * Add custom requirement * Add policy * Run formatting * Work on EF Migrations * Work on OrganizationConnection * Work on database * Work on additional database table * Run formatting * Small fixes * More cleanup * Cleanup * Add RevisionDate * Add GO * Finish Sql project * Add newlines * Fix stored proc file * Fix sqlproj * Add newlines * Fix table * Add navigation property * Delete Connections when organization is deleted * Add connection validation * Start adding ID column * Work on ID column * Work on SQL migration * Work on migrations * Run formatting * Fix test build * Fix sprocs * Work on migrations * Fix Create table * Fix sproc * Add prints to migration * Add default value * Update EF migrations * Formatting * Add to integration tests * Minor fixes * Formatting * Cleanup * Address PR feedback * Address more PR feedback * Fix formatting * Fix formatting * Fix * Address PR feedback * Remove accidential change * Fix SQL build * Run formatting * Address PR feedback * Add sync data to OrganizationUserOrgDetails * Add comments * Remove OrganizationConnectionService interface * Remove unused using * Address PR feedback * Formatting * Minor fix * Feature/self f4e/update db (#1930) * Fix migration * Fix TimesRenewed * Add comments * Make two properties non-nullable * Remove need for SponsoredOrg on SH (#1934) * Remove need for SponsoredOrg on SH * Add Family prefix * Add check for enterprise org on BillingSync key (#1936) * [PS-10] Feature/sponsorships removed at end of term (#1938) * Rename commands to min unique names * Inject revoke command based on self hosting * WIP: Remove/Revoke marks to delete * Complete WIP * Improve remove/revoke tests * PR review * Fail validation if sponsorship has failed to sync for 6 months * Feature/do not accept old self host sponsorships (#1939) * Do not accept >6mo old self-hosted sponsorships * Give disabled grace period of 3 months * Fix issues of Sql.proj differing from migration outcome (#1942) * Fix issues of Sql.proj differing from migration outcome * Yoink int tests * Add missing assert helpers * Feature/org sponsorship sync (#1922) * Self-hosted side sync first pass TODO: * flush out org sponsorship model * implement cloud side * process cloud-side response and update self-hosted records * sync scaffolding second pass * remove list of Org User ids from sync and begin work on SelfHostedRevokeSponsorship * allow authenticated http calls from server to return a result * update models * add logic for sync and change offer email template * add billing sync key and hide CreateSponsorship without user * fix tests * add job scheduling * add authorize attributes to endpoints * separate models into data/model and request/response * batch sync more, add EnableCloudCommunication for testing * send emails in bulk * make userId and sponsorshipType non nullable * batch more on self hosted side of sync * remove TODOs and formatting * changed logic of cloud sync * let BaseIdentityClientService handle all logging * call sync from scheduled job on self host * create bulk db operations for OrganizationSponsorships * remove SponsoredOrgId from sync, return default from server http call * validate BillingSyncKey during sync revert changes to CreateSponsorshipCommand * revert changes to ICreateSponsorshipCommand * add some tests * add DeleteExpiredSponsorshipsJob * add cloud sync test * remove extra method * formatting * prevent new sponsorships from disabled orgs * update packages * - pulled out send sponsorship command dependency from sync on cloud - don't throw error when sponsorships are empty - formatting * formatting models * more formatting * remove licensingService dependency from selfhosted sync * use installation urls and formatting * create constructor for RequestModel and formatting * add date parameter to OrganizationSponsorship_DeleteExpired * add new migration * formatting * rename OrganizationCreateSponsorshipRequestModel to OrganizationSponsorshipCreateRequestModel * prevent whole sync from failing if one sponsorship type is unsupported * deserialize config and billingsynckey from org connection * alter log message when sync disabled * Add grace period to disabled orgs * return early on self hosted if there are no sponsorships in database * rename BillingSyncConfig * send sponsorship offers from controller * allow config to be a null object * better exception handling in sync scheduler * add ef migrations * formatting * fix tests * fix validate test Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Fix OrganizationApiKey issues (#1941) Co-authored-by: Justin Baur <admin@justinbaur.com> * Feature/org sponsorship self hosted tests (#1947) * Self-hosted side sync first pass TODO: * flush out org sponsorship model * implement cloud side * process cloud-side response and update self-hosted records * sync scaffolding second pass * remove list of Org User ids from sync and begin work on SelfHostedRevokeSponsorship * allow authenticated http calls from server to return a result * update models * add logic for sync and change offer email template * add billing sync key and hide CreateSponsorship without user * fix tests * add job scheduling * add authorize attributes to endpoints * separate models into data/model and request/response * batch sync more, add EnableCloudCommunication for testing * send emails in bulk * make userId and sponsorshipType non nullable * batch more on self hosted side of sync * remove TODOs and formatting * changed logic of cloud sync * let BaseIdentityClientService handle all logging * call sync from scheduled job on self host * create bulk db operations for OrganizationSponsorships * remove SponsoredOrgId from sync, return default from server http call * validate BillingSyncKey during sync revert changes to CreateSponsorshipCommand * revert changes to ICreateSponsorshipCommand * add some tests * add DeleteExpiredSponsorshipsJob * add cloud sync test * remove extra method * formatting * prevent new sponsorships from disabled orgs * update packages * - pulled out send sponsorship command dependency from sync on cloud - don't throw error when sponsorships are empty - formatting * formatting models * more formatting * remove licensingService dependency from selfhosted sync * use installation urls and formatting * create constructor for RequestModel and formatting * add date parameter to OrganizationSponsorship_DeleteExpired * add new migration * formatting * rename OrganizationCreateSponsorshipRequestModel to OrganizationSponsorshipCreateRequestModel * prevent whole sync from failing if one sponsorship type is unsupported * deserialize config and billingsynckey from org connection * add mockHttp nuget package and use httpclientfactory * fix current tests * WIP of creating tests * WIP of new self hosted tests * WIP self hosted tests * finish self hosted tests * formatting * format of interface * remove extra config file * added newlines Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Fix Organization_DeleteById (#1950) * Fix Organization_Delete * Fix L * [PS-4] block enterprise user from sponsoring itself (#1943) * [PS-248] Feature/add connections enabled endpoint (#1953) * Move Organization models to sub namespaces * Add Organization Connection api endpoints * Get all connections rather than just enabled ones * Add missing services to DI * pluralize private api endpoints * Add type protection to org connection request/response * Fix route * Use nullable Id to signify no connection * Test Get Connections enabled * Fix data discoverer * Also drop this sproc for rerunning * Id is the OUTPUT of create sprocs * Fix connection config parsing * Linter fixes * update sqlproj file name * Use param xdocs on methods * Simplify controller path attribute * Use JsonDocument to avoid escaped json in our response/request strings * Fix JsonDoc tests * Linter fixes * Fix ApiKey Command and add tests (#1949) * Fix ApiKey command * Formatting * Fix test failures introduced in #1943 (#1957) * Remove "Did you know?" copy from emails. (#1962) * Remove "Did you know" * Remove jsonIf helper * Feature/fix send single sponsorship offer email (#1956) * Fix sponsorship offer email * Do not sanitize org name * PR feedback * Feature/f4e sync event [PS-75] (#1963) * Create sponsorship sync event type * Add InstallationId to Event model * Add combinatorics-based test case generators * Log sponsorships sync event on sync * Linter and test fixes * Fix failing test * Migrate sprocs and view * Remove unused `using`s * [PS-190] Add manual sync trigger in self hosted (#1955) * WIP add button to admin project for billing sync * add connection table to view page * minor fixes for self hosted side of sync * fixes number of bugs for cloud side of sync * deserialize before returning for some reason * add json attributes to return models * list of sponsorships parameter is immutable, add secondary list * change sproc name * add error handling * Fix tests * modify call to connection * Update src/Admin/Controllers/OrganizationsController.cs Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * undo change to sproc name * simplify logic * Update src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * register services despite if self hosted or cloud * remove json properties * revert merge conflict Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Update OrganizationSponsorship valid until when updating org expirati… (#1966) * Update OrganizationSponsorship valid until when updating org expiration date * Linter fixes * [PS-7] change revert email copy and add ValidUntil to sponsorship (#1965) * change revert email copy and add ValidUntil to sponsorship * add 15 days if no ValidUntil * Chore/merge/self hosted families for enterprise (#1972) * Log swallowed HttpRequestExceptions (#1866) Co-authored-by: Hinton <oscar@oscarhinton.com> * Allow for utilization of readonly db connection (#1937) * Bump the pin of the download-artifacts action to bypass the broken GitHub api (#1952) * Bumped version to 1.48.0 (#1958) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-160] Give Provider Users access to all org ciphers and collections (#1959) * Bumped version to 1.48.1 (#1961) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Avoid sending "user need confirmation" emails when there are no org admins (#1960) * Remove noncompliant users for new policies (#1951) * [PS-284] Allow installation clients to not need a user. (#1968) * Allow installation clients to not need a user. * Run formatting Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com> Co-authored-by: Hinton <oscar@oscarhinton.com> Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Justin Baur <136baur@gmail.com> * Fix/license file not found (#1974) * Handle null license * Throw hint message if license is not found by the admin project. * Use CloudOrganizationId from Connection config * Change test to support change * Fix test Co-authored-by: Matt Gibson <mgibson@bitwarden.com> * Feature/f4e selfhosted rename migration to .sql (#1971) * rename migration to .sql * format * Add unit tests to self host F4E (#1975) * Work on tests * Added more tests * Run linting * Address PR feedback * Fix AssertRecent * Linting * Fixed empty tests * Fix/misc self hosted f4e (#1973) * Allow setting of ApiUri * Return updates sponsorshipsData objects * Bind arguments by name * Greedy load sponsorships to email. When upsert was called, it creates Ids on _all_ records, which meant that the lazy-evaluation from this call always returned an empty list. * add scope for sync command DI in job. simplify error logic * update the sync job to get CloudOrgId from the BillingSyncKey Co-authored-by: Jacob Fink <jfink@bitwarden.com> * Chore/merge/self hosted families for enterprise (#1987) * Log swallowed HttpRequestExceptions (#1866) Co-authored-by: Hinton <oscar@oscarhinton.com> * Allow for utilization of readonly db connection (#1937) * Bump the pin of the download-artifacts action to bypass the broken GitHub api (#1952) * Bumped version to 1.48.0 (#1958) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * [EC-160] Give Provider Users access to all org ciphers and collections (#1959) * Bumped version to 1.48.1 (#1961) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Avoid sending "user need confirmation" emails when there are no org admins (#1960) * Remove noncompliant users for new policies (#1951) * [PS-284] Allow installation clients to not need a user. (#1968) * Allow installation clients to not need a user. * Run formatting * Use accept flow for sponsorship offers (#1964) * PS-82 check send 2FA email for new devices on TwoFactorController send-email-login (#1977) * [Bug] Skip WebAuthn 2fa event logs during login flow (#1978) * [Bug] Supress WebAuthn 2fa event logs during login process * Formatting * Simplified method call with new paramter input * Update RealIps Description (#1980) Describe the syntax of the real_ips configuration key with an example, to prevent type errors in the `setup` container when parsing `config.yml` * add proper URI validation to duo host (#1984) * captcha scores (#1967) * captcha scores * some api fixes * check bot on captcha attribute * Update src/Core/Services/Implementations/HCaptchaValidationService.cs Co-authored-by: e271828- <e271828-@users.noreply.github.com> Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> Co-authored-by: e271828- <e271828-@users.noreply.github.com> * ensure no path specific in duo host (#1985) Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com> Co-authored-by: Hinton <oscar@oscarhinton.com> Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Justin Baur <136baur@gmail.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Jordan Cooks <notnamed@users.noreply.github.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com> Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> Co-authored-by: e271828- <e271828-@users.noreply.github.com> * Address feedback (#1990) Co-authored-by: Justin Baur <admin@justinbaur.com> Co-authored-by: Carlos Muentes <cmuentes@bitwarden.com> Co-authored-by: Jake Fink <jfink@bitwarden.com> Co-authored-by: Justin Baur <136baur@gmail.com> Co-authored-by: Andrei <30410186+Manolachi@users.noreply.github.com> Co-authored-by: Hinton <oscar@oscarhinton.com> Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com> Co-authored-by: Joseph Flinn <58369717+joseph-flinn@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Jordan Cooks <notnamed@users.noreply.github.com> Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com> Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> Co-authored-by: e271828- <e271828-@users.noreply.github.com>
2022-05-10 17:12:09 -04:00
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;
2022-08-29 14:53:16 -04:00
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;
2022-08-29 14:53:16 -04:00
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)
2022-08-29 14:53:16 -04:00
{
var isBot = (validatorContext.CaptchaResponse?.IsBot ?? false);
if (isBot)
2022-08-29 14:53:16 -04:00
{
_logger.LogInformation(Constants.BypassFiltersEventId,
"Login attempt for {0} detected as a captcha bot with score {1}.",
request.UserName, validatorContext.CaptchaResponse.Score);
2022-08-29 14:53:16 -04:00
}
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);
2022-08-29 14:53:16 -04:00
var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User;
if (!valid)
{
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
}
if (!valid || isBot)
2022-08-29 14:53:16 -04:00
{
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
2022-08-29 14:53:16 -04:00
return;
}
var (isTwoFactorRequired, requires2FABecauseNewDevice, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request);
if (isTwoFactorRequired)
2022-08-29 14:53:16 -04:00
{
// 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);
2022-08-29 14:53:16 -04:00
return;
}
2022-08-29 14:53:16 -04:00
}
else
2022-08-29 14:53:16 -04:00
{
twoFactorRequest = false;
twoFactorRemember = false;
twoFactorToken = null;
2022-08-29 14:53:16 -04:00
}
// Returns true if can finish validation process
if (await IsValidAuthTypeAsync(user, request.GrantType))
2022-08-29 14:53:16 -04:00
{
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);
}
2022-08-29 14:53:16 -04:00
else
{
SetSsoResult(context, new Dictionary<string, object>
2022-08-29 14:53:16 -04:00
{{
"ErrorModel", new ErrorResponseModel("SSO authentication is required.")
2022-08-29 14:53:16 -04:00
}});
}
}
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
2022-08-29 14:53:16 -04:00
{
await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);
2022-08-29 14:53:16 -04:00
var claims = new List<Claim>();
2022-08-29 14:53:16 -04:00
if (device != null)
{
claims.Add(new Claim("device", device.Identifier));
2022-08-29 14:53:16 -04:00
}
var customResponse = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
2022-08-29 14:53:16 -04:00
{
customResponse.Add("PrivateKey", user.PrivateKey);
2022-08-29 14:53:16 -04:00
}
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)
2022-08-29 14:53:16 -04:00
{
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
customResponse.Add("TwoFactorToken", token);
}
await ResetFailedAuthDetailsAsync(user);
await SetSuccessResult(context, user, claims, customResponse);
2022-08-29 14:53:16 -04:00
}
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context, bool requires2FABecauseNewDevice)
2022-08-29 14:53:16 -04:00
{
var providerKeys = new List<byte>();
var providers = new Dictionary<string, Dictionary<string, object>>();
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
if (organization?.GetTwoFactorProviders() != null)
2022-08-29 14:53:16 -04:00
{
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);
}
}
2022-08-29 14:53:16 -04:00
}
if (!enabledProviders.Any())
2022-08-29 14:53:16 -04:00
{
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
2022-08-29 14:53:16 -04:00
};
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 }
2022-08-29 14:53:16 -04:00
});
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
2022-08-29 14:53:16 -04:00
{
// Send email now if this is their only 2FA method
await _userService.SendTwoFactorEmailAsync(user, requires2FABecauseNewDevice);
}
2022-08-29 14:53:16 -04:00
}
protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)
2022-08-29 14:53:16 -04:00
{
if (user != null)
2022-08-29 14:53:16 -04:00
{
await _eventService.LogUserEventAsync(user.Id,
twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn);
}
if (_globalSettings.SelfHosted)
2022-08-29 14:53:16 -04:00
{
_logger.LogWarning(Constants.BypassFiltersEventId,
string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".",
$" {_currentContext.IpAddress}"));
2022-08-29 14:53:16 -04:00
}
await Task.Delay(2000); // Delay for brute force.
SetErrorResult(context,
new Dictionary<string, object>
2022-08-29 14:53:16 -04:00
{{
"ErrorModel", new ErrorResponseModel(message)
2022-08-29 14:53:16 -04:00
}});
}
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);
2022-08-29 14:53:16 -04:00
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
2022-08-29 14:53:16 -04:00
private async Task<Tuple<bool, bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
2022-08-29 14:53:16 -04:00
{
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())
2022-08-29 14:53:16 -04:00
{
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());
}
2022-08-29 14:53:16 -04:00
}
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)
2022-08-29 14:53:16 -04:00
{
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())
2022-08-29 14:53:16 -04:00
{
// 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;
2022-08-29 14:53:16 -04:00
}
private Device GetDeviceFromRequest(ValidatedRequest request)
2022-08-29 14:53:16 -04:00
{
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))
2022-08-29 14:53:16 -04:00
{
return null;
}
return new Device
2022-08-29 14:53:16 -04:00
{
Identifier = deviceIdentifier,
Name = deviceName,
Type = type,
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
2022-08-29 14:53:16 -04:00
};
}
private void BeforeVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
2022-08-29 14:53:16 -04:00
{
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
}
});
}
2022-08-29 14:53:16 -04:00
}
private void AfterVerifyTwoFactor(User user, TwoFactorProviderType type, bool requires2FABecauseNewDevice)
2022-08-29 14:53:16 -04:00
{
if (type == TwoFactorProviderType.Email && requires2FABecauseNewDevice)
{
user.ClearTwoFactorProviders();
}
2022-08-29 14:53:16 -04:00
}
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
string token)
2022-08-29 14:53:16 -04:00
{
switch (type)
{
case TwoFactorProviderType.Authenticator:
case TwoFactorProviderType.Email:
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.YubiKey:
2021-03-22 23:21:43 +01:00
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)
2022-08-29 14:53:16 -04:00
{
return false;
2022-08-29 14:53:16 -04:00
}
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
2022-08-29 14:53:16 -04:00
default:
return false;
}
2022-08-29 14:53:16 -04:00
}
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
TwoFactorProviderType type, TwoFactorProvider provider)
2022-08-29 14:53:16 -04:00
{
switch (type)
{
case TwoFactorProviderType.Duo:
2021-03-22 23:21:43 +01:00
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)
2022-08-29 14:53:16 -04:00
{
return new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["Signature"] = token
};
}
2021-03-22 23:21:43 +01:00
else if (type == TwoFactorProviderType.WebAuthn)
2022-08-29 14:53:16 -04:00
{
if (token == null)
2021-03-22 23:21:43 +01:00
{
return null;
2021-03-22 23:21:43 +01:00
}
2022-08-29 14:53:16 -04:00
2021-03-22 23:21:43 +01:00
return JsonSerializer.Deserialize<Dictionary<string, object>>(token);
2022-08-29 14:53:16 -04:00
}
else if (type == TwoFactorProviderType.Email)
2022-08-29 14:53:16 -04:00
{
return new Dictionary<string, object>
{
["Email"] = token
};
}
else if (type == TwoFactorProviderType.YubiKey)
2022-08-29 14:53:16 -04:00
{
return new Dictionary<string, object>
{
["Nfc"] = (bool)provider.MetaData["Nfc"]
};
}
return null;
case TwoFactorProviderType.OrganizationDuo:
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
2022-08-29 14:53:16 -04:00
{
return new Dictionary<string, object>
{
["Host"] = provider.MetaData["Host"],
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
};
}
return null;
default:
return null;
}
2022-08-29 14:53:16 -04:00
}
protected async Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request) =>
(await GetKnownDeviceAsync(user, request)) != default;
protected async Task<Device> GetKnownDeviceAsync(User user, ValidatedTokenRequest request)
2022-08-29 14:53:16 -04:00
{
if (user == null)
{
return default;
}
return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id);
2022-08-29 14:53:16 -04:00
}
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
2022-08-29 14:53:16 -04:00
{
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))
2022-08-29 14:53:16 -04:00
{
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;
}
2022-08-29 14:53:16 -04:00
return null;
}
private async Task ResetFailedAuthDetailsAsync(User user)
2022-08-29 14:53:16 -04:00
{
// 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);
2022-08-29 14:53:16 -04:00
}
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
2022-08-29 14:53:16 -04:00
{
if (user == null)
2022-08-29 14:53:16 -04:00
{
return;
}
var utcNow = DateTime.UtcNow;
user.FailedLoginCount = ++user.FailedLoginCount;
user.LastFailedLoginDate = user.RevisionDate = utcNow;
await _userRepository.ReplaceAsync(user);
2022-08-29 14:53:16 -04:00
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
{
if (twoFactorInvalid)
{
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
}
else
{
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, _currentContext.IpAddress);
}
}
2022-08-29 14:53:16 -04:00
}
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
{
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
var failedLoginCount = user?.FailedLoginCount ?? 0;
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
}
}