1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-28 23:04: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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 562 additions and 33 deletions

View File

@ -0,0 +1,37 @@
using System;
using System.Text.Json.Serialization;
using Bit.Core.Models.Table;
namespace Bit.Core.Models.Business.Tokenables
{
public class EmergencyAccessInviteTokenable : Tokens.ExpiringTokenable
{
public const string ClearTextPrefix = "";
public const string DataProtectorPurpose = "EmergencyAccessServiceDataProtector";
public const string TokenIdentifier = "EmergencyAccessInvite";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }
public string Email { get; set; }
[JsonConstructor]
public EmergencyAccessInviteTokenable(DateTime expirationDate)
{
ExpirationDate = expirationDate;
}
public EmergencyAccessInviteTokenable(EmergencyAccess user, int hoursTillExpiration)
{
Id = user.Id;
Email = user.Email;
ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration);
}
public bool IsValid(Guid id, string email)
{
return Id == id &&
Email.Equals(email, StringComparison.InvariantCultureIgnoreCase);
}
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Text.Json.Serialization;
using Bit.Core.Models.Table;
using Bit.Core.Tokens;
namespace Bit.Core.Models.Business.Tokenables
{
public class HCaptchaTokenable : ExpiringTokenable
{
private const double _tokenLifetimeInHours = (double)5 / 60; // 5 minutes
public const string ClearTextPrefix = "BWCaptchaBypass_";
public const string DataProtectorPurpose = "CaptchaServiceDataProtector";
public const string TokenIdentifier = "CaptchaBypassToken";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }
public string Email { get; set; }
[JsonConstructor]
public HCaptchaTokenable()
{
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours);
}
public HCaptchaTokenable(User user) : this()
{
Id = user.Id;
Email = user.Email;
}
public bool TokenIsValid(User user)
{
return Id == user.Id &&
Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
}
// Validates deserialized
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);
}
}

View File

@ -5,12 +5,12 @@ using System.Threading.Tasks;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models; using Bit.Core.Models;
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Tokens;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Services namespace Bit.Core.Services
@ -25,10 +25,10 @@ namespace Bit.Core.Services
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IDataProtector _dataProtector;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IPasswordHasher<User> _passwordHasher; private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer;
public EmergencyAccessService( public EmergencyAccessService(
IEmergencyAccessRepository emergencyAccessRepository, IEmergencyAccessRepository emergencyAccessRepository,
@ -40,9 +40,9 @@ namespace Bit.Core.Services
IMailService mailService, IMailService mailService,
IUserService userService, IUserService userService,
IPasswordHasher<User> passwordHasher, IPasswordHasher<User> passwordHasher,
IDataProtectionProvider dataProtectionProvider,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IOrganizationService organizationService) IOrganizationService organizationService,
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer)
{ {
_emergencyAccessRepository = emergencyAccessRepository; _emergencyAccessRepository = emergencyAccessRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -53,9 +53,9 @@ namespace Bit.Core.Services
_mailService = mailService; _mailService = mailService;
_userService = userService; _userService = userService;
_passwordHasher = passwordHasher; _passwordHasher = passwordHasher;
_dataProtector = dataProtectionProvider.CreateProtector("EmergencyAccessServiceDataProtector");
_globalSettings = globalSettings; _globalSettings = globalSettings;
_organizationService = organizationService; _organizationService = organizationService;
_dataProtectorTokenizer = dataProtectorTokenizer;
} }
public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime) public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime)
@ -118,8 +118,7 @@ namespace Bit.Core.Services
throw new BadRequestException("Emergency Access not valid."); throw new BadRequestException("Emergency Access not valid.");
} }
if (!CoreHelpers.TokenIsValid("EmergencyAccessInvite", _dataProtector, token, user.Email, emergencyAccessId, if (!_dataProtectorTokenizer.TryUnprotect(token, out var data) && data.IsValid(emergencyAccessId, user.Email))
_globalSettings.OrganizationInviteExpirationHours))
{ {
throw new BadRequestException("Invalid token."); throw new BadRequestException("Invalid token.");
} }
@ -403,8 +402,7 @@ namespace Bit.Core.Services
private async Task SendInviteAsync(EmergencyAccess emergencyAccess, string invitingUsersName) private async Task SendInviteAsync(EmergencyAccess emergencyAccess, string invitingUsersName)
{ {
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow); var token = _dataProtectorTokenizer.Protect(new EmergencyAccessInviteTokenable(emergencyAccess, _globalSettings.OrganizationInviteExpirationHours));
var token = _dataProtector.Protect($"EmergencyAccessInvite {emergencyAccess.Id} {emergencyAccess.Email} {nowMillis}");
await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token); await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);
} }

