From 997ed2ea9e4ea2971a0df058f59be6785aa8375a Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Tue, 29 Sep 2026 13:34:03 +0200 Subject: [PATCH] feat: pass the installing user's id to IWorkspaceInstallationHandler oauth.v2.access always returns authed_user.id, but the response model bound it as user_id, so it never deserialized and Workspace had nowhere to carry it. --- .../IWorkspaceInstallationHandler.cs | 2 +- .../SlackbotCodeTokenExchangeMiddleware.cs | 3 ++- .../OAuth/OAuthClient.cs | 2 +- .../DistributionRedirectTests.cs | 21 +++++++++++++++++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/source/src/Slackbot.Net.Endpoints/Configurations/IWorkspaceInstallationHandler.cs b/source/src/Slackbot.Net.Endpoints/Configurations/IWorkspaceInstallationHandler.cs index 30835c6..f946229 100644 --- a/source/src/Slackbot.Net.Endpoints/Configurations/IWorkspaceInstallationHandler.cs +++ b/source/src/Slackbot.Net.Endpoints/Configurations/IWorkspaceInstallationHandler.cs @@ -16,4 +16,4 @@ public interface IWorkspaceInstallationHandler Task Uninstall(string teamId); } -public record Workspace(string TeamId, string TeamName, string Token); +public record Workspace(string TeamId, string TeamName, string Token, string InstallerUserId = null); diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotCodeTokenExchangeMiddleware.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotCodeTokenExchangeMiddleware.cs index f9a4333..37dc904 100644 --- a/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotCodeTokenExchangeMiddleware.cs +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/SlackbotCodeTokenExchangeMiddleware.cs @@ -37,7 +37,8 @@ await installationHandler.Install(new Workspace ( response.Team.Id, response.Team.Name, - response.Access_Token + response.Access_Token, + response.Authed_User?.Id )); var stateTheAppSent = ctx.Request.Query["state"].FirstOrDefault(); diff --git a/source/src/Slackbot.Net.Endpoints/OAuth/OAuthClient.cs b/source/src/Slackbot.Net.Endpoints/OAuth/OAuthClient.cs index fd3d19c..f0701fe 100644 --- a/source/src/Slackbot.Net.Endpoints/OAuth/OAuthClient.cs +++ b/source/src/Slackbot.Net.Endpoints/OAuth/OAuthClient.cs @@ -44,7 +44,7 @@ internal record OAuthAccessV2Response( internal record Team(string Id, string Name); - internal record OAuthUser(string User_Id, string App_Home); + internal record OAuthUser(string Id); } internal static class HttpClientExtensions diff --git a/source/test/Slackbot.Net.SlackClients.Http.Tests/DistributionRedirectTests.cs b/source/test/Slackbot.Net.SlackClients.Http.Tests/DistributionRedirectTests.cs index aee8b4f..ef91c37 100644 --- a/source/test/Slackbot.Net.SlackClients.Http.Tests/DistributionRedirectTests.cs +++ b/source/test/Slackbot.Net.SlackClients.Http.Tests/DistributionRedirectTests.cs @@ -24,6 +24,23 @@ public async Task WithoutState_TheWorkspaceIsInstalledAndTheSuccessPageUsedUntou Assert.Equal("/success?default=1", ctx.Response.Headers.Location.ToString()); } + [Fact] + public async Task TheInstallingUserIsCarriedToTheInstallationHandler() + { + var (_, handler) = await Install(state: null, successRedirectUri: "/success", + """{"ok":true,"access_token":"xoxb-token","scope":"chat:write","team":{"id":"T1","name":"Team"},"app_id":"A1","authed_user":{"id":"U1"}}"""); + + Assert.Equal("U1", Assert.Single(handler.Installed).InstallerUserId); + } + + [Fact] + public async Task WithoutAnAuthedUser_TheWorkspaceIsInstalledWithNoInstaller() + { + var (_, handler) = await Install(state: null, successRedirectUri: "/success"); + + Assert.Null(Assert.Single(handler.Installed).InstallerUserId); + } + [Fact] public async Task StateRidesBackToTheSuccessPageAsAQueryParameter() { @@ -45,7 +62,7 @@ public async Task StateIsNeverTheRedirectTarget_WhateverItHolds(string state) } private static async Task<(HttpContext Context, RecordingInstallationHandler Handler)> Install( - string state, string successRedirectUri) + string state, string successRedirectUri, string oauthAccessResponse = OauthAccessResponse) { var services = new ServiceCollection(); services.AddLogging(); @@ -57,7 +74,7 @@ public async Task StateIsNeverTheRedirectTarget_WhateverItHolds(string state) o.SuccessRedirectUri = successRedirectUri; }); services.ConfigureHttpClientDefaults(b => - b.ConfigurePrimaryHttpMessageHandler(() => new StubHttpMessageHandler(OauthAccessResponse))); + b.ConfigurePrimaryHttpMessageHandler(() => new StubHttpMessageHandler(oauthAccessResponse))); await using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); await using var scope = provider.CreateAsyncScope();