Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7a0e73f
[PM-40673 Part 2] update validators
rr-bw Sep 23, 2026
ba3e8c6
[PM-40673 Part 2] accept old tokens and automatically upgrade to new …
rr-bw Sep 23, 2026
d3eab66
[PM-40673 Part 2] split line into 2 lines, update comment
rr-bw Oct 8, 2026
a736aee
[PM-40673 Part 2] simplify comment
rr-bw Oct 9, 2026
f90b553
[PM-40673 Part 2] update TODO comment to point to specific Jira ticket
rr-bw Oct 9, 2026
a2e0b3f
[PM-40673 Part 2] update 2FA remember branch comment for clarity
rr-bw Oct 9, 2026
236fdf3
[PM-40673 Part 2] remove test summary comment prefixes
rr-bw Oct 9, 2026
908bbc6
[PM-40673 Part 2] remove test summary comment prefixes from Remembere…
rr-bw Oct 9, 2026
8c3d919
[PM-40673 Part 2] remove unnecessary usings
rr-bw Oct 9, 2026
5d61a99
[PM-40673 Part 2] simplify comments on IdentityApplicationFactory.cs …
rr-bw Oct 9, 2026
9563523
[PM-40673 Part 2] remove old file
rr-bw Oct 9, 2026
989b551
[PM-40673 Part 2] add check for org-required-2FA when checking if 2FA…
rr-bw Oct 9, 2026
5ca46f0
[PM-40673 Part 2] add organization to arguments
rr-bw Oct 9, 2026
b4a5685
[PM-40673 Part 2] run dotnet format
rr-bw Oct 9, 2026
17585fc
[PM-40673 Part 2] remove 2fa-enabled check from VerifyTwoFactorAsync
rr-bw Oct 14, 2026
c8c5e31
[PM-40673 Part 2] refactor legacy format check to make legacy path re…
rr-bw Oct 17, 2026
0ec9aaa
[PM-40673 Part 2] change remember token query call back to three argu…
rr-bw Oct 17, 2026
9212d29
[PM-40673 Part 2] cover TwoFactorRemember=1 in the no-reissue remembe…
rr-bw Oct 17, 2026
89faab1
[PM-40673 Part 2] fix remembered device tests for device-bound email …
rr-bw Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Identity/IdentityServer/CustomValidatorRequestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,14 @@ public class CustomValidatorRequestContext
/// Whether the user has requested a Remember Me token for their current device.
/// </summary>
public bool RememberMeRequested { get; set; } = false;
/// <summary>
/// Whether the Remember Me token presented on this request was in the previous format and should
/// be replaced with one in the current format on this response.
/// </summary>
/// <remarks>
/// Distinct from <see cref="RememberMeRequested"/> on purpose. It is set only when a
/// previous-format token was accepted, never merely because the request used the Remember
/// provider — which is what keeps a remember-login from extending its own lifetime.
/// </remarks>
public bool RememberUpgradeRequired { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.Devices.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
Expand All @@ -22,7 +23,6 @@
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity;

Expand All @@ -42,6 +42,7 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly IMailService _mailService;
private readonly IClientVersionValidator _clientVersionValidator;
protected readonly IUpdateDeviceLastActivityCommand _updateDeviceLastActivityCommand;
private readonly IIssueTwoFactorRememberTokenCommand _issueTwoFactorRememberTokenCommand;

protected ICurrentContext CurrentContext { get; }
protected Bitwarden.Server.Sdk.Features.IFeatureService _featureService { get; }
Expand Down Expand Up @@ -70,7 +71,8 @@ public BaseRequestValidator(
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator,
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand,
IIssueTwoFactorRememberTokenCommand issueTwoFactorRememberTokenCommand
)
{
_userManager = userManager;
Expand All @@ -92,6 +94,7 @@ IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand
_accountKeysQuery = userAccountKeysQuery;
_clientVersionValidator = clientVersionValidator;
_updateDeviceLastActivityCommand = updateDeviceLastActivityCommand;
_issueTwoFactorRememberTokenCommand = issueTwoFactorRememberTokenCommand;
}

// NOTE: Feature flags with progressive rollout (device-keyed or user-keyed) cannot be
Expand All @@ -113,8 +116,12 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
return;
}

// A remember token is issued either because the user asked for one, or because they
// presented one in the previous format and it is being replaced. The second condition is
// set only on a successful previous-format validation — never merely because the request
// used the Remember provider, which would let an active user's remember-me renew forever.
await BuildSuccessResultAsync(validatorContext.User, context, validatorContext.Device,
validatorContext.RememberMeRequested);
validatorContext.RememberMeRequested || validatorContext.RememberUpgradeRequired);
}

