mirror of
https://github.com/bitwarden/server
synced 2025-12-25 20:53:16 +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:
10
src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs
Normal file
10
src/Core/AdminConsole/Enums/OrganizationIntegrationStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public enum OrganizationIntegrationStatus : int
|
||||
{
|
||||
NotApplicable,
|
||||
Invalid,
|
||||
Initiated,
|
||||
InProgress,
|
||||
Completed
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ public interface ISlackService
|
||||
Task<string> GetChannelIdAsync(string token, string channelName);
|
||||
Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames);
|
||||
Task<string> GetDmChannelByEmailAsync(string token, string email);
|
||||
string GetRedirectUrl(string redirectUrl);
|
||||
string GetRedirectUrl(string callbackUrl, string state);
|
||||
Task<string> ObtainTokenViaOAuth(string code, string redirectUrl);
|
||||
Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public class SlackService(
|
||||
private readonly string _slackApiBaseUrl = globalSettings.Slack.ApiBaseUrl;
|
||||
|
||||
public const string HttpClientName = "SlackServiceHttpClient";
|
||||
private const string _slackOAuthBaseUri = "https://slack.com/oauth/v2/authorize";
|
||||
|
||||
public async Task<string> GetChannelIdAsync(string token, string channelName)
|
||||
{
|
||||
@@ -73,9 +74,18 @@ public class SlackService(
|
||||
return await OpenDmChannel(token, userId);
|
||||
}
|
||||
|
||||
public string GetRedirectUrl(string redirectUrl)
|
||||
public string GetRedirectUrl(string callbackUrl, string state)
|
||||
{
|
||||
return $"https://slack.com/oauth/v2/authorize?client_id={_clientId}&scope={_scopes}&redirect_uri={redirectUrl}";
|
||||
var builder = new UriBuilder(_slackOAuthBaseUri);
|
||||
var query = HttpUtility.ParseQueryString(builder.Query);
|
||||
|
||||
query["client_id"] = _clientId;
|
||||
query["scope"] = _scopes;
|
||||
query["redirect_uri"] = callbackUrl;
|
||||
query["state"] = state;
|
||||
|
||||
builder.Query = query.ToString();
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
|
||||
|
||||
@@ -19,7 +19,7 @@ public class NoopSlackService : ISlackService
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public string GetRedirectUrl(string redirectUrl)
|
||||
public string GetRedirectUrl(string callbackUrl, string state)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user