1
0
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:
Matt Gibson
2022-01-10 10:58:16 -05:00
committed by GitHub
parent 924ebca153
commit e2c6fc81f4
19 changed files with 562 additions and 33 deletions

View File

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

View File

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

View File

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

View 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());
}
}
}

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

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

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