From f05750161178896ff29f40a50b630ccdea2b7e6d Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 2 Nov 2022 20:24:25 +0100 Subject: [PATCH] feat(slack): use oauth2 authentication flow --- .../src/apps/slack/auth/create-auth-data.ts | 61 ++++++++ packages/backend/src/apps/slack/auth/index.ts | 141 +++++++++++++++++- .../src/apps/slack/auth/verify-credentials.ts | 49 ++++-- .../src/apps/slack/common/add-auth-header.ts | 15 +- 4 files changed, 241 insertions(+), 25 deletions(-) create mode 100644 packages/backend/src/apps/slack/auth/create-auth-data.ts diff --git a/packages/backend/src/apps/slack/auth/create-auth-data.ts b/packages/backend/src/apps/slack/auth/create-auth-data.ts new file mode 100644 index 00000000..2e47305b --- /dev/null +++ b/packages/backend/src/apps/slack/auth/create-auth-data.ts @@ -0,0 +1,61 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import qs from 'qs'; + +export default async function createAuthData($: IGlobalVariable) { + const scopes = [ + 'channels:manage', + 'channels:read', + 'channels:join', + 'chat:write', + 'chat:write.customize', + 'chat:write.public', + 'files:write', + 'im:write', + 'mpim:write', + 'team:read', + 'users.profile:read', + 'users:read', + 'workflow.steps:execute', + 'users:read.email', + 'commands', + ]; + const userScopes = [ + 'channels:history', + 'channels:read', + 'channels:write', + 'chat:write', + 'emoji:read', + 'files:read', + 'files:write', + 'groups:history', + 'groups:read', + 'groups:write', + 'im:write', + 'mpim:write', + 'reactions:read', + 'reminders:write', + 'search:read', + 'stars:read', + 'team:read', + // 'users.profile:read', + 'users.profile:write', + 'users:read', + 'users:read.email', + ]; + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + const searchParams = qs.stringify({ + client_id: $.auth.data.consumerKey as string, + redirect_uri: redirectUri, + scope: scopes.join(','), + user_scope: userScopes.join(','), + }); + + const url = `${$.app.baseUrl}/oauth/v2/authorize?${searchParams}`; + + await $.auth.set({ + url, + }); +}; diff --git a/packages/backend/src/apps/slack/auth/index.ts b/packages/backend/src/apps/slack/auth/index.ts index 0f36afcc..88569ba0 100644 --- a/packages/backend/src/apps/slack/auth/index.ts +++ b/packages/backend/src/apps/slack/auth/index.ts @@ -1,17 +1,41 @@ +import createAuthData from './create-auth-data'; import verifyCredentials from './verify-credentials'; import isStillVerified from './is-still-verified'; export default { fields: [ { - key: 'accessToken', - label: 'Access Token', + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/slack/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Slack OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'consumerKey', + label: 'API Key', type: 'string' as const, required: true, readOnly: false, value: null, placeholder: null, - description: 'Access token of slack that Automatisch will connect to.', + description: null, + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'API Secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, clickToCopy: false, }, ], @@ -30,8 +54,12 @@ export default { value: null, properties: [ { - name: 'accessToken', - value: '{fields.accessToken}', + name: 'consumerKey', + value: '{fields.consumerKey}', + }, + { + name: 'consumerSecret', + value: '{fields.consumerSecret}', }, ], }, @@ -40,6 +68,53 @@ export default { { step: 2, type: 'mutation' as const, + name: 'createAuthData', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, + { + step: 3, + type: 'openWithPopup' as const, + name: 'openAuthPopup', + arguments: [ + { + name: 'url', + value: '{createAuthData.url}', + }, + ], + }, + { + step: 4, + type: 'mutation' as const, + name: 'updateConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + { + name: 'formattedData', + value: null, + properties: [ + { + name: 'code', + value: '{openAuthPopup.code}', + }, + { + name: 'state', + value: '{openAuthPopup.state}', + }, + ], + }, + ], + }, + { + step: 5, + type: 'mutation' as const, name: 'verifyConnection', arguments: [ { @@ -75,8 +150,12 @@ export default { value: null, properties: [ { - name: 'accessToken', - value: '{fields.accessToken}', + name: 'consumerKey', + value: '{fields.consumerKey}', + }, + { + name: 'consumerSecret', + value: '{fields.consumerSecret}', }, ], }, @@ -85,6 +164,53 @@ export default { { step: 3, type: 'mutation' as const, + name: 'createAuthData', + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + ], + }, + { + step: 4, + type: 'openWithPopup' as const, + name: 'openAuthPopup', + arguments: [ + { + name: 'url', + value: '{createAuthData.url}', + }, + ], + }, + { + step: 5, + type: 'mutation' as const, + name: 'updateConnection', + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + { + name: 'formattedData', + value: null, + properties: [ + { + name: 'code', + value: '{openAuthPopup.code}', + }, + { + name: 'state', + value: '{openAuthPopup.state}', + }, + ], + }, + ], + }, + { + step: 6, + type: 'mutation' as const, name: 'verifyConnection', arguments: [ { @@ -95,6 +221,7 @@ export default { }, ], + createAuthData, verifyCredentials, isStillVerified, }; diff --git a/packages/backend/src/apps/slack/auth/verify-credentials.ts b/packages/backend/src/apps/slack/auth/verify-credentials.ts index 0152348a..3f3feefc 100644 --- a/packages/backend/src/apps/slack/auth/verify-credentials.ts +++ b/packages/backend/src/apps/slack/auth/verify-credentials.ts @@ -1,34 +1,51 @@ -import qs from 'qs'; import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; const verifyCredentials = async ($: IGlobalVariable) => { - const headers = { - 'Content-Type': 'application/x-www-form-urlencoded', + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + const params = { + code: $.auth.data.code, + client_id: $.auth.data.consumerKey, + client_secret: $.auth.data.consumerSecret, + redirect_uri: redirectUri, }; - - const stringifiedBody = qs.stringify({ - token: $.auth.data.accessToken, - }); - - const response = await $.http.post('/auth.test', stringifiedBody, { - headers, - }); + const response = await $.http.post('/oauth.v2.access', null, { params }); if (response.data.ok === false) { throw new Error( - `Error occured while verifying credentials: ${response.data.error}.(More info: https://api.slack.com/methods/auth.test#errors)` + `Error occured while verifying credentials: ${response.data.error}. (More info: https://api.slack.com/methods/oauth.v2.access#errors)` ); } - const { bot_id: botId, user: screenName } = response.data; + const { + bot_user_id: botId, + authed_user: { + id: userId, + access_token: userAccessToken, + }, + access_token: botAccessToken, + team: { + name: teamName, + } + } = response.data; - $.auth.set({ + await $.auth.set({ botId, - screenName, + userId, + userAccessToken, + botAccessToken, + screenName: teamName, token: $.auth.data.accessToken, }); - return response.data; + const currentUser = await getCurrentUser($); + + await $.auth.set({ + screenName: `${currentUser.real_name} @ ${teamName}` + }); }; export default verifyCredentials; diff --git a/packages/backend/src/apps/slack/common/add-auth-header.ts b/packages/backend/src/apps/slack/common/add-auth-header.ts index f4f2076f..89ab2eec 100644 --- a/packages/backend/src/apps/slack/common/add-auth-header.ts +++ b/packages/backend/src/apps/slack/common/add-auth-header.ts @@ -1,10 +1,21 @@ import { TBeforeRequest } from '@automatisch/types'; const addAuthHeader: TBeforeRequest = ($, requestConfig) => { - if (requestConfig.headers && $.auth.data?.accessToken) { - requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`; + const authData = $.auth.data; + if ( + requestConfig.headers + && authData?.userAccessToken + && authData?.botAccessToken + ) { + if (requestConfig.additionalProperties?.sendAsBot) { + requestConfig.headers.Authorization = `Bearer ${authData.botAccessToken}`; + } else { + requestConfig.headers.Authorization = `Bearer ${authData.userAccessToken}`; + } } + requestConfig.headers['Content-Type'] = requestConfig.headers['Content-Type'] || 'application/json; charset=utf-8'; + return requestConfig; };