diff --git a/package.json b/package.json index 7b3212827..7a8afef56 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@slack/logger": "^2.0.0", + "@slack/oauth": "file:../node-slack-sdk/packages/oauth", "@slack/types": "^1.5.0", "@slack/web-api": "^5.8.0", "@types/express": "^4.16.1", diff --git a/src/App.ts b/src/App.ts index d6a2c37c0..0e2754369 100644 --- a/src/App.ts +++ b/src/App.ts @@ -63,6 +63,14 @@ export interface AppOptions { signingSecret?: ExpressReceiverOptions['signingSecret']; endpoints?: ExpressReceiverOptions['endpoints']; processBeforeResponse?: ExpressReceiverOptions['processBeforeResponse']; + clientId?: ExpressReceiverOptions['clientId']; + clientSecret?: ExpressReceiverOptions['clientSecret']; + stateStore?: ExpressReceiverOptions['stateStore']; // default ClearStateStore + stateSecret?: ExpressReceiverOptions['stateSecret']; // required when using default stateStore + installationStore?: ExpressReceiverOptions['installationStore']; // default MemoryInstallationStore + authVersion?: ExpressReceiverOptions['authVersion']; // default 'v2' + scopes?: ExpressReceiverOptions['scopes']; + metadata?: ExpressReceiverOptions['metadata']; agent?: Agent; clientTls?: Pick; convoStore?: ConversationStore | false; @@ -83,7 +91,7 @@ export { LogLevel, Logger } from '@slack/logger'; export interface Authorize { ( source: AuthorizeSourceData, - body: AnyMiddlewareArgs['body'], + body?: AnyMiddlewareArgs['body'], ): Promise; } @@ -158,7 +166,7 @@ export default class App { private logger: Logger; /** Authorize */ - private authorize: Authorize; + private authorize!: Authorize; /** Global middleware chain */ private middleware: Middleware[]; @@ -186,6 +194,14 @@ export default class App { ignoreSelf = true, clientOptions = undefined, processBeforeResponse = false, + clientId = undefined, + clientSecret = undefined, + stateStore = undefined, + stateSecret = undefined, + installationStore = undefined, + authVersion = 'v2', + scopes = undefined, + metadata = undefined, }: AppOptions = {}) { if (typeof logger === 'undefined') { @@ -218,21 +234,6 @@ export default class App { clientTls, )); - if (token !== undefined) { - if (authorize !== undefined) { - throw new AppInitializationError( - `Both token and authorize options provided. ${tokenUsage}`, - ); - } - this.authorize = singleTeamAuthorization(this.client, { botId, botUserId, botToken: token }); - } else if (authorize === undefined) { - throw new AppInitializationError( - `No token and no authorize options provided. ${tokenUsage}`, - ); - } else { - this.authorize = authorize; - } - this.middleware = []; this.listeners = []; @@ -252,11 +253,50 @@ export default class App { signingSecret, endpoints, processBeforeResponse, + clientId, + clientSecret, + stateStore, + stateSecret, + installationStore, + authVersion, + metadata, + scopes, logger: this.logger, }); } } + let usingBuiltinOauth = undefined; + if ( + clientId !== undefined + && clientSecret !== undefined + && (stateSecret !== undefined || stateStore !== undefined) + && this.receiver instanceof ExpressReceiver + ) { + usingBuiltinOauth = true; + } + + if (token !== undefined) { + if (authorize !== undefined || usingBuiltinOauth !== undefined) { + throw new AppInitializationError( + `token as well as authorize options or installer options were provided. ${tokenUsage}`, + ); + } + this.authorize = singleTeamAuthorization(this.client, { botId, botUserId, botToken: token }); + } else if (authorize === undefined && usingBuiltinOauth === undefined) { + throw new AppInitializationError( + `No token, no authorize options, and no installer options provided. ${tokenUsage}`, + ); + } else if (authorize !== undefined && usingBuiltinOauth !== undefined) { + throw new AppInitializationError( + `Both authorize options and installer options provided. ${tokenUsage}`, + ); + } else if (authorize === undefined && usingBuiltinOauth !== undefined) { + this.authorize = (this.receiver as ExpressReceiver).oauthAuthorize as Authorize; + } else if (authorize !== undefined && usingBuiltinOauth === undefined) { + this.authorize = authorize; + } + // Conditionally use a global middleware that ignores events (including messages) that are sent from this app if (ignoreSelf) { this.use(ignoreSelfMiddleware()); diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index fb1a95a20..c8ad1c50e 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -1,6 +1,6 @@ import { AnyMiddlewareArgs, Receiver, ReceiverEvent } from './types'; import { createServer, Server } from 'http'; -import express, { Request, Response, Application, RequestHandler } from 'express'; +import express, { Request, Response, Application, RequestHandler, Router } from 'express'; import rawBody from 'raw-body'; import querystring from 'querystring'; import crypto from 'crypto'; @@ -8,6 +8,7 @@ import tsscmp from 'tsscmp'; import App from './App'; import { ReceiverAuthenticityError, ReceiverMultipleAckError } from './errors'; import { Logger, ConsoleLogger } from '@slack/logger'; +import { InstallProvider, StateStore, InstallationStore, OAuthInterface, CallbackOptions } from '@slack/oauth'; // TODO: we throw away the key names for endpoints, so maybe we should use this interface. is it better for migrations? // if that's the reason, let's document that with a comment. @@ -18,6 +19,15 @@ export interface ExpressReceiverOptions { [endpointType: string]: string; }; processBeforeResponse?: boolean; + clientId?: string; + clientSecret?: string; + stateStore?: StateStore; // default ClearStateStore + stateSecret?: string; // ClearStateStoreOptions['secret']; // required when using default stateStore + installationStore?: InstallationStore; // default MemoryInstallationStore + authVersion?: 'v1' | 'v2'; // default 'v2' + scopes?: string | string[]; + metadata?: string; + oAuthCallbackOptions?: CallbackOptions; } /** @@ -32,18 +42,28 @@ export default class ExpressReceiver implements Receiver { private bolt: App | undefined; private logger: Logger; private processBeforeResponse: boolean; + public router: Router; + public oauthAuthorize: OAuthInterface['authorize'] | undefined; constructor({ signingSecret = '', logger = new ConsoleLogger(), endpoints = { events: '/slack/events' }, processBeforeResponse = false, + clientId = undefined, + clientSecret = undefined, + stateStore = undefined, + stateSecret = undefined, + installationStore = undefined, + authVersion = 'v2', + metadata = undefined, + scopes = undefined, }: ExpressReceiverOptions) { this.app = express(); // TODO: what about starting an https server instead of http? what about other options to create the server? this.server = createServer(this.app); - const expressMiddleware: RequestHandler[] = [ + const expressMiddleware = [ verifySignatureAndParseRawBody(logger, signingSecret), respondToSslCheck, respondToUrlVerification, @@ -52,9 +72,49 @@ export default class ExpressReceiver implements Receiver { this.processBeforeResponse = processBeforeResponse; this.logger = logger; - const endpointList: string[] = typeof endpoints === 'string' ? [endpoints] : Object.values(endpoints); + const endpointList = typeof endpoints === 'string' ? [endpoints] : Object.values(endpoints); + this.router = Router(); for (const endpoint of endpointList) { - this.app.post(endpoint, ...expressMiddleware); + this.router.post(endpoint, ...expressMiddleware); + } + this.app.use(this.router); + this.oauthAuthorize = undefined; + + let oauthInstaller: OAuthInterface | undefined = undefined; + if ( + clientId !== undefined + && clientSecret !== undefined + && (stateSecret !== undefined || stateStore !== undefined) + ) { + + oauthInstaller = new InstallProvider({ + clientId, + clientSecret, + stateSecret, + stateStore, + installationStore, + authVersion, + }); + } + + // Add OAuth routes to receiver + if (oauthInstaller !== undefined) { + this.app.use('/slack/install', (req, res) => { + return oauthInstaller!.handleCallback(req, res, {}); + }); + this.app.get('/slack/begin_auth', (_req, res, next) => { + // TODO, take in arguments or function for + oauthInstaller!.generateInstallUrl({ + metadata, + scopes: scopes!, + }).then((url: string) => { + res.send(``); + }).catch(next); + }); + this.oauthAuthorize = oauthInstaller.authorize; } }