feat(slack): use oauth2 authentication flow
This commit is contained in:
61
packages/backend/src/apps/slack/auth/create-auth-data.ts
Normal file
61
packages/backend/src/apps/slack/auth/create-auth-data.ts
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
@@ -1,17 +1,41 @@
|
|||||||
|
import createAuthData from './create-auth-data';
|
||||||
import verifyCredentials from './verify-credentials';
|
import verifyCredentials from './verify-credentials';
|
||||||
import isStillVerified from './is-still-verified';
|
import isStillVerified from './is-still-verified';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'accessToken',
|
key: 'oAuthRedirectUrl',
|
||||||
label: 'Access Token',
|
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,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: 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,
|
clickToCopy: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -30,8 +54,12 @@ export default {
|
|||||||
value: null,
|
value: null,
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: 'accessToken',
|
name: 'consumerKey',
|
||||||
value: '{fields.accessToken}',
|
value: '{fields.consumerKey}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'consumerSecret',
|
||||||
|
value: '{fields.consumerSecret}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -40,6 +68,53 @@ export default {
|
|||||||
{
|
{
|
||||||
step: 2,
|
step: 2,
|
||||||
type: 'mutation' as const,
|
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',
|
name: 'verifyConnection',
|
||||||
arguments: [
|
arguments: [
|
||||||
{
|
{
|
||||||
@@ -75,8 +150,12 @@ export default {
|
|||||||
value: null,
|
value: null,
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: 'accessToken',
|
name: 'consumerKey',
|
||||||
value: '{fields.accessToken}',
|
value: '{fields.consumerKey}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'consumerSecret',
|
||||||
|
value: '{fields.consumerSecret}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -85,6 +164,53 @@ export default {
|
|||||||
{
|
{
|
||||||
step: 3,
|
step: 3,
|
||||||
type: 'mutation' as const,
|
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',
|
name: 'verifyConnection',
|
||||||
arguments: [
|
arguments: [
|
||||||
{
|
{
|
||||||
@@ -95,6 +221,7 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
createAuthData,
|
||||||
verifyCredentials,
|
verifyCredentials,
|
||||||
isStillVerified,
|
isStillVerified,
|
||||||
};
|
};
|
||||||
|
@@ -1,34 +1,51 @@
|
|||||||
import qs from 'qs';
|
|
||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
|
||||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||||
const headers = {
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
(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 response = await $.http.post('/oauth.v2.access', null, { params });
|
||||||
const stringifiedBody = qs.stringify({
|
|
||||||
token: $.auth.data.accessToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await $.http.post('/auth.test', stringifiedBody, {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.ok === false) {
|
if (response.data.ok === false) {
|
||||||
throw new Error(
|
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,
|
botId,
|
||||||
screenName,
|
userId,
|
||||||
|
userAccessToken,
|
||||||
|
botAccessToken,
|
||||||
|
screenName: teamName,
|
||||||
token: $.auth.data.accessToken,
|
token: $.auth.data.accessToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
const currentUser = await getCurrentUser($);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
screenName: `${currentUser.real_name} @ ${teamName}`
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default verifyCredentials;
|
export default verifyCredentials;
|
||||||
|
@@ -1,10 +1,21 @@
|
|||||||
import { TBeforeRequest } from '@automatisch/types';
|
import { TBeforeRequest } from '@automatisch/types';
|
||||||
|
|
||||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||||
if (requestConfig.headers && $.auth.data?.accessToken) {
|
const authData = $.auth.data;
|
||||||
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`;
|
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;
|
return requestConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user