1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23:52:50 -05:00

[PM-8107] Remove Duo v2 from server (#4934)

refactor(TwoFactorAuthentication): Remove references to old Duo SDK version 2 code and replace them with the Duo SDK version 4 supported library DuoUniversal code.

Increased unit test coverage in the Two Factor Authentication code space. We opted to use DI instead of Inheritance for the Duo and OrganizaitonDuo two factor tokens to increase testability, since creating a testing mock of the Duo.Client was non-trivial.

Reviewed-by: @JaredSnider-Bitwarden
This commit is contained in:
Ike
2024-11-18 15:58:05 -08:00
committed by GitHub
parent e16cad50b1
commit ab5d4738d6
36 changed files with 1412 additions and 1369 deletions

View File

@ -1,5 +1,5 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Entities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;

View File

@ -19,7 +19,6 @@ public abstract class BaseTokenProviderTests<T>
{
public abstract TwoFactorProviderType TwoFactorProviderType { get; }
#region Helpers
protected static IEnumerable<object[]> SetupCanGenerateData(params (Dictionary<string, object> MetaData, bool ExpectedResponse)[] data)
{
return data.Select(d =>
@ -48,6 +47,9 @@ public abstract class BaseTokenProviderTests<T>
userService
.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user)
.Returns(true);
userService
.CanAccessPremium(user)
.Returns(true);
}
protected static UserManager<User> SubstituteUserManager()
@ -76,7 +78,6 @@ public abstract class BaseTokenProviderTests<T>
user.TwoFactorProviders = JsonHelpers.LegacySerialize(providers);
}
#endregion
public virtual async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,
User user, SutProvider<T> sutProvider)

View File

@ -0,0 +1,262 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
using Bit.Core.Tokens;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using Duo = DuoUniversal;
namespace Bit.Core.Test.Auth.Identity;
public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests<DuoUniversalTokenProvider>
{
private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For<IDuoUniversalTokenService>();
public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo;
public static IEnumerable<object[]> CanGenerateTwoFactorTokenAsyncData
=> SetupCanGenerateData(
( // correct data
new Dictionary<string, object>
{
["ClientId"] = new string('c', 20),
["ClientSecret"] = new string('s', 40),
["Host"] = "https://api-abcd1234.duosecurity.com",
},
true
),
( // correct data duo federal
new Dictionary<string, object>
{
["ClientId"] = new string('c', 20),
["ClientSecret"] = new string('s', 40),
["Host"] = "https://api-abcd1234.duofederal.com",
},
true
),
( // correct data duo federal
new Dictionary<string, object>
{
["ClientId"] = new string('c', 20),
["ClientSecret"] = new string('s', 40),
["Host"] = "https://api-abcd1234.duofederal.com",
},
true
),
( // invalid host
new Dictionary<string, object>
{
["ClientId"] = new string('c', 20),
["ClientSecret"] = new string('s', 40),
["Host"] = "",
},
false
),
( // clientId missing
new Dictionary<string, object>
{
["ClientSecret"] = new string('s', 40),
["Host"] = "https://api-abcd1234.duofederal.com",
},
false
)
);
public static IEnumerable<object[]> NonPremiumCanGenerateTwoFactorTokenAsyncData
=> SetupCanGenerateData(
( // correct data
new Dictionary<string, object>
{
["ClientId"] = new string('c', 20),
["ClientSecret"] = new string('s', 40),
["Host"] = "https://api-abcd1234.duosecurity.com",
},
false
)
);
[Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))]
public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary<string, object> metaData, bool expectedResponse,
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
{
// Arrange
user.Premium = true;
user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(expectedResponse);
// Act
// Assert
await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);
}
[Theory, BitMemberAutoData(nameof(NonPremiumCanGenerateTwoFactorTokenAsyncData))]
public async Task CanGenerateTwoFactorTokenAsync_UserCanNotAccessPremium_ReturnsNull(Dictionary<string, object> metaData, bool expectedResponse,
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
{
// Arrange
user.Premium = false;
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(expectedResponse);
// Act
// Assert
await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);
}
[Theory]
[BitAutoData]
public async Task GenerateToken_Success_ReturnsAuthUrl(
User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string authUrl)
{
// Arrange
SetUpProperDuoUniversalTokenService(user, sutProvider);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.GenerateAuthUrl(
Arg.Any<Duo.Client>(),
Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(),
user)
.Returns(authUrl);
// Act
var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user);
// Assert
Assert.NotNull(token);
Assert.Equal(token, authUrl);
}
[Theory]
[BitAutoData]
public async Task GenerateToken_DuoClientNull_ReturnsNull(
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
{
// Arrange
user.Premium = true;
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
AdditionalSetup(sutProvider, user);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(true);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
.Returns(null as Duo.Client);
// Act
var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user);
// Assert
Assert.Null(token);
}
[Theory]
[BitAutoData]
public async Task GenerateToken_UserCanNotAccessPremium_ReturnsNull(
User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
{
// Arrange
user.Premium = false;
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
AdditionalSetup(sutProvider, user);
// Act
var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user);
// Assert
Assert.Null(token);
}
[Theory]
[BitAutoData]
public async Task ValidateToken_ValidToken_ReturnsTrue(
User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string token)
{
// Arrange
SetUpProperDuoUniversalTokenService(user, sutProvider);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.RequestDuoValidationAsync(
Arg.Any<Duo.Client>(),
Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(),
user,
token)
.Returns(true);
// Act
var response = await sutProvider.Sut.ValidateAsync("purpose", token, SubstituteUserManager(), user);
// Assert
Assert.True(response);
}
[Theory]
[BitAutoData]
public async Task ValidateToken_DuoClientNull_ReturnsFalse(
User user, SutProvider<DuoUniversalTokenProvider> sutProvider, string token)
{
user.Premium = true;
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
AdditionalSetup(sutProvider, user);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(true);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
.Returns(null as Duo.Client);
// Act
var result = await sutProvider.Sut.ValidateAsync("purpose", token, SubstituteUserManager(), user);
// Assert
Assert.False(result);
}
/// <summary>
/// Ensures that the IDuoUniversalTokenService is properly setup for the test.
/// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider
/// methods will return true enabling the test to execute on the correct path.
/// </summary>
/// <param name="user">user from calling test</param>
/// <param name="sutProvider">self</param>
private void SetUpProperDuoUniversalTokenService(User user, SutProvider<DuoUniversalTokenProvider> sutProvider)
{
user.Premium = true;
user.TwoFactorProviders = GetTwoFactorDuoProvidersJson();
var client = BuildDuoClient();
AdditionalSetup(sutProvider, user);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(true);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
.Returns(client);
}
private Duo.Client BuildDuoClient()
{
var clientId = new string('c', 20);
var clientSecret = new string('s', 40);
return new Duo.ClientBuilder(clientId, clientSecret, "api-abcd1234.duosecurity.com", "redirectUrl").Build();
}
private string GetTwoFactorDuoProvidersJson()
{
return
"{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
}
}