protected async Task FailAuthForLegacyUserAsync(User user, T context)
Expand Down Expand Up @@ -321,10 +328,12 @@ private async Task<bool> ValidateTwoFactorAsync(T context, ValidatedTokenRequest
return false;
}

var twoFactorTokenValid =
var twoFactorResult =
await _twoFactorAuthenticationValidator
.VerifyTwoFactorAsync(validatorContext.User, twoFactorOrganization, twoFactorProviderType,
twoFactorToken, request.Raw["DeviceIdentifier"]);
var twoFactorTokenValid = twoFactorResult.Succeeded;
validatorContext.RememberUpgradeRequired = twoFactorResult.LegacyRememberUpgradeRequired;

// 3b. Response for 2FA required but request is not valid or remember token expired state.
if (!twoFactorTokenValid)
Expand Down Expand Up @@ -654,9 +663,9 @@ private async Task<Dictionary<string, object>> BuildCustomResponse(User user, T

if (sendRememberToken)
{
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
customResponse.Add("TwoFactorToken", token);
// The device is non-null here because this runs only after DeviceValidator succeeded
var rememberToken = await _issueTwoFactorRememberTokenCommand.IssueAsync(user, device);
customResponse.Add("TwoFactorToken", rememberToken);
}

return customResponse;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.Devices.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Queries.Interfaces;
Expand Down Expand Up @@ -47,7 +48,8 @@ public CustomTokenRequestValidator(
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator,
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand)
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand,
IIssueTwoFactorRememberTokenCommand issueTwoFactorRememberTokenCommand)
: base(
userManager,
userService,
Expand All @@ -67,7 +69,8 @@ public CustomTokenRequestValidator(
mailService,
userAccountKeysQuery,
clientVersionValidator,
updateDeviceLastActivityCommand)
updateDeviceLastActivityCommand,
issueTwoFactorRememberTokenCommand)
{
_userManager = userManager;
_updateInstallationCommand = updateInstallationCommand;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,20 @@ public interface ITwoFactorAuthenticationValidator
/// <param name="twoFactorProviderType">Two Factor Provider to use to verify the token</param>
/// <param name="token">secret passed from the user and consumed by the two-factor provider's verify method</param>
/// <param name="deviceIdentifier">
/// identifier of the device making the request; an email code only verifies for the device it was issued to
/// The client-generated device identifier from the raw request. An email code only verifies for the
/// device it was issued to, and a remember token is bound to its device.
/// </param>
/// <returns>boolean</returns>
Task<bool> VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType,
string token, string deviceIdentifier);
Task<TwoFactorVerificationResult> VerifyTwoFactorAsync(
User user,
Organization organization,
TwoFactorProviderType twoFactorProviderType,
string token,
string deviceIdentifier);
}

/// <param name="Succeeded">Whether the presented second factor was accepted.</param>
/// <param name="LegacyRememberUpgradeRequired">
/// Set when a remember token in the pre-<c>TwoFactorRememberTokenable</c> format was accepted, so
/// the caller knows to issue a replacement in the current format on this response.
/// </param>
public record TwoFactorVerificationResult(bool Succeeded, bool LegacyRememberUpgradeRequired = false);
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.Devices.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Queries.Interfaces;
Expand Down Expand Up @@ -44,7 +45,8 @@ public ResourceOwnerPasswordValidator(
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator,
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand)
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand,
IIssueTwoFactorRememberTokenCommand issueTwoFactorRememberTokenCommand)
: base(
userManager,
userService,
Expand All @@ -64,7 +66,8 @@ public ResourceOwnerPasswordValidator(
mailService,
userAccountKeysQuery,
clientVersionValidator,
updateDeviceLastActivityCommand)
updateDeviceLastActivityCommand,
issueTwoFactorRememberTokenCommand)
{
_userManager = userManager;
_currentContext = currentContext;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
Expand All @@ -32,8 +33,12 @@ public class TwoFactorAuthenticationValidator(
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
ICurrentContext currentContext,
ITwoFactorEmailService twoFactorEmailService) : ITwoFactorAuthenticationValidator
ITwoFactorEmailService twoFactorEmailService,
IValidateTwoFactorRememberTokenQuery validateTwoFactorRememberTokenQuery) : ITwoFactorAuthenticationValidator
{
private readonly IValidateTwoFactorRememberTokenQuery _validateTwoFactorRememberTokenQuery =
validateTwoFactorRememberTokenQuery;

private readonly IUserService _userService = userService;
private readonly UserManager<User> _userManager = userManager;
private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider;
Expand Down Expand Up @@ -120,7 +125,7 @@ public async Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User use
return twoFactorResultDict;
}

