1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

Add tests for ProfileService (#6466)

* feat(profile-service) [PM-24621]: Add ProfileService test fixtures.

* feat(profile-service) [PM-24621]: Add ProfileService test suite.

* feat(profile-servie) [PM-24621]: Re-spell to more consistently use constants across tests.
This commit is contained in:
Dave
2025-10-20 11:45:11 -04:00
committed by GitHub
parent c6f1acede9
commit 629672c4b0
2 changed files with 616 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
using System.Reflection;
using System.Security.Claims;
using AutoFixture;
using AutoFixture.Xunit2;
using Bit.Core.Auth.Identity;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation;
namespace Bit.Identity.Test.AutoFixture;
internal class ProfileDataRequestContextCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<ProfileDataRequestContext>(composer => composer
.With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([
new Claim("sub", Guid.NewGuid().ToString()),
new Claim("name", "Test User"),
new Claim("email", "test@example.com")
])))
.With(o => o.Client, new Client { ClientId = "web" })
.With(o => o.ValidatedRequest, () => null)
.With(o => o.RequestedResources, new ResourceValidationResult())
.With(o => o.IssuedClaims, [])
.Without(o => o.Caller));
}
}
public class ProfileDataRequestContextAttribute : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{
return new ProfileDataRequestContextCustomization();
}
}
internal class IsActiveContextCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<IsActiveContext>(composer => composer
.With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([
new Claim("sub", Guid.NewGuid().ToString()),
new Claim(Claims.SecurityStamp, "test-security-stamp")
])))
.With(o => o.Client, new Client { ClientId = "web" })
.With(o => o.IsActive, false)
.Without(o => o.Caller));
}
}
public class IsActiveContextAttribute : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{
return new IsActiveContextCustomization();
}
}

View File

