Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add built-in AwsLambdaReceiver #784

Closed
5 of 10 tasks
seratch opened this issue Feb 9, 2021 · 10 comments · Fixed by #785
Closed
5 of 10 tasks

Add built-in AwsLambdaReceiver #784

seratch opened this issue Feb 9, 2021 · 10 comments · Fixed by #785
Labels
enhancement M-T: A feature request for new functionality

Comments

@seratch
Copy link
Member

seratch commented Feb 9, 2021

Description

We have been recommending the combination of ExpressReceiver + aws-serverless-express for AWS Lambda deployments. https://slack.dev/bolt-js/deployments/aws-lambda

Although it's still a great option for running Bolt apps on AWS Lambda, initializing an Express app plus running aws-serverless-express as an adapter may cause unnecessary overhead for both booting and response time.

const { App, AwsLambdaReceiver } = require('@slack/bolt');

// processBeforeResponse: true by default
const receiver = new AwsLambdaReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const app = new App({ receiver });
app.event('app_mention', async ({event, logger}) => {
  logger.info(event);
})

module.exports.main = receiver.toHandler();

For OAuth flow handing, we still recommend using ExpressReceiver and aws-serverless-express. The reason for this is that @slack/oauth package depends on the standard HTTP module in Node. The following is a simple SAM app example. You can find the complete version here: https://github.com/seratch/bolt-js-aws-lambda

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

const myInstallationStore = {
  storeInstallation: async (installation) => { ... },
  fetchInstallation: async (query) => { ... },
};

// OAuth Flow
const expressReceiver = new ExpressReceiver({
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  scopes: process.env.SLACK_SCOPES,
  stateSecret: 'my-secret',
  installationStore: myInstallationStore
});
const awsServerlessExpress = require('aws-serverless-express');
const server = awsServerlessExpress.createServer(expressReceiver.app);
module.exports.oauthHandler = (event, context) => {
  awsServerlessExpress.proxy(server, event, context);
}

// Slack Event Handler
const eventReceiver = new AwsLambdaReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const app = new App({
  receiver: eventReceiver,
  authorize: async (source) => { myInstallationStore... }
});
app.command("/hello-bolt-js", async ({ ack }) => {
  await ack("I'm working!");
});
module.exports.eventHandler = eventReceiver.toHandler();

See also:

What type of issue is this? (place an x in one of the [ ])

  • bug
  • enhancement (feature request)
  • question
  • documentation related
  • example code related
  • testing related
  • discussion

Requirements (place an x in each of the [ ])

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.
@seratch seratch added the enhancement M-T: A feature request for new functionality label Feb 9, 2021
seratch added a commit to seratch/bolt-js that referenced this issue Feb 9, 2021
seratch added a commit to seratch/bolt-js that referenced this issue Feb 10, 2021
seratch added a commit to seratch/bolt-js that referenced this issue Feb 10, 2021
seratch added a commit to seratch/bolt-js that referenced this issue Feb 15, 2021
seratch added a commit to seratch/bolt-js that referenced this issue Feb 24, 2021
seratch added a commit to seratch/bolt-js that referenced this issue Feb 24, 2021
@mwbrooks
Copy link
Member

We'll use issue #815 to add documentation and example code for AWSLambdaReceiver.

@viperfx
Copy link

viperfx commented Oct 7, 2021

@seratch @mwbrooks I am still having issues with the AWS Lambda + OAuth process. I am using it bolt successfully for handling events, dialogs etc. But I am trying to convert our old code to both for our oauth install and callback.

We are getting the following error.

api | [WARN]   Unexpected content-type detected: undefined
api | [ERROR]   Failed to parse body as JSON data for content-type: undefined

Here is an example of the code - https://gist.github.com/viperfx/310a5ad7aed0f300889d52234b2fb85e

@seratch
Copy link
Member Author

seratch commented Oct 7, 2021

@viperfx As the @slack/oauth requires Node's HTTP interface as of today, AwsLambdaReceiver does not support the OAuth flow.

💡 If you are planning on implementing authentication with OAuth, as of today you need to use the ExpressReceiver.
https://slack.dev/bolt-js/deployments/aws-lambda

For the OAuth flow part, please consider using ExpressReceiver instead.

@viperfx
Copy link

viperfx commented Oct 7, 2021

@seratch ok - that's fine. Is there a full example of how to use the ExpressReceiver? Is the code at the top of PR what we follow?

@seratch
Copy link
Member Author

seratch commented Oct 7, 2021

@viperfx Here is an example app: https://github.com/slackapi/bolt-js/tree/main/examples/oauth-express-receiver Also, you can use https://github.com/vendia/serverless-express to make your Express functional as an AWS Lambda handler.

@viperfx
Copy link

viperfx commented Oct 7, 2021

Ah I see. Yea ideally we just want to setup everything and return a handler function. Are you aware of a method that does not involve adding an external library?

@viperfx
Copy link

viperfx commented Oct 7, 2021

PS - I gave it a quick try.

const app = new App({
  receiver,
  logLevel: LogLevel.DEBUG, // set loglevel at the App level
})

export const handler = serverlessExpress({ app })

