1
0
mirror of https://github.com/bitwarden/server synced 2025-12-10 05:13:48 +00:00

Refactor Slack Callback Mechanism (#6388)

* Refactor Slack Callback

* Add more safety to state param, clarify if logic, update tests

* Added an additional 2 possible cases to test: integration is not a slack integration, and the integration has already been claimed

* Implement SonarQube suggestion

* Adjusted org hash to include timestamp; addressed PR feedback
This commit is contained in:
Brant DeBow
2025-10-03 09:30:29 -04:00
committed by GitHub
parent 1dc4c327e4
commit cde458760c
11 changed files with 678 additions and 86 deletions

View File

@@ -0,0 +1,91 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Time.Testing;
using Xunit;
namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationOAuthStateTests
{
private readonly FakeTimeProvider _fakeTimeProvider = new(
new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc)
);
[Theory, BitAutoData]
public void FromIntegration_ToString_RoundTripsCorrectly(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
var parsed = IntegrationOAuthState.FromString(state.ToString(), _fakeTimeProvider);
Assert.NotNull(parsed);
Assert.Equal(state.IntegrationId, parsed.IntegrationId);
Assert.True(parsed.ValidateOrg(integration.OrganizationId));
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("not-a-valid-state")]
public void FromString_InvalidString_ReturnsNull(string state)
{
var parsed = IntegrationOAuthState.FromString(state, _fakeTimeProvider);
Assert.Null(parsed);
}
[Fact]
public void FromString_InvalidGuid_ReturnsNull()
{
var badState = $"not-a-guid.ABCD1234.1706313600";
var parsed = IntegrationOAuthState.FromString(badState, _fakeTimeProvider);
Assert.Null(parsed);
}
[Theory, BitAutoData]
public void FromString_ExpiredState_ReturnsNull(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
// Advance time 30 minutes to exceed the 20-minute max age
_fakeTimeProvider.Advance(TimeSpan.FromMinutes(30));
var parsed = IntegrationOAuthState.FromString(state.ToString(), _fakeTimeProvider);
Assert.Null(parsed);
}
[Theory, BitAutoData]
public void ValidateOrg_WithCorrectOrgId_ReturnsTrue(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
Assert.True(state.ValidateOrg(integration.OrganizationId));
}
[Theory, BitAutoData]
public void ValidateOrg_WithWrongOrgId_ReturnsFalse(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
Assert.False(state.ValidateOrg(Guid.NewGuid()));
}
[Theory, BitAutoData]
public void ValidateOrg_ModifiedTimestamp_ReturnsFalse(OrganizationIntegration integration)
{
var state = IntegrationOAuthState.FromIntegration(integration, _fakeTimeProvider);
var parts = state.ToString().Split('.');
parts[2] = $"{_fakeTimeProvider.GetUtcNow().ToUnixTimeSeconds() - 1}";
var modifiedState = IntegrationOAuthState.FromString(string.Join(".", parts), _fakeTimeProvider);
Assert.True(state.ValidateOrg(integration.OrganizationId));
Assert.NotNull(modifiedState);
Assert.False(modifiedState.ValidateOrg(integration.OrganizationId));
}
}

View File

@@ -2,6 +2,7 @@
using System.Net;
using System.Text.Json;
using System.Web;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -261,10 +262,19 @@ public class SlackServiceTests
var sutProvider = GetSutProvider();
var clientId = sutProvider.GetDependency<GlobalSettings>().Slack.ClientId;
var scopes = sutProvider.GetDependency<GlobalSettings>().Slack.Scopes;
var redirectUrl = "https://example.com/callback";
var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={clientId}&scope={scopes}&redirect_uri={redirectUrl}";
var result = sutProvider.Sut.GetRedirectUrl(redirectUrl);
Assert.Equal(expectedUrl, result);
var callbackUrl = "https://example.com/callback";
var state = Guid.NewGuid().ToString();
var result = sutProvider.Sut.GetRedirectUrl(callbackUrl, state);
var uri = new Uri(result);
var query = HttpUtility.ParseQueryString(uri.Query);
Assert.Equal(clientId, query["client_id"]);
Assert.Equal(scopes, query["scope"]);
Assert.Equal(callbackUrl, query["redirect_uri"]);
Assert.Equal(state, query["state"]);
Assert.Equal("slack.com", uri.Host);
Assert.Equal("/oauth/v2/authorize", uri.AbsolutePath);
}
[Fact]