1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +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

@@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
@@ -18,25 +15,58 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations/slack")]
[Route("organizations")]
[Authorize("Application")]
public class SlackIntegrationController(
ICurrentContext currentContext,
IOrganizationIntegrationRepository integrationRepository,
ISlackService slackService) : Controller
ISlackService slackService,
TimeProvider timeProvider) : Controller
{
[HttpGet("redirect")]
[HttpGet("{organizationId:guid}/integrations/slack/redirect")]
public async Task<IActionResult> RedirectAsync(Guid organizationId)
{
if (!await currentContext.OrganizationOwner(organizationId))
{
throw new NotFoundException();
}
string callbackUrl = Url.RouteUrl(
nameof(CreateAsync),
new { organizationId },
currentContext.HttpContext.Request.Scheme);
var redirectUrl = slackService.GetRedirectUrl(callbackUrl);
string? callbackUrl = Url.RouteUrl(
routeName: nameof(CreateAsync),
values: null,
protocol: currentContext.HttpContext.Request.Scheme,
host: currentContext.HttpContext.Request.Host.ToUriComponent()
);
if (string.IsNullOrEmpty(callbackUrl))
{
throw new BadRequestException("Unable to build callback Url");
}
var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
var integration = integrations.FirstOrDefault(i => i.Type == IntegrationType.Slack);
if (integration is null)
{
// No slack integration exists, create Initiated version
integration = await integrationRepository.CreateAsync(new OrganizationIntegration
{
OrganizationId = organizationId,
Type = IntegrationType.Slack,
Configuration = null,
});
}
else if (integration.Configuration is not null)
{
// A Completed (fully configured) Slack integration already exists, throw to prevent overriding
throw new BadRequestException("There already exists a Slack integration for this organization");
} // An Initiated slack integration exits, re-use it and kick off a new OAuth flow
var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);
var redirectUrl = slackService.GetRedirectUrl(
callbackUrl: callbackUrl,
state: state.ToString()
);
if (string.IsNullOrEmpty(redirectUrl))
{
@@ -46,23 +76,42 @@ public class SlackIntegrationController(
return Redirect(redirectUrl);
}
[HttpGet("create", Name = nameof(CreateAsync))]
public async Task<IActionResult> CreateAsync(Guid organizationId, [FromQuery] string code)
[HttpGet("integrations/slack/create", Name = nameof(CreateAsync))]
[AllowAnonymous]
public async Task<IActionResult> CreateAsync([FromQuery] string code, [FromQuery] string state)
{
if (!await currentContext.OrganizationOwner(organizationId))
var oAuthState = IntegrationOAuthState.FromString(state: state, timeProvider: timeProvider);
if (oAuthState is null)
{
throw new NotFoundException();
}
if (string.IsNullOrEmpty(code))
// Fetch existing Initiated record
var integration = await integrationRepository.GetByIdAsync(oAuthState.IntegrationId);
if (integration is null ||
integration.Type != IntegrationType.Slack ||
integration.Configuration is not null)
{
throw new BadRequestException("Missing code from Slack.");
throw new NotFoundException();
}
string callbackUrl = Url.RouteUrl(
nameof(CreateAsync),
new { organizationId },
currentContext.HttpContext.Request.Scheme);
// Verify Organization matches hash
if (!oAuthState.ValidateOrg(integration.OrganizationId))
{
throw new NotFoundException();
}
// Fetch token from Slack and store to DB
string? callbackUrl = Url.RouteUrl(
routeName: nameof(CreateAsync),
values: null,
protocol: currentContext.HttpContext.Request.Scheme,
host: currentContext.HttpContext.Request.Host.ToUriComponent()
);
if (string.IsNullOrEmpty(callbackUrl))
{
throw new BadRequestException("Unable to build callback Url");
}
var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl);
if (string.IsNullOrEmpty(token))
@@ -70,14 +119,10 @@ public class SlackIntegrationController(
throw new BadRequestException("Invalid response from Slack.");
}
var integration = await integrationRepository.CreateAsync(new OrganizationIntegration
{
OrganizationId = organizationId,
Type = IntegrationType.Slack,
Configuration = JsonSerializer.Serialize(new SlackIntegration(token)),
});
var location = $"/organizations/{organizationId}/integrations/{integration.Id}";
integration.Configuration = JsonSerializer.Serialize(new SlackIntegration(token));
await integrationRepository.UpsertAsync(integration);
var location = $"/organizations/{integration.OrganizationId}/integrations/{integration.Id}";
return Created(location, new OrganizationIntegrationResponseModel(integration));
}
}