public async Task<bool> VerifyTwoFactorAsync(
public async Task<TwoFactorVerificationResult> VerifyTwoFactorAsync(
User user,
Organization organization,
TwoFactorProviderType type,
Expand All @@ -131,14 +136,15 @@ public async Task<bool> VerifyTwoFactorAsync(
{
if (organization.TwoFactorProviderIsEnabled(type))
{
return await _organizationDuoUniversalTokenProvider.ValidateAsync(token, organization, user);
return new TwoFactorVerificationResult(
await _organizationDuoUniversalTokenProvider.ValidateAsync(token, organization, user));
}
return false;
return new TwoFactorVerificationResult(false);
}

if (type is TwoFactorProviderType.RecoveryCode)
{
return await _userService.RecoverTwoFactorAsync(user, token);
return new TwoFactorVerificationResult(await _userService.RecoverTwoFactorAsync(user, token));
}

// These cases we want to always return false, U2f is deprecated and OrganizationDuo
Expand All @@ -147,31 +153,51 @@ public async Task<bool> VerifyTwoFactorAsync(
// provider flow. See IOrganizationDuoUniversalTokenProvider.cs
if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo)
{
return false;
return new TwoFactorVerificationResult(false);
}

// Remember is not a provider a user configures, so user.GetTwoFactorProvider(Remember) is always
// null and the lookup below would reject it. It is validated on its own terms here instead.
if (type is TwoFactorProviderType.Remember)
{
// TODO: PM-44171 - Remove the legacy token path. PM-40673 introduced a new remember token format and stopped
// issuing legacy format tokens. Legacy tokens have a 30 day expiration, so we must wait until more than 30 days
// have passed after the release of PM-40673 before we remove this legacy path.
var isLegacyFormat = token is null || !token.StartsWith(TwoFactorRememberTokenable.ClearTextPrefix, StringComparison.Ordinal);

if (isLegacyFormat)
{
var legacyTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, CoreHelpers.CustomProviderName(type), token);

// Accepting a legacy format token earns a replacement in the new format on this same response,
// so the device migrates over on this login rather than waiting out the legacy token's lifetime.
return new TwoFactorVerificationResult(legacyTokenValid, legacyTokenValid);
}

return new TwoFactorVerificationResult(
await _validateTwoFactorRememberTokenQuery.ValidateAsync(user, deviceIdentifier, token));
}

// Now we are concerning the rest of the Two Factor Provider Types

// The intent of this check is to make sure that the user is using a 2FA provider that
// is enabled and allowed by their premium status.
// The exception for Remember is because it is a "special" 2FA type that isn't ever explicitly
// enabled by a user, so we can't check the user's 2FA providers to see if they're
// enabled. We just have to check if the token is valid.
if (type != TwoFactorProviderType.Remember &&
user.GetTwoFactorProvider(type) == null)
if (user.GetTwoFactorProvider(type) == null)
{
return false;
return new TwoFactorVerificationResult(false);
}

// Email codes are bound to the device they were issued to, so they are verified with that device.
if (type == TwoFactorProviderType.Email)
{
return await _twoFactorEmailService.VerifyTwoFactorLoginTokenAsync(user, deviceIdentifier, token);
return new TwoFactorVerificationResult(
await _twoFactorEmailService.VerifyTwoFactorLoginTokenAsync(user, deviceIdentifier, token));
}

// Finally, verify the token based on the provider type.
return await _userManager.VerifyTwoFactorTokenAsync(
user, CoreHelpers.CustomProviderName(type), token);
return new TwoFactorVerificationResult(
await _userManager.VerifyTwoFactorTokenAsync(user, CoreHelpers.CustomProviderName(type), token));
}

private async Task<List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>> GetEnabledTwoFactorProvidersAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.Devices.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
using Bit.Core.Context;
using Bit.Core.Entities;
Expand Down Expand Up @@ -50,7 +51,8 @@ public WebAuthnGrantValidator(
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator,
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand)
IUpdateDeviceLastActivityCommand updateDeviceLastActivityCommand,
IIssueTwoFactorRememberTokenCommand issueTwoFactorRememberTokenCommand)
: base(
userManager,
userService,
Expand All @@ -70,7 +72,8 @@ public WebAuthnGrantValidator(
mailService,
userAccountKeysQuery,
clientVersionValidator,
updateDeviceLastActivityCommand)
updateDeviceLastActivityCommand,
issueTwoFactorRememberTokenCommand)
{
_assertionOptionsDataProtector = assertionOptionsDataProtector;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
Expand Down
Loading
Loading