View File

@ -1,5 +1,5 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Entities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;

View File

@ -0,0 +1,289 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
using Bit.Core.Tokens;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using Duo = DuoUniversal;
namespace Bit.Core.Test.Auth.Identity;
[SutProviderCustomize]
public class OrganizationDuoUniversalTwoFactorTokenProviderTests
{
private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For<IDuoUniversalTokenService>();
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = Substitute.For<IDataProtectorTokenFactory<DuoUserStateTokenable>>();
// Happy path
[Theory]
[BitAutoData]
public async Task CanGenerateTwoFactorTokenAsync_ReturnsTrue(
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
// Arrange
organization.Enabled = true;
organization.Use2fa = true;
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
// Act
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData]
public async Task CanGenerateTwoFactorTokenAsync_DuoTwoFactorNotEnabled_ReturnsFalse(
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
// Arrange
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderNotEnabledJson();
organization.Use2fa = true;
organization.Enabled = true;
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(true);
// Act
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async Task CanGenerateTwoFactorTokenAsync_BadMetaData_ProviderNull_ReturnsFalse(
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
// Arrange
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Use2fa = true;
organization.Enabled = true;
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(false);
// Act
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async Task GetDuoTwoFactorProvider_OrganizationNull_ReturnsNull(
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
// Act
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(null);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async Task GetDuoTwoFactorProvider_OrganizationNotEnabled_ReturnsNull(
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
// Arrange
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
organization.Enabled = false;
// Act
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async Task GetDuoTwoFactorProvider_OrganizationUse2FAFalse_ReturnsNull(
Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
// Arrange
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
organization.Use2fa = false;
// Act
var result = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(organization);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async Task GetDuoClient_ProviderNull_ReturnsNull(
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
// Act
var result = await sutProvider.Sut.GenerateAsync(null, default);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async Task GetDuoClient_DuoClientNull_ReturnsNull(
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
Organization organization)
{
// Arrange
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Use2fa = true;
organization.Enabled = true;
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(true);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
.Returns(null as Duo.Client);
// Act
var result = await sutProvider.Sut.GenerateAsync(organization, default);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async Task GenerateAsync_ReturnsAuthUrl(
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
Organization organization,
User user,
string AuthUrl)
{
// Arrange
SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.GenerateAuthUrl(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user)
.Returns(AuthUrl);
// Act
var result = await sutProvider.Sut.GenerateAsync(organization, user);
// Assert
Assert.NotNull(result);
Assert.Equal(AuthUrl, result);
}
[Theory]
[BitAutoData]
public async Task GenerateAsync_ClientNull_ReturnsNull(
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
Organization organization,
User user)
{
// Arrange
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
// Act
var result = await sutProvider.Sut.GenerateAsync(organization, user);
// Assert
Assert.Null(result);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_TokenValid_ReturnsTrue(
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
Organization organization,
User user,
string token)
{
// Arrange
SetUpProperOrganizationDuoUniversalTokenService(BuildDuoClient(), organization, sutProvider);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.RequestDuoValidationAsync(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user, token)
.Returns(true);
// Act
var result = await sutProvider.Sut.ValidateAsync(token, organization, user);
// Assert
Assert.True(result);
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_ClientNull_ReturnsFalse(
SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider,
Organization organization,
User user,
string token)
{
// Arrange
SetUpProperOrganizationDuoUniversalTokenService(null, organization, sutProvider);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.RequestDuoValidationAsync(Arg.Any<Duo.Client>(), Arg.Any<IDataProtectorTokenFactory<DuoUserStateTokenable>>(), user, token)
.Returns(true);
// Act
var result = await sutProvider.Sut.ValidateAsync(token, organization, user);
// Assert
Assert.False(result);
}
/// <summary>
/// Ensures that the IDuoUniversalTokenService is properly setup for the test.
/// This ensures that the private GetDuoClientAsync, and GetDuoTwoFactorProvider
/// methods will return true enabling the test to execute on the correct path.
///
/// BitAutoData cannot create the Duo.Client since it does not have a public constructor
/// so we have to use the ClientBUilder(), the client is not used meaningfully in the tests.
/// </summary>
/// <param name="user">user from calling test</param>
/// <param name="sutProvider">self</param>
private void SetUpProperOrganizationDuoUniversalTokenService(
Duo.Client client, Organization organization, SutProvider<OrganizationDuoUniversalTokenProvider> sutProvider)
{
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
organization.Enabled = true;
organization.Use2fa = true;
sutProvider.GetDependency<IDuoUniversalTokenService>()
.HasProperDuoMetadata(Arg.Any<TwoFactorProvider>())
.Returns(true);
sutProvider.GetDependency<IDuoUniversalTokenService>()
.BuildDuoTwoFactorClientAsync(Arg.Any<TwoFactorProvider>())
.Returns(client);
}
private Duo.Client BuildDuoClient()
{
var clientId = new string('c', 20);
var clientSecret = new string('s', 40);
return new Duo.ClientBuilder(clientId, clientSecret, "api-abcd1234.duosecurity.com", "redirectUrl").Build();
}
private string GetTwoFactorOrganizationDuoProviderJson()
{
return
"{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
}
private string GetTwoFactorOrganizationDuoProviderNotEnabledJson()
{
return
"{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
}
}

View File

@ -0,0 +1,91 @@
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.Models;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Auth.Services;
[SutProviderCustomize]
public class DuoUniversalTokenServiceTests
{
[Theory]
[BitAutoData("", "ClientId", "ClientSecret")]
[BitAutoData("api-valid.duosecurity.com", "", "ClientSecret")]
[BitAutoData("api-valid.duosecurity.com", "ClientId", "")]
public async void ValidateDuoConfiguration_InvalidConfig_ReturnsFalse(
string host, string clientId, string clientSecret, SutProvider<DuoUniversalTokenService> sutProvider)
{
// Arrange
/* AutoData handles arrangement */
// Act
var result = await sutProvider.Sut.ValidateDuoConfiguration(clientSecret, clientId, host);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData(true, "api-valid.duosecurity.com")]
[BitAutoData(false, "invalid")]
[BitAutoData(false, "api-valid.duosecurity.com", null, "clientSecret")]
[BitAutoData(false, "api-valid.duosecurity.com", "ClientId", null)]
[BitAutoData(false, "api-valid.duosecurity.com", null, null)]
public void HasProperDuoMetadata_ReturnMatchesExpected(
bool expectedResponse, string host, string clientId,
string clientSecret, SutProvider<DuoUniversalTokenService> sutProvider)
{
// Arrange
var metaData = new Dictionary<string, object> { ["Host"] = host };
if (clientId != null)
{
metaData.Add("ClientId", clientId);
}
if (clientSecret != null)
{
metaData.Add("ClientSecret", clientSecret);
}
var provider = new TwoFactorProvider
{
MetaData = metaData
};
// Act
var result = sutProvider.Sut.HasProperDuoMetadata(provider);
// Assert
Assert.Equal(result, expectedResponse);
}
[Theory]
[BitAutoData]
public void HasProperDuoMetadata_ProviderIsNull_ReturnsFalse(
SutProvider<DuoUniversalTokenService> sutProvider)
{
// Act
var result = sutProvider.Sut.HasProperDuoMetadata(null);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData("api-valid.duosecurity.com", true)]
[BitAutoData("api-valid.duofederal.com", true)]
[BitAutoData("invalid", false)]
public void ValidDuoHost_HostIsValid_ReturnTrue(
string host, bool expectedResponse)
{
// Act
var result = DuoUniversalTokenService.ValidDuoHost(host);
// Assert
Assert.Equal(result, expectedResponse);
}
}