View File

@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Tokens;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -14,31 +14,27 @@ namespace Bit.Core.Services
{ {
public class HCaptchaValidationService : ICaptchaValidationService public class HCaptchaValidationService : ICaptchaValidationService
{ {
private const double TokenLifetimeInHours = (double)5 / 60; // 5 minutes
private const string TokenName = "CaptchaBypassToken";
private const string TokenClearTextPrefix = "BWCaptchaBypass_";
private readonly ILogger<HCaptchaValidationService> _logger; private readonly ILogger<HCaptchaValidationService> _logger;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IDataProtector _dataProtector; private readonly IDataProtectorTokenFactory<HCaptchaTokenable> _tokenizer;
public HCaptchaValidationService( public HCaptchaValidationService(
ILogger<HCaptchaValidationService> logger, ILogger<HCaptchaValidationService> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IDataProtectionProvider dataProtectorProvider, IDataProtectorTokenFactory<HCaptchaTokenable> tokenizer,
GlobalSettings globalSettings) GlobalSettings globalSettings)
{ {
_logger = logger; _logger = logger;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_dataProtector = dataProtectorProvider.CreateProtector("CaptchaServiceDataProtector"); _tokenizer = tokenizer;
} }
public string SiteKeyResponseKeyName => "HCaptcha_SiteKey"; public string SiteKeyResponseKeyName => "HCaptcha_SiteKey";
public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey; public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey;
public string GenerateCaptchaBypassToken(User user) => public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user));
$"{TokenClearTextPrefix}{_dataProtector.Protect(CaptchaBypassTokenContent(user))}";
public bool ValidateCaptchaBypassToken(string bypassToken, User user) => public bool ValidateCaptchaBypassToken(string bypassToken, User user) =>
TokenIsApiKey(bypassToken, user) || TokenIsCaptchaBypassToken(bypassToken, user); TokenIsApiKey(bypassToken, user) || TokenIsCaptchaBypassToken(bypassToken, user);
@ -89,20 +85,12 @@ namespace Bit.Core.Services
public bool RequireCaptchaValidation(ICurrentContext currentContext) => public bool RequireCaptchaValidation(ICurrentContext currentContext) =>
currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired; currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
private static string CaptchaBypassTokenContent(User user) =>
string.Join(' ', new object[] {
TokenName,
user?.Id,
user?.Email,
CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow.AddHours(TokenLifetimeInHours))
});
private static bool TokenIsApiKey(string bypassToken, User user) => private static bool TokenIsApiKey(string bypassToken, User user) =>
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken; !string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
private bool TokenIsCaptchaBypassToken(string encryptedToken, User user) => private bool TokenIsCaptchaBypassToken(string encryptedToken, User user)
encryptedToken.StartsWith(TokenClearTextPrefix) && user != null && {
CoreHelpers.TokenIsValid(TokenName, _dataProtector, encryptedToken[TokenClearTextPrefix.Length..], return _tokenizer.TryUnprotect(encryptedToken, out var data) &&
user.Email, user.Id, TokenLifetimeInHours); data.Valid && data.TokenIsValid(user);
}
} }
} }

View File

@ -0,0 +1,15 @@
using System;
namespace Bit.Core.Tokens
{
public class BadTokenException : Exception
{
public BadTokenException()
{
}
public BadTokenException(string message) : base(message)
{
}
}
}

View File