@@ -0,0 +1,558 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.Context;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Identity;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Identity.IdentityServer;
using Bit.Test.Common.AutoFixture.Attributes;
using Duende.IdentityServer.Models;
using NSubstitute;
using Xunit;
using AuthFixtures = Bit.Identity.Test.AutoFixture;
namespace Bit.Identity.Test.IdentityServer;
public class ProfileServiceTests
{
private readonly IUserService _userService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly ILicensingService _licensingService;
private readonly ICurrentContext _currentContext;
private readonly ProfileService _sut;
public ProfileServiceTests()
{
_userService = Substitute.For<IUserService>();
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
_providerUserRepository = Substitute.For<IProviderUserRepository>();
_providerOrganizationRepository = Substitute.For<IProviderOrganizationRepository>();
_licensingService = Substitute.For<ILicensingService>();
_currentContext = Substitute.For<ICurrentContext>();
_sut = new ProfileService(
_userService,
_organizationUserRepository,
_providerUserRepository,
_providerOrganizationRepository,
_licensingService,
_currentContext);
}
/// <summary>
/// For Bitwarden Sends, the zero-knowledge feature architecture is enforced by preserving claims as issued,
/// without attempting user lookup or claims mutation.
/// When acting on behalf of a Send client, the service preserves existing claims, including those issued
/// by the SendAccessGrantValidator, and returns without further claims lookup.
/// </summary>
/// <seealso cref="Bit.Identity.IdentityServer.RequestValidators.SendAccess.SendAccessGrantValidator"/>
[Theory, BitAutoData]
public async Task GetProfileDataAsync_SendClient_PreservesExistingClaims(
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context)
{
context.Client.ClientId = BitwardenClient.Send;
var existingClaims = new[]
{
new Claim(Claims.SendAccessClaims.SendId, Guid.NewGuid().ToString()), new Claim("send_access", "test")
};
context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims));
await _sut.GetProfileDataAsync(context);
Assert.Equal(existingClaims.Length, context.IssuedClaims.Count);
Assert.All(existingClaims, existingClaim =>
Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == existingClaim.Type
&& issuedClaim.Value == existingClaim.Value));
}
/// <summary>
/// For Bitwarden Sends, Send access tokens neither represent a user state nor require user profile data.
/// The SendAccessGrantValidator handles validity of requests, including resource passwords and 2FA.
/// Separation of concerns dictates that actions on behalf of Send clients should complete without
/// further lookup of user data.
/// </summary>
/// <seealso cref="Bit.Identity.IdentityServer.RequestValidators.SendAccess.SendAccessGrantValidator"/>
[Theory, BitAutoData]
public async Task GetProfileDataAsync_SendClient_DoesNotCallUserService(
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context)
{
context.Client.ClientId = BitwardenClient.Send;
await _sut.GetProfileDataAsync(context);
await _userService.DidNotReceive().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
}
/// <summary>
/// For Bitwarden Sends, the client is treated as having always-active behavior, and is neither representative of
/// a user state nor requires user profile data.
/// </summary>
/// <seealso cref="Bit.Identity.IdentityServer.RequestValidators.SendAccess.SendAccessGrantValidator"/>
[Theory, BitAutoData]
public async Task IsActiveAsync_SendClient_ReturnsTrue(
[AuthFixtures.IsActiveContext] IsActiveContext context)
{
context.Client.ClientId = BitwardenClient.Send;
context.IsActive = false;
await _sut.IsActiveAsync(context);
Assert.True(context.IsActive);
}
/// <summary>
/// For Bitwarden Sends, the client should not interrogate the user principal as part of evaluating
/// whether it is active.
/// </summary>
[Theory, BitAutoData]
public async Task IsActiveAsync_SendClient_DoesNotCallUserService(
[AuthFixtures.IsActiveContext] IsActiveContext context)
{
context.Client.ClientId = BitwardenClient.Send;
await _sut.IsActiveAsync(context);
await _userService.DidNotReceive().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
}
/// <summary>
/// When IdentityServer issues a new access token or services a UserInfo request for a given user,
/// re-evaluate the claims for that user to ensure freshness.
/// Organization-specific claims should be filtered out if the user is null for any reason.
/// This allows users to continue acting on their own behalf from a valid authenticated state, but enforces
/// a security boundary which prevents leaking of organization data and ensures organization claims,
/// which are more likely to change than user claims, are accurate and not present if the user cannot be
/// verified.
/// </summary>
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task GetProfileDataAsync_UserNull_PreservesExistingNonOrgClaims(
string client,
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context)
{
context.Client.ClientId = client;
var existingClaims = new[]
{
new Claim("sub", Guid.NewGuid().ToString()), new Claim("email", "test@example.com"),
new Claim(Claims.OrganizationOwner, Guid.NewGuid().ToString()) // This should be filtered out
};
context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims));
_userService.GetUserByPrincipalAsync(context.Subject).Returns((User)null);
await _sut.GetProfileDataAsync(context);
// Should preserve user claims
Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "sub");
Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "email");
// Should filter out organization-related claims
Assert.DoesNotContain(context.IssuedClaims, issuedClaim => issuedClaim.Type.StartsWith("org"));
}
/// <summary>
/// When IdentityServer issues a new access token or services a UserInfo request for a given user,
/// re-evaluate the claims for that user to ensure freshness.
/// New or updated claims, including premium access and organization or provider membership,
/// should be served with the response.
/// </summary>
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task GetProfileDataAsync_UserExists_BuildsIdentityClaims(
string client,
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context,
User user)
{
context.Client.ClientId = client;
user.Id = Guid.Parse(context.Subject.FindFirst("sub")!.Value);
var orgMemberships = new List<CurrentContextOrganization>
{
new() { Id = Guid.NewGuid(), Type = OrganizationUserType.User }
};
var providerMemberships = new List<CurrentContextProvider>();
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
_licensingService.ValidateUserPremiumAsync(user).Returns(true);
_currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)
.Returns(orgMemberships);
_currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id)
.Returns(providerMemberships);
await _sut.GetProfileDataAsync(context);
Assert.NotEmpty(context.IssuedClaims);
Assert.Contains(context.IssuedClaims,
issuedClaim => issuedClaim.Type == Claims.Premium &&
issuedClaim.Value.Equals("true", StringComparison.CurrentCultureIgnoreCase));
await _licensingService.Received(1).ValidateUserPremiumAsync(user);
await _currentContext.Received(1).OrganizationMembershipAsync(_organizationUserRepository, user.Id);
await _currentContext.Received(1).ProviderMembershipAsync(_providerUserRepository, user.Id);
}
/// <summary>
/// OpenID Connect Core and JWT distinguish between string and boolean types. For spec compliance,
/// boolean types should be served as booleans, not as strings (e.g., true, not "true"). See
/// https://datatracker.ietf.org/doc/html/rfc7159#section-3, and
/// https://datatracker.ietf.org/doc/html/rfc7519#section-2.
/// For proper claims deserialization and type safety, ensure boolean values are treated as
/// ClaimType.Boolean.
/// </summary>
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task GetProfileDataAsync_UserExists_BooleanClaimsHaveBooleanType(
string client,
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context,
User user)
{
context.Client.ClientId = client;
user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value);
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
_licensingService.ValidateUserPremiumAsync(user).Returns(true);
_currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)
.Returns(new List<CurrentContextOrganization>());
_currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id)
.Returns(new List<CurrentContextProvider>());
await _sut.GetProfileDataAsync(context);
var booleanClaims = context.IssuedClaims.Where(claim =>
claim.Value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
claim.Value.Equals("false", StringComparison.OrdinalIgnoreCase));
Assert.All(booleanClaims, claim =>
Assert.Equal(ClaimValueTypes.Boolean, claim.ValueType));
}
/// <summary>
/// When IdentityServer issues a new access token or services a UserInfo request for a given user,
/// re-evaluate the claims for that user to ensure freshness.
/// Organization-specific claims should never be allowed to persist, and should always be fetched fresh.
/// </summary>
/// <seealso cref="Bit.Core.Context.ICurrentContext.OrganizationMembershipAsync" />
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task GetProfileDataAsync_FiltersOutOrgClaimsFromExisting(
string client,
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context,
User user)
{
context.Client.ClientId = client;
user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value);
var existingClaims = new[]
{
new Claim(Claims.OrganizationOwner, Guid.NewGuid().ToString()),
new Claim(Claims.OrganizationAdmin, Guid.NewGuid().ToString()), new Claim("email", "test@example.com"),
new Claim("name", "Test User")
};
context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims));
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
_licensingService.ValidateUserPremiumAsync(user).Returns(false);
_currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)
.Returns(new List<CurrentContextOrganization>());
_currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id)
.Returns(new List<CurrentContextProvider>());
await _sut.GetProfileDataAsync(context);
Assert.DoesNotContain(context.IssuedClaims, issuedClaim => issuedClaim.Type.StartsWith("org"));
Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "email");
Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "name");
}
/// <summary>
/// When IdentityServer issues a new access token or services a UserInfo request for a given user,
/// re-evaluate the claims for that user to ensure freshness.
/// Existing claims should always be updated, even if their type exists in the incoming collection.
/// </summary>
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task GetProfileDataAsync_NewClaimsOverrideExistingNonOrgClaims(
string client,
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context,
User user)
{
context.Client.ClientId = client;
user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value);
user.Email = "new@example.com";
var existingClaims = new[]
{
new Claim("sub", user.Id.ToString()), new Claim("email", "old@example.com"),
new Claim(Claims.Premium, "false")
};
context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims));
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
_licensingService.ValidateUserPremiumAsync(user).Returns(true);
_currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)
.Returns(new List<CurrentContextOrganization>());
_currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id)
.Returns(new List<CurrentContextProvider>());
await _sut.GetProfileDataAsync(context);
// Should have new premium claim, not old one
Assert.Contains(context.IssuedClaims,
issuedClaim => issuedClaim.Type == Claims.Premium &&
issuedClaim.Value.Equals("true", StringComparison.CurrentCultureIgnoreCase));
Assert.DoesNotContain(context.IssuedClaims,
issuedClaim => issuedClaim.Type == Claims.Premium &&
issuedClaim.Value.Equals("false", StringComparison.CurrentCultureIgnoreCase));
// Should have new email
Assert.Contains(context.IssuedClaims,
issuedClaim => issuedClaim.Type == "email" && issuedClaim.Value == "new@example.com");
Assert.DoesNotContain(context.IssuedClaims,
issuedClaim => issuedClaim.Type == "email" && issuedClaim.Value == "old@example.com");
}
/// <summary>
/// Users may belong to multiple organizations. Claims should be properly scoped to each relevant organization
/// and not cross-pollinate claims across organizations, and should be fetched fresh on each request.
/// </summary>
/// <seealso cref="Bit.Core.Context.ICurrentContext.OrganizationMembershipAsync" />
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task GetProfileDataAsync_WithMultipleOrganizations_IncludesOrgClaims(
string client,
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context,
User user)
{
context.Client.ClientId = client;
user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value);
var orgId1 = Guid.NewGuid();
var orgId2 = Guid.NewGuid();
var orgMemberships = new List<CurrentContextOrganization>
{
new() { Id = orgId1, Type = OrganizationUserType.Owner },
new() { Id = orgId2, Type = OrganizationUserType.Admin }
};
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
_licensingService.ValidateUserPremiumAsync(user).Returns(false);
_currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)
.Returns(orgMemberships);
_currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id)
.Returns(new List<CurrentContextProvider>());
await _sut.GetProfileDataAsync(context);
Assert.Contains(context.IssuedClaims,
issuedClaim => issuedClaim.Type == Claims.OrganizationOwner && issuedClaim.Value == orgId1.ToString());
Assert.Contains(context.IssuedClaims,
issuedClaim => issuedClaim.Type == Claims.OrganizationAdmin && issuedClaim.Value == orgId2.ToString());
}
/// <summary>
/// Users may belong to providers. Claims should be properly scoped to each relevant provider
/// and not cross-pollinate claims across providers, and should be fetched fresh on each request.
/// </summary>
/// <seealso cref="Bit.Core.Context.ICurrentContext.ProviderMembershipAsync" />
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task GetProfileDataAsync_WithProviders_IncludesProviderClaims(
string client,
[AuthFixtures.ProfileDataRequestContext]
ProfileDataRequestContext context,
User user)
{
context.Client.ClientId = client;
user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value);
var providerId = Guid.NewGuid();
var providerMemberships = new List<CurrentContextProvider>
{
new() { Id = providerId, Type = ProviderUserType.ProviderAdmin }
};
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
_licensingService.ValidateUserPremiumAsync(user).Returns(false);
_currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)
.Returns(new List<CurrentContextOrganization>());
_currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id)
.Returns(providerMemberships);
await _sut.GetProfileDataAsync(context);
Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type.StartsWith("provider"));
}
/// <summary>
/// Evaluates the happy path for the core session invalidation mechanism.
/// Critical events (e.g., password change) update the security stamp, and any subsequent request through
/// this service should expose the stamp as invalid. A found user and matching security stamp
/// prove out an active session.
/// </summary>
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task IsActiveAsync_SecurityStampMatches_ReturnsTrue(
string client,
[AuthFixtures.IsActiveContext] IsActiveContext context,
User user)
{
context.Client.ClientId = client;
var securityStamp = "matching-security-stamp";
user.SecurityStamp = securityStamp;
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, securityStamp)
]));
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
await _sut.IsActiveAsync(context);
Assert.True(context.IsActive);
}
/// <summary>
/// Critical events (e.g., password change) update the security stamp, and any subsequent request through
/// this service should expose the stamp as invalid.
/// See also examples for stamp invalidation (non-exhaustive):
/// </summary>
/// <seealso cref="Bit.Core.KeyManagement.UserKey.Implementations.RotateUserAccountKeysCommand.RotateUserAccountKeysAsync"/>
/// <seealso cref="Bit.Core.Services.UserService.ChangePasswordAsync"/>
/// <seealso cref="Bit.Core.Services.UserService.UpdatePasswordHash"/>
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task IsActiveAsync_SecurityStampDoesNotMatch_ReturnsFalse(
string client,
[AuthFixtures.IsActiveContext] IsActiveContext context,
User user)
{
context.Client.ClientId = client;
user.SecurityStamp = "current-security-stamp";
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, "old-security-stamp")
]));
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
await _sut.IsActiveAsync(context);
Assert.False(context.IsActive);
}
/// <summary>
/// Because security stamps are GUIDs, and database collations, etc., might treat case differently,
/// a case-insensitive comparison is sufficient for proving the match of a security stamp.
/// </summary>
[Theory]
[BitAutoData(BitwardenClient.Web, "CuRrEnT-StAmP")]
[BitAutoData(BitwardenClient.Browser, "CuRrEnT-StAmP")]
[BitAutoData(BitwardenClient.Cli, "CuRrEnT-StAmP")]
[BitAutoData(BitwardenClient.Desktop, "CuRrEnT-StAmP")]
[BitAutoData(BitwardenClient.Mobile, "CuRrEnT-StAmP")]
[BitAutoData(BitwardenClient.DirectoryConnector, "CuRrEnT-StAmP")]
public async Task IsActiveAsync_SecurityStampComparison_IsCaseInsensitive(
string client,
string claimStamp,
[AuthFixtures.IsActiveContext] IsActiveContext context,
User user)
{
context.Client.ClientId = client;
user.SecurityStamp = "current-stamp";
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, claimStamp)
]));
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
await _sut.IsActiveAsync(context);
Assert.True(context.IsActive);
}
/// <summary>
/// Security stamps should be evaluated when present, but should not always be expected to be present.
/// Given a successful user lookup, absent a security stamp, the session is treated as active.
/// Only if the stamp is presented on context claims should it be validated.
/// </summary>
[Theory]
[BitAutoData(BitwardenClient.Web)]
[BitAutoData(BitwardenClient.Browser)]
[BitAutoData(BitwardenClient.Cli)]
[BitAutoData(BitwardenClient.Desktop)]
[BitAutoData(BitwardenClient.Mobile)]
[BitAutoData(BitwardenClient.DirectoryConnector)]
public async Task IsActiveAsync_UserExistsButNoSecurityStampClaim_ReturnsTrue(
string client,
[AuthFixtures.IsActiveContext] IsActiveContext context,
User user)
{
context.Client.ClientId = client;
context.Subject = new ClaimsPrincipal(new ClaimsIdentity([
new Claim("sub", user.Id.ToString()), new Claim("email", user.Email)
]));
_userService.GetUserByPrincipalAsync(context.Subject).Returns(user);
await _sut.IsActiveAsync(context);
Assert.True(context.IsActive);
}
}