slackapi / bolt-js

A framework to build Slack apps using JavaScript
https://slack.dev/bolt-js
MIT License
2.73k stars 392 forks source link

Install and authorize multiple slack app clients with a single bolt js server #2206

Open KondaHarika opened 4 weeks ago

KondaHarika commented 4 weeks ago

I have a scenario where people create slack app and would provide the client credentials. Generally the installation url provide is like {URL}/slack/install,

but here idea is to pass a query param and identify the respective slack app, hence based on that I will get client data from MongoDB and hence the install process.

Question here is that is this approach possible or do we have any other way to execute this case?

Second, I have been trying execute the above approach so trying to use the express middleware, to set the params required but I am unable to access the middleware here?! Is there anything wrong that Im doing here !?

const { App, ExpressReceiver, LogLevel } = require('@slack/bolt');
const express = require('express');

require('dotenv').config();

const expApp = express();

const oauthRedirect = process.env.SLACK_OAUTHREDIRECT_URL;
const botScopes = process.env.SLACK_BOT_SCOPES ? process.env.SLACK_BOT_SCOPES.split(',') : [];

const expressReceiver = new ExpressReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET,
    clientId: process.env.SLACK_CLIENT_ID,
    clientSecret: process.env.SLACK_CLIENT_SECRET,
    stateSecret: "state-secret",
    scopes: botScopes,
    redirectUri: oauthRedirect,
    installerOptions: {
      stateVerification: true,
      redirectUriPath: "/slack/oauth_redirect",
      directInstall: true,
    },
    installationStore: {
        storeInstallation: async (installation) => {
          if (installation.isEnterpriseInstall && installation.enterprise !== undefined) {
            return await appInstall.saveApp(installation.enterprise.id, installation);
          }
          if (installation.team !== undefined) {
            return await appInstall.saveApp(installation.team.id, installation);
          }
          throw new Error('Failed saving installation data to installationStore');
        },
        fetchInstallation: async (installQuery) => {
          if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
            return await appInstall.getApp(installQuery.enterpriseId);
          }
          if (installQuery.teamId !== undefined) {
            return await appInstall.getApp(installQuery.teamId);
          }
          throw new Error('Failed fetching installation');
        },
        deleteInstallation: async (installQuery) => {
          if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
            return await appInstall.deleteApp(installQuery.enterpriseId);
          }
          if (installQuery.teamId !== undefined) {
            return await appInstall.deleteApp(installQuery.teamId);
          }
          throw new Error('Failed to delete installation');
        },
      },
  })

const app = new App({
    receiver: expressReceiver,
    // logLevel: LogLevel.DEBUG,
});

expressReceiver.router.get('/api/health', async (req, res) => {
 // health check code
});

function myMiddleware(req, res, next) {
  console.log('Middleware is running');
  next();
}

expApp.use(myMiddleware);

expApp.use('/slack/install', expressReceiver.router);

(async () => {
    try {
        await app.start(process.env.PORT || 4000);
        console.log("Slack Bolt app is running on port", process.env.PORT);
    } catch (error) {
        console.error("Unable to start App", error);
    }
})();
zimeg commented 4 weeks ago

Hey @KondaHarika :wave: This is a super interesting question but I might need more clarification around what you're hoping to achieve.

I have a scenario where people create slack app and would provide the client credentials.

What are the client credentials provided here? I'm wondering mostly if these are associated with the same app ID and signing secrets, just a different client ID and client secret?

If so, deciding this from the installationStore that's part of the App constructor might not be possible. Sharing a few thoughts on this below!

I have been trying execute the above approach so trying to use the express middleware, to set the params required but I am unable to access the middleware here?!

I'm not sure that this path is registering as a router for your app at all - instead it might be the default /slack/install path that is reached, which might make it appear that no middleware is called:

expApp.use('/slack/install', expressReceiver.router);

As for some thoughts- it is my understanding that installationStore is intended for use with a single client, so a more detailed approach might be needed when handling multiple installations for multiple clients.

Instead, installations can be handled with the @slack/oauth package and authorizations with the authorize function.

This might require custom route handling using the router of ExpressReceiver, but should also allow for gathering client credentials from query parameters. Then, installation information can be stored on redirect and fetched during authorization of later incoming events.

Please let me know if this seems like a way to handle installations or if I'm missing some of the nuance with this! I do understand this might be a bit more code, but I'm also hoping it's an alright approach 🙏

KondaHarika commented 4 weeks ago

@zimeg, Thanks for explaining.

What are the client credentials provided here? I'm wondering mostly if these are associated with the same app ID and signing secrets, just a different client ID and client secret?

Regarding above, its different app with different app credentials. Basically, the case here is different people will have there own app with different bot name, bot logo etc, but the functionality would be eventually different. Hence I will save the credentials in the DB and provide their installation URL.

zimeg commented 4 weeks ago

@KondaHarika For sure! I'm now wondering when the app functionalities differ 🤔 It sounds like you're wanting to use the same initialization logic and listeners, but with different credentials within the app and different apps and signing secrets altogether?

Could setting up an HTTP router before the app initialization provide enough customization to the routes and request URLs needed for these different installations? I might still be needing more details on the setup you're planning! 💭