Where receiver is an instance of ExpressReciever - and I got the following error.

api | (node:74956) UnhandledPromiseRejectionWarning: Error: Invalid app supplied. Valid frameworks include: Express, Koa, Hapi

@seratch
Copy link
Member Author

seratch commented Oct 7, 2021

Are you aware of a method that does not involve adding an external library?

I don't have any alternatives for it. The only alternative that I can suggest at this point is to implement a simple OAuth handler relying on only @slack/web-api package (= implement a simpler version of @slack/oauth on your own).

api | (node:74956) UnhandledPromiseRejectionWarning: Error: Invalid app supplied. Valid frameworks include: Express, Koa, Hapi

The default receiver is HTTPReceiver, which is not compatible with AWS Lambda runtime. That's why you're getting this error. You need to explicitly use ExpressReceiver instead.

For the future, we'd love to add an easier solution for OAuth flow using AWS Lambda but we don't have the bandwidth in the short term. I will be away from keyboard for a while. I hope this helps!

@viperfx
Copy link

viperfx commented Oct 7, 2021

We actually had a version we were using

const { InstallProvider, LogLevel } = require('@slack/oauth')
import { db } from 'src/lib/db'

export const handler = async (event) => {
  const { ping, state, code } = event.queryStringParameters

  if (ping)
    return {
      statusCode: 200,
      body: 'Pong!',
    }

  const req = {
    url: `/?code=${code}&state=${state}`,
  }

  let redirectUrl
  let installation

  const installer = new InstallProvider({
    authVersion: 'v2',
    clientId: process.env.SLACK_CLIENT_ID,
    clientSecret: process.env.SLACK_CLIENT_SECRET,
    stateSecret: process.env.SLACK_CLIENT_SECRET,
    logLevel: LogLevel.DEBUG,
  })

  const installOptions = await installer.stateStore.verifyStateParam(new Date(), state)

  const meta = JSON.parse(installOptions.metadata)
  redirectUrl = meta.redirectUrl
  const callbackOptions = {
    success: async (install, _options) => {
      installation = install
    },
  }
  await installer.handleCallback(req, null, callbackOptions)

  const newData = {
    events: {
      new_insight_new_story: false,
      new_insight_old_story: false,
      state_change: false,
      new_message_inbox: false
    },
    ...installation,
  }

  await db.teamIntegration.upsert({
    where: {
      slackWorkspaceId_teamId: {
        slackWorkspaceId: installation.team.id,
        teamId: meta.teamId,
      },
    },
    update: { data: newData, slackWorkspaceId: installation.team.id, accessToken: installation.bot.token },
    create: {
      service: 'SLACK',
      data: newData,
      slackWorkspaceId: installation.team.id,
      active: true,
      accessToken: installation.bot.token,
      team: {
        connect: {
          id: meta.teamId,
        },
      },
    },
  })
  return {
    statusCode: 302,
    headers: {
      Location: redirectUrl,
    },
  }
}

But then something changed in the slack package and we started getting the following

api | [ERROR]  OAuth:InstallProvider:1 TypeError: Cannot read property 'host' of undefined
api |     at Function.InstallProvider.extractSearchParams (/Users/viperfx/Projects/uservitals/node_modules/@slack/oauth/src/index.ts:265:80)
api |     at InstallProvider.<anonymous> (/Users/viperfx/Projects/uservitals/node_modules/@slack/oauth/src/index.ts:345:46)

As for the serverlessExpress library

The default receiver is HTTPReceiver, which is not compatible with AWS Lambda runtime. That's why you're getting this error. You need to explicitly use ExpressReceiver instead.

I am using the ExpressReceiver - mentioned in my previous comment.

const receiver = new ExpressReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  stateSecret: 'my-secret',
  scopes: ['chat:write'],
  installerOptions: {
    // If below is true, /slack/install redirects installers to the Slack authorize URL
    // without rendering the web page with "Add to Slack" button.
    // This flag is available in @slack/bolt v3.7 or higher
    // directInstall: true,
  },
  installationStore: {
    storeInstallation: async (installation) => {
      // replace database.set so it fetches from your database
      if (installation.isEnterpriseInstall && installation.enterprise !== undefined) {
        // support for org wide app installation
        return await database.set(installation.enterprise.id, installation)
      }
      if (installation.team !== undefined) {
        // single team app installation
        return await database.set(installation.team.id, installation)
      }
      throw new Error('Failed saving installation data to installationStore')
    },
    fetchInstallation: async (installQuery) => {
      // replace database.get so it fetches from your database
      if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
        // org wide app installation lookup
        return await database.get(installQuery.enterpriseId)
      }
      if (installQuery.teamId !== undefined) {
        // single team app installation lookup
        return await database.get(installQuery.teamId)
      }
      throw new Error('Failed fetching installation')
    },
  },
})

// Create the Bolt App, using the receiver
const app = new App({
  receiver,
  logLevel: LogLevel.DEBUG, // set loglevel at the App level
})

export const handler = serverlessExpress({ app })

@thomasstep
Copy link

Are there plans to make the AwsLambdaReceiver capable of handling OAuth?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement M-T: A feature request for new functionality
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants