diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 241c28e6954b..ec62520b4098 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -74,12 +74,18 @@ public async Task AcceptOrgUserByEmailTokenAsync(Guid organiza throw new BadRequestException("User invalid."); } - var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken( - _orgUserInviteTokenDataFactory, emailToken, orgUser); - - if (!tokenValid) + var tokenValidationError = _orgUserInviteTokenDataFactory.TryUnprotect(emailToken, out var decryptedToken) switch + { + // Used by clients to show better error message on token expiration, adjust both as-needed + true when decryptedToken.IsExpired => "Expired token.", + true when !(decryptedToken.Valid && decryptedToken.TokenIsValid(orgUser)) => "Invalid token.", + false => "Invalid token.", + _ => null + }; + + if (tokenValidationError != null) { - throw new BadRequestException("Invalid token."); + throw new BadRequestException(tokenValidationError); } var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync( diff --git a/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs index 5be7ed481f2a..1a763f91f0a2 100644 --- a/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs @@ -63,7 +63,6 @@ public bool TokenIsValid(Guid orgUserId, string orgUserEmail) protected override bool TokenIsValid() => Identifier == TokenIdentifier && OrgUserId != default && !string.IsNullOrWhiteSpace(OrgUserEmail); - public static bool ValidateOrgUserInviteStringToken( IDataProtectorTokenFactory orgUserInviteTokenDataFactory, string orgUserInviteToken, OrganizationUser orgUser) diff --git a/src/Core/Tokens/ExpiringTokenable.cs b/src/Core/Tokens/ExpiringTokenable.cs index 5e90a2406606..5aac7080fc41 100644 --- a/src/Core/Tokens/ExpiringTokenable.cs +++ b/src/Core/Tokens/ExpiringTokenable.cs @@ -12,7 +12,9 @@ public abstract class ExpiringTokenable : Tokenable /// Checks if the token is still within its valid duration and if its data is valid. /// For data validation, this property relies on the method. /// - public override bool Valid => ExpirationDate > DateTime.UtcNow && TokenIsValid(); + public override bool Valid => !IsExpired && TokenIsValid(); + + public bool IsExpired => ExpirationDate < DateTime.UtcNow; /// /// Validates that the token data properties are correct. diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 5cf660b90247..cd003b3ab07a 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -451,8 +451,39 @@ public async Task AcceptOrgUserByToken_ExpiredNewToken_ThrowsBadRequest( var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService)); - Assert.Equal("Invalid token.", exception.Message); + Assert.Equal("Expired token.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AcceptOrgUserByToken_InvalidNewToken_ThrowsBadRequest( + SutProvider sutProvider, + User user, OrganizationUser orgUser) + { + // Arrange + // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order + // to avoid resetting mocks + sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); + sutProvider.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(orgUser.Id) + .Returns(Task.FromResult(orgUser)); + + // Must come after common mocks as they mutate the org user. + // Send a null org-user to force an invalid token result + _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(null!) + { + ExpirationDate = DateTime.UtcNow.AddDays(1), + }); + var newToken = CreateToken(orgUser); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptOrgUserByEmailTokenAsync(orgUser.Id, user, newToken, _userService)); + + Assert.Equal("Invalid token.", exception.Message); } [Theory]