mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05:00
Feature/token service (#1785)
* Implement draft token service * Add tokenizer and factory * Handle expiring tokens through base class * Allow direct token validity checks * Add safe unprotect to tokenizer * Add interface to tokenizer factory * Use tokenizer * Fix rebase * Handle cleartext prefix in tokenizer base * Use epoch milliseconds for expiration in tokens * Use tokenizers * Test tokens * Linter fixes * Add TokenizerFactory to DI services * Test epoch milliseconds deserialization * Use separate injectables for each token type * Fix directory * Add functional unprotect to token * Fix namespace and correct object names * Remove Tokenable interface * Test remaining Tokens classes * Dotnet format * Fix sut provider errors with update * Remove useless property Co-authored-by: Hinton <oscar@oscarhinton.com>
This commit is contained in:
@ -118,6 +118,11 @@ namespace Bit.Test.Common.AutoFixture
|
||||
{
|
||||
return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name);
|
||||
}
|
||||
// Return default type if set
|
||||
else if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, ""))
|
||||
{
|
||||
return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
|
||||
}
|
||||
|
||||
|
||||
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
|
||||
|
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Tokens;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Business.Tokenables
|
||||
{
|
||||
public class EmergencyAccessInviteTokenableTests
|
||||
{
|
||||
[Theory, AutoData]
|
||||
public void SerializationSetsCorrectDateTime(EmergencyAccess emergencyAccess)
|
||||
{
|
||||
var token = new EmergencyAccessInviteTokenable(emergencyAccess, 2);
|
||||
Assert.Equal(Tokenable.FromToken<EmergencyAccessInviteTokenable>(token.ToToken().ToString()).ExpirationDate,
|
||||
token.ExpirationDate,
|
||||
TimeSpan.FromMilliseconds(10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsInvalidIfIdentifierIsWrong()
|
||||
{
|
||||
var token = new EmergencyAccessInviteTokenable(DateTime.MaxValue)
|
||||
{
|
||||
Email = "email",
|
||||
Id = Guid.NewGuid(),
|
||||
Identifier = "not correct"
|
||||
};
|
||||
|
||||
Assert.False(token.Valid);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Tokens;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Business.Tokenables
|
||||
{
|
||||
public class HCaptchaTokenableTests
|
||||
{
|
||||
[Theory, AutoData]
|
||||
public void CanUpdateExpirationToNonStandard(User user)
|
||||
{
|
||||
var token = new HCaptchaTokenable(user)
|
||||
{
|
||||
ExpirationDate = DateTime.MinValue
|
||||
};
|
||||
|
||||
Assert.Equal(DateTime.MinValue, token.ExpirationDate, TimeSpan.FromMilliseconds(10));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void SetsDataFromUser(User user)
|
||||
{
|
||||
var token = new HCaptchaTokenable(user);
|
||||
|
||||
Assert.Equal(user.Id, token.Id);
|
||||
Assert.Equal(user.Email, token.Email);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void SerializationSetsCorrectDateTime(User user)
|
||||
{
|
||||
var expectedDateTime = DateTime.UtcNow.AddHours(-5);
|
||||
var token = new HCaptchaTokenable(user)
|
||||
{
|
||||
ExpirationDate = expectedDateTime
|
||||
};
|
||||
|
||||
var result = Tokenable.FromToken<HCaptchaTokenable>(token.ToToken());
|
||||
|
||||
Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void IsInvalidIfIdentifierIsWrong(User user)
|
||||
{
|
||||
var token = new HCaptchaTokenable(user)
|
||||
{
|
||||
Identifier = "not correct"
|
||||
};
|
||||
|
||||
Assert.False(token.Valid);
|
||||
}
|
||||
}
|
||||
}
|
54
test/Core.Test/Tokens/DataProtectorTokenFactoryTests.cs
Normal file
54
test/Core.Test/Tokens/DataProtectorTokenFactoryTests.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tokens
|
||||
{
|
||||
[SutProviderCustomize]
|
||||
public class DataProtectorTokenFactoryTests
|
||||
{
|
||||
public static SutProvider<DataProtectorTokenFactory<TestTokenable>> GetSutProvider()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
return new SutProvider<DataProtectorTokenFactory<TestTokenable>>(fixture)
|
||||
.SetDependency<IDataProtectionProvider>(fixture.Create<EphemeralDataProtectionProvider>())
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void CanRoundTripTokenables(TestTokenable tokenable)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
|
||||
var token = sutProvider.Sut.Protect(tokenable);
|
||||
var recoveredTokenable = sutProvider.Sut.Unprotect(token);
|
||||
|
||||
AssertHelper.AssertPropertyEqual(tokenable, recoveredTokenable);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void PrependsClearText(TestTokenable tokenable)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
|
||||
var token = sutProvider.Sut.Protect(tokenable);
|
||||
|
||||
Assert.StartsWith(sutProvider.GetDependency<string>("clearTextPrefix"), token);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void EncryptsToken(TestTokenable tokenable)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var prefix = sutProvider.GetDependency<string>("clearTextPrefix");
|
||||
|
||||
var token = sutProvider.Sut.Protect(tokenable);
|
||||
|
||||
Assert.NotEqual(new Token(token).RemovePrefix(prefix), tokenable.ToToken());
|
||||
}
|
||||
}
|
||||
}
|
72
test/Core.Test/Tokens/ExpiringTokenTests.cs
Normal file
72
test/Core.Test/Tokens/ExpiringTokenTests.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tokens
|
||||
{
|
||||
public class ExpiringTokenTests
|
||||
{
|
||||
[Theory, AutoData]
|
||||
public void ExpirationSerializesToEpochMilliseconds(DateTime expirationDate)
|
||||
{
|
||||
var sut = new TestExpiringTokenable
|
||||
{
|
||||
ExpirationDate = expirationDate
|
||||
};
|
||||
|
||||
var result = JsonSerializer.Serialize(sut);
|
||||
var expectedDate = CoreHelpers.ToEpocMilliseconds(expirationDate);
|
||||
|
||||
Assert.Contains($"\"ExpirationDate\":{expectedDate}", result);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void ExpirationSerializationRoundTrip(DateTime expirationDate)
|
||||
{
|
||||
var sut = new TestExpiringTokenable
|
||||
{
|
||||
ExpirationDate = expirationDate
|
||||
};
|
||||
|
||||
var intermediate = JsonSerializer.Serialize(sut);
|
||||
var result = JsonSerializer.Deserialize<TestExpiringTokenable>(intermediate);
|
||||
|
||||
Assert.Equal(sut.ExpirationDate, result.ExpirationDate, TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidIfPastExpiryDate()
|
||||
{
|
||||
var sut = new TestExpiringTokenable
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.AddHours(-1)
|
||||
};
|
||||
|
||||
Assert.False(sut.Valid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidIfWithinExpirationAndTokenReportsValid()
|
||||
{
|
||||
var sut = new TestExpiringTokenable
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.AddHours(1)
|
||||
};
|
||||
|
||||
Assert.True(sut.Valid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HonorsTokenIsValidAbstractMember()
|
||||
{
|
||||
var sut = new TestExpiringTokenable(forceInvalid: true)
|
||||
{
|
||||
ExpirationDate = DateTime.UtcNow.AddHours(1)
|
||||
};
|
||||
|
||||
Assert.False(sut.Valid);
|
||||
}
|
||||
}
|
||||
}
|
22
test/Core.Test/Tokens/TestTokenable.cs
Normal file
22
test/Core.Test/Tokens/TestTokenable.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Bit.Core.Tokens;
|
||||
|
||||
namespace Bit.Core.Test.Tokens
|
||||
{
|
||||
public class TestTokenable : Tokenable
|
||||
{
|
||||
public override bool Valid => true;
|
||||
}
|
||||
|
||||
public class TestExpiringTokenable : ExpiringTokenable
|
||||
{
|
||||
private bool _forceInvalid;
|
||||
|
||||
public TestExpiringTokenable() : this(false) { }
|
||||
|
||||
public TestExpiringTokenable(bool forceInvalid)
|
||||
{
|
||||
_forceInvalid = forceInvalid;
|
||||
}
|
||||
protected override bool TokenIsValid() => !_forceInvalid;
|
||||
}
|
||||
}
|
39
test/Core.Test/Tokens/TokenTests.cs
Normal file
39
test/Core.Test/Tokens/TokenTests.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.Tokens;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tokens
|
||||
{
|
||||
public class TokenTests
|
||||
{
|
||||
[Theory, AutoData]
|
||||
public void InitializeWithString_ReturnsString(string initString)
|
||||
{
|
||||
var token = new Token(initString);
|
||||
|
||||
Assert.Equal(initString, token.ToString());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void AddsPrefix(Token token, string prefix)
|
||||
{
|
||||
Assert.Equal($"{prefix}{token.ToString()}", token.WithPrefix(prefix).ToString());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void RemovePrefix_WithPrefix_RemovesPrefix(string initString, string prefix)
|
||||
{
|
||||
var token = new Token(initString).WithPrefix(prefix);
|
||||
|
||||
Assert.Equal(initString, token.RemovePrefix(prefix).ToString());
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public void RemovePrefix_WithoutPrefix_Throws(Token token, string prefix)
|
||||
{
|
||||
var exception = Assert.Throws<BadTokenException>(() => token.RemovePrefix(prefix));
|
||||
|
||||
Assert.Equal($"Expected prefix, {prefix}, was not present.", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user