1
0
mirror of https://github.com/bitwarden/server synced 2025-12-29 06:33:43 +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,71 @@
using System.Security.Cryptography;
using System.Text;
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationOAuthState
{
private const int _orgHashLength = 12;
private static readonly TimeSpan _maxAge = TimeSpan.FromMinutes(20);
public Guid IntegrationId { get; }
private DateTimeOffset Issued { get; }
private string OrganizationIdHash { get; }
private IntegrationOAuthState(Guid integrationId, string organizationIdHash, DateTimeOffset issued)
{
IntegrationId = integrationId;
OrganizationIdHash = organizationIdHash;
Issued = issued;
}
public static IntegrationOAuthState FromIntegration(OrganizationIntegration integration, TimeProvider timeProvider)
{
var integrationId = integration.Id;
var issuedUtc = timeProvider.GetUtcNow();
var organizationIdHash = ComputeOrgHash(integration.OrganizationId, issuedUtc.ToUnixTimeSeconds());
return new IntegrationOAuthState(integrationId, organizationIdHash, issuedUtc);
}
public static IntegrationOAuthState? FromString(string state, TimeProvider timeProvider)
{
if (string.IsNullOrWhiteSpace(state)) return null;
var parts = state.Split('.');
if (parts.Length != 3) return null;
// Verify timestamp
if (!long.TryParse(parts[2], out var unixSeconds)) return null;
var issuedUtc = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
var now = timeProvider.GetUtcNow();
var age = now - issuedUtc;
if (age > _maxAge) return null;
// Parse integration id and store org
if (!Guid.TryParse(parts[0], out var integrationId)) return null;
var organizationIdHash = parts[1];
return new IntegrationOAuthState(integrationId, organizationIdHash, issuedUtc);
}
public bool ValidateOrg(Guid orgId)
{
var expected = ComputeOrgHash(orgId, Issued.ToUnixTimeSeconds());
return expected == OrganizationIdHash;
}
public override string ToString()
{
return $"{IntegrationId}.{OrganizationIdHash}.{Issued.ToUnixTimeSeconds()}";
}
private static string ComputeOrgHash(Guid orgId, long timestamp)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes($"{orgId:N}:{timestamp}"));
return Convert.ToHexString(bytes)[.._orgHashLength];
}
}