@ -0,0 +1,48 @@
using Microsoft.AspNetCore.DataProtection;
namespace Bit.Core.Tokens
{
public class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where T : Tokenable
{
private readonly IDataProtector _dataProtector;
private readonly string _clearTextPrefix;
public DataProtectorTokenFactory(string clearTextPrefix, string purpose, IDataProtectionProvider dataProtectionProvider)
{
_dataProtector = dataProtectionProvider.CreateProtector(purpose);
_clearTextPrefix = clearTextPrefix;
}
public string Protect(T data) =>
data.ToToken().ProtectWith(_dataProtector).WithPrefix(_clearTextPrefix).ToString();
public T Unprotect(string token) =>
Tokenable.FromToken<T>(new Token(token).RemovePrefix(_clearTextPrefix).UnprotectWith(_dataProtector).ToString());
public bool TokenValid(string token)
{
try
{
return Unprotect(token).Valid;
}
catch
{
return false;
}
}
public bool TryUnprotect(string token, out T data)
{
try
{
data = Unprotect(token);
return true;
}
catch
{
data = default;
return false;
}
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
using Bit.Core.Utilities;
namespace Bit.Core.Tokens
{
public abstract class ExpiringTokenable : Tokenable
{
[JsonConverter(typeof(EpochDateTimeJsonConverter))]
public DateTime ExpirationDate { get; set; }
public override bool Valid => ExpirationDate > DateTime.UtcNow && TokenIsValid();
protected abstract bool TokenIsValid();
}
}

View File

@ -0,0 +1,10 @@
namespace Bit.Core.Tokens
{
public interface IDataProtectorTokenFactory<T> where T : Tokenable
{
string Protect(T data);
T Unprotect(string token);
bool TryUnprotect(string token, out T data);
bool TokenValid(string token);
}
}

37
src/Core/Tokens/Token.cs Normal file
View File

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.DataProtection;
namespace Bit.Core.Tokens
{
public class Token
{
private readonly string _token;
public Token(string token)
{
_token = token;
}
public Token WithPrefix(string prefix)
{
return new Token($"{prefix}{_token}");
}
public Token RemovePrefix(string expectedPrefix)
{
if (!_token.StartsWith(expectedPrefix))
{
throw new BadTokenException($"Expected prefix, {expectedPrefix}, was not present.");
}
return new Token(_token[expectedPrefix.Length..]);
}
public Token ProtectWith(IDataProtector dataProtector) =>
new(dataProtector.Protect(ToString()));
public Token UnprotectWith(IDataProtector dataProtector) =>
new(dataProtector.Unprotect(ToString()));
public override string ToString() => _token;
}
}

View File

@ -0,0 +1,20 @@
using System.Text.Json;
namespace Bit.Core.Tokens
{
public abstract class Tokenable
{
public abstract bool Valid { get; }
public Token ToToken()
{
return new Token(JsonSerializer.Serialize(this, this.GetType()));
}
public static T FromToken<T>(string token) => FromToken<T>(new Token(token));
public static T FromToken<T>(Token token)
{
return JsonSerializer.Deserialize<T>(token.ToString());
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Bit.Core.Utilities
{
public class EpochDateTimeJsonConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return CoreHelpers.FromEpocMilliseconds(reader.GetInt64());
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteNumberValue(CoreHelpers.ToEpocMilliseconds(value));
}
}
}

View File

@ -9,11 +9,13 @@ using AutoMapper;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Resources; using Bit.Core.Resources;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using IdentityModel; using IdentityModel;
using IdentityServer4.AccessTokenValidation; using IdentityServer4.AccessTokenValidation;
@ -181,6 +183,22 @@ namespace Bit.Core.Utilities
services.AddScoped<ISendService, SendService>(); services.AddScoped<ISendService, SendService>();
} }
public static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<EmergencyAccessInviteTokenable>(
EmergencyAccessInviteTokenable.ClearTextPrefix,
EmergencyAccessInviteTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider())
);
services.AddSingleton<IDataProtectorTokenFactory<HCaptchaTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<HCaptchaTokenable>(
HCaptchaTokenable.ClearTextPrefix,
HCaptchaTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider())
);
}
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings) public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
{ {
// Required for UserService // Required for UserService
@ -201,6 +219,7 @@ namespace Bit.Core.Utilities
services.AddSingleton<IPaymentService, StripePaymentService>(); services.AddSingleton<IPaymentService, StripePaymentService>();
services.AddSingleton<IMailService, HandlebarsMailService>(); services.AddSingleton<IMailService, HandlebarsMailService>();
services.AddSingleton<ILicensingService, LicensingService>(); services.AddSingleton<ILicensingService, LicensingService>();
services.AddTokenizers();
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName)) CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))

View File

@ -118,6 +118,11 @@ namespace Bit.Test.Common.AutoFixture
{ {
return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name); 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 // 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);
}
}
}