Skip to content

Conversation

brant-livefront
Copy link
Contributor

📔 Objective

This PR adds an integration with Microsoft Teams.

This is a multi-step process that we will need to take into account when building the UI for this.

  1. The organization owner kicks off an OAuth flow via the redirect
  2. Microsoft authenticates the user and our request
  3. Microsoft sends the user back to our create url, passing along a code and our state
  4. We verify the state and use the code provided via OAuth to fetch (via Microsoft's Graph API) the TenantId and list of Teams for the owner who kicked off the OAtuh flow. We store these into an Initiated record in OrganizationIntegration
  5. The owner installs our official app into their Teams instance and picks the channel in which to receive messages from the bot
  6. Microsoft sends us an incoming bot event about the app installation
  7. We parse the information from that incoming installation event, pulling out the tenant id, team id, conversation id, and serviceUrl
  8. We lookup in our OrganizationIntegration table to see if there was a previously stored Initiated record with a matching tenant id and team id. If it is present, we update that record to include the serviceUrl and conversation id of the channel, thus moving the Teams integration to Completed. If there is not a matching record, we disregard the request
  9. At this point the Teams integration is ready to send event information. The only renaming piece is to setup OrganizationIntegrationConfiguration records for the desired event types and template strings. These records will all contain null for the Configuration at this level because all of the necessary configuration is provided in the OrganizationIntegration configuration object.

Setup tasks

  • I'm currently using a minimalist app containing a minimal bot just as a POC. We will need an official app to be made in order to launch this.
    • I'll write up more notes on the specific steps but overall we need to register an app, register a bot, put together a manifest/package, and submit it to Microsoft's store
    • It is possible to side load it directly in a Team, which is what I'm doing to test currently
  • This PR adds a new integration, which will need new Subscriptions to be created in ASB
    • If it launches before we turn the feature flag on, then we can create these subscriptions ahead of time
    • If it launches after, we'll need to follow the process outlined in the docs (i.e. create the subscription with a false filter before deploying the code, then remove the filter after the code is live).
    • The subscriptions / defaults are:
      • globalSettings.EventLogging.AzureServiceBus.TeamsEventSubscriptionName with a default name of events-teams-subscription
      • globalSettings.EventLogging.AzureServiceBus.TeamsIntegrationSubscriptionName with a default name of integration-teams-subscription

Notes

  • This PR builds on Refactor Slack Callback Mechanism #6388 and specifically the IntegrationOAuthState to do the OAuth validation piece. This helps validate the information coming back on the create method was actually generated by a legitimate actor and for the correct organization. As the API is public and not behind auth, we need to make sure that the requests received are valid
  • Additionally, the bot callback that we receive on app install is not behind our auth. However, Microsoft does send their own JWT which is validated by the TeamsBotCredentialProvider. It will validate that the requests are issued by Microsoft and contain our client id as the audience. Additionally, this API can only add information to an existing, matching Initiated record - you must have a matching tenant id and teams id. and t can't modify a Completed record
  • Microsoft does not allow bots to post to private channels. This is a known limitation for which I couldn't find a workaround.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Contributor

github-actions bot commented Oct 3, 2025

Logo
Checkmarx One – Scan Summary & Details01447fe9-2e90-4c38-b042-50f0a567b50a

New Issues (2)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1511
detailsMethod at line 1511 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: pN371p7oxzg66CFSL1H%2F7897Dng%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1392
detailsMethod at line 1392 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: xow1RlTy2gYME4GMTq55sfh%2BBY0%3D
Attack Vector
Fixed Issues (1)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 299

Comment on lines +23 to +26
where oi.Type == IntegrationType.Teams &&
oi.Configuration != null &&
oi.Configuration.Contains($"\"TenantId\":\"{_tenantId}\"") &&
oi.Configuration.Contains($"\"id\":\"{_teamId}\"")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@withinfocus I couldn't find a good way to do a JSON query in EF and keep it generic, so I opted to use this Contains approach instead. I think it's functionally equivalent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but you could also parse it as JSON and make the checks a bit more cleanly. It doesn't have to all be in the where clause.

Copy link

codecov bot commented Oct 3, 2025

Codecov Report

❌ Patch coverage is 68.30601% with 116 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.72%. Comparing base (c9970a0) to head (81fd273).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
.../Implementations/EventIntegrations/TeamsService.cs 72.64% 30 Missing and 2 partials ⚠️
...tionReadByTeamsConfigurationTenantIdTeamIdQuery.cs 0.00% 21 Missing ⚠️
...SharedWeb/Utilities/ServiceCollectionExtensions.cs 28.57% 19 Missing and 1 partial ⚠️
...e/Services/NoopImplementations/NoopTeamsService.cs 0.00% 12 Missing ⚠️
.../Repositories/OrganizationIntegrationRepository.cs 0.00% 9 Missing ⚠️
...ta/EventIntegrations/TeamsListenerConfiguration.cs 0.00% 7 Missing ⚠️
.../Repositories/OrganizationIntegrationRepository.cs 0.00% 7 Missing ⚠️
...nConsole/Controllers/TeamsIntegrationController.cs 92.85% 4 Missing and 2 partials ⚠️
...anizations/OrganizationIntegrationResponseModel.cs 90.00% 0 Missing and 1 partial ⚠️
...tions/EventIntegrations/TeamsIntegrationHandler.cs 95.83% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6410      +/-   ##
==========================================
+ Coverage   50.54%   54.72%   +4.17%     
==========================================
  Files        1854     1864      +10     
  Lines       82177    82541     +364     
  Branches     7271     7299      +28     
==========================================
+ Hits        41535    45167    +3632     
+ Misses      39050    35699    -3351     
- Partials     1592     1675      +83     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@eliykat eliykat requested review from a team and removed request for eliykat October 3, 2025 21:58
@eliykat
Copy link
Member

eliykat commented Oct 3, 2025

I've requested a review from the team as I'm trying to wrap up work before going on leave.

Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few things. This is a complex one.

Comment on lines +41 to +42
<PackageReference Include="Microsoft.Bot.Builder" Version="4.23.0" />
<PackageReference Include="Microsoft.Bot.Connector" Version="4.23.0" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ These save a lot of time and code, but I don't love adding dependencies so strongly for an integration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree 100% in general. I did all of the graph api calls natively since they were just normal REST. But the incoming bot stuff seemed to push heavily in the dependency direction.

Comment on lines +23 to +26
where oi.Type == IntegrationType.Teams &&
oi.Configuration != null &&
oi.Configuration.Contains($"\"TenantId\":\"{_tenantId}\"") &&
oi.Configuration.Contains($"\"id\":\"{_teamId}\"")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but you could also parse it as JSON and make the checks a bit more cleanly. It doesn't have to all be in the where clause.

@withinfocus
Copy link
Contributor

Oh, and do you also want to update the docs?

@brant-livefront
Copy link
Contributor Author

@withinfocus Looking at what needs to change in the docs now, but wanted to push up the code changes I have. I think I've addressed all your comments. 👍

withinfocus
withinfocus previously approved these changes Oct 7, 2025
Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation can be added here in another commit and I'll re-review but this all works. I still have one open comment about the EF query getting cleaned up, but what you have works.

withinfocus
withinfocus previously approved these changes Oct 8, 2025
Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left two comments on the commit (that often gets me when I click on email notifications) with one being a ⛏️ you can quickly fix, but this is good to go at this point.

@JimmyVo16 JimmyVo16 requested review from JimmyVo16 and removed request for JimmyVo16 October 8, 2025 15:15
throw new BadRequestException("No teams were found.");
}

var teamsIntegration = new TeamsIntegration(TenantId: teams.First().TenantId, Teams: teams);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var teamsIntegration = new TeamsIntegration(TenantId: teams.First().TenantId, Teams: teams);
var teamsIntegration = new TeamsIntegration(TenantId: teams[0].TenantId, Teams: teams);

To get rid of warning.

throw new NotFoundException();
}

string? callbackUrl = Url.RouteUrl(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
string? callbackUrl = Url.RouteUrl(
var callbackUrl = Url.RouteUrl(

To remove warning.

throw new NotFoundException();
}

string? callbackUrl = Url.RouteUrl(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
string? callbackUrl = Url.RouteUrl(
var callbackUrl = Url.RouteUrl(

To remove warning.

string? ChannelId = null,
Uri? ServiceUrl = null)
{
public bool isVerified => (!string.IsNullOrEmpty(ChannelId) && ServiceUrl is not null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public bool isVerified => (!string.IsNullOrEmpty(ChannelId) && ServiceUrl is not null);
public bool IsVerified => !string.IsNullOrEmpty(ChannelId) && ServiceUrl is not null;

To remove warning

GlobalSettings globalSettings,
ILogger<TeamsService> logger) : ActivityHandler, ITeamsService
{
private readonly IOrganizationIntegrationRepository _integrationRepository = integrationRepository;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we just use the integration repository from the primary constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 fixed in next commit

Copy link
Contributor

@jrmccannon jrmccannon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. Thanks for all the updates.

Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't realize this was waiting on me after AC review.

@brant-livefront brant-livefront merged commit a565fd9 into main Oct 10, 2025
43 checks passed
@brant-livefront brant-livefront deleted the brant/microsoft-teams-integration branch October 10, 2025 14:39
aliwahhan added a commit to aliwahhan/server that referenced this pull request Oct 12, 2025
* Add Microsoft Teams integration

* Fix method naming error

* Expand and clean up unit test coverage

* Update with PR feedback

* Add documentation, add In Progress logic/tests for Teams

* Fixed lowercase Slack

* Added docs; Updated PR suggestions;

* Fix broken tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants