When you need to integrate with external services, you can use the Slack CLI to encrypt and to store OAuth2 credentials.
OAuth2 stands for Open Authorization 2.0. It is a standard designed to allow apps to access resources hosted by other apps on behalf of a user. Unlike basic authorization, where you share a password with a user, OAuth uses access tokens to verify a user's identity.
The following steps guide you through integrating your app with an external service using Google as an example.
The first step is to obtain your OAuth credentials. To do that, create a new OAuth2 credential with the external service you'll be integrating with.
For our example, navigate to the Google API Console to obtain your OAuth2 client ID and client secret.
If you're asked to provide a redirect URL, use the following:
https://oauth2.slack.com/external/auth/callback
When you're done creating your credential, copy the credential's client ID and client secret, then head to your manifest file (manifest.ts
).
Next, tell your app about your OAuth2 provider by defining an OAuth2 provider within your app. Inside your app's manifest, import DefineOAuth2Provider
. Then, create a new provider instance.
An example provider instance for Google is below. You can define it right in your manifest, or put it in its own file and import it into the manifest.
// manifest.ts
import { DefineFunction, DefineOAuth2Provider, DefineWorkflow, Manifest, Schema,} from "deno-slack-sdk/mod.ts";
// ...
// Define a new OAuth2 provider
// Note: replace <your_client_id> with your actual client ID
// if you're following along and creating an OAuth2 provider for
// Google.
const GoogleProvider = DefineOAuth2Provider({
provider_key: "google",
provider_type: Schema.providers.oauth2.CUSTOM,
options: {
"provider_name": "Google",
"authorization_url": "https://accounts.google.com/o/oauth2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"client_id": "<your_client_id>.apps.googleusercontent.com",
"scope": [
"https://www.googleapis.com/auth/spreadsheets.readonly",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
],
"authorization_url_extras": {
"prompt": "consent",
"access_type": "offline",
},
"identity_config": {
"url": "https://www.googleapis.com/oauth2/v1/userinfo",
"account_identifier": "$.email",
},
},
});
// ...
OAuth2 provider properties are described in the table below.
Field | Type | Description |
---|---|---|
provider_key |
string |
The unique string identifier for a provider. An app cannot have two providers with the same unique identifier. Changing unique identifiers will be treated as the deletion of a provider. Providers with active tokens cannot be deleted. |
provider_type |
Schema.providers.oauth2 |
The only supported provider type value at this time is Schema.providers.oauth2.CUSTOM . |
provider_name |
string |
The name of your provider. |
client_id |
string |
The client ID from your provider. |
authorization_url |
string |
An OAuth2 requirement to complete the OAuth2 flow and to direct the user to the provider consent screen. |
token_url |
string |
An OAuth2 requirement to complete the OAuth2 flow and to exchange the code with an access token. |
scope |
array |
An OAuth2 requirement to complete the OAuth2 flow and to grant only the scopes provided to the access token. |
identity_config |
object |
Used to obtain user identity by finding the account associated with the access token. Must include url (the endpoint the provider exposes to fetch user identity) and account_identifier (the field in the response that represents the identity). May also include headers (object ), in case your OAuth2 provider requires additional headers to fetch user identity. |
authorization_url_extras |
object |
(Optional) This is for providers who require additional query parameters in their authorize_url . |
If the same user has two different accounts for the the same provider, but the identity_config
is configured such that the same external_user_id
value (obtained via the identity_config
property) is returned in case of both the accounts, the user will not be able to issue multiple tokens for multiple accounts as existing tokens will be overwritten if the external user IDs are the same. This identity is what is used to identify different accounts for a user for the same provider, and only one token per identity is stored for a user, team, and provider combination.
In your manifest file, insert the newly-defined provider into the externalAuthProviders
property (if that property doesn't exist yet, go ahead and create it):
export default Manifest({
//...
// Tell your app about your OAuth2 providers here:
externalAuthProviders: [GoogleProvider],
//...
});
Now, with your OAuth2 provider defined and your manifest configured to use it, you can encrypt and store your client secret so that your app's users can utilize the OAuth2 authorization flow.
Your app needs to be deployed to Slack once in order to create a place to store your encrypted client secret. Run the slack deploy
command in your terminal window:
slack deploy
This command will bring up a list of currently authorized workspaces. Select the workspace where your app will exist, and wait for the CLI to finish deploying.
When finished, stay in your terminal window to add your client secret for the newly-defined provider, ensuring that you wrap the secret string in double quotes as follows:
slack external-auth add-secret --provider google --secret "GOCSPX-abc123..."
Running the add-secret
command will bring up a list of workspaces available to you. Find and select the workspace you recently deployed your app to; you'll know it's the workspace you recently installed the app in by locating the item in the list with your app's name and ID (e.g., myapp A01BC...
) rather than "App is not installed to this workspace."
If you get a provider_not_found
error, go back to your manifest file and check to make sure that you included your OAuth2 provider in the externalAuthProviders
properties of your manifest definition.
If everything was successful, the CLI will let you know:
✨ successfully added external auth client secret for google
Great! With your app configured to interact with your defined OAuth2 provider, we can now initialize the OAuth2 sign-in flow, connecting your external provider to your Slack app.
Once your provider's client secret has been added, it's time to create a token for your app to interact with your OAuth2 provider with external-auth add
.
Run the following command:
slack external-auth add
This will display a list of workspaces your app is deployed to. Select the one you're currently working in. Upon selection, you'll be provided a list of all providers that have been defined for this app, along with whether there's a secret and token.
$ slack external-auth add
? Select a provider [Use arrows to move, type to filter]
> Provider Key: google
Provider Name: Google
Client ID: <your_id>.apps.googleusercontent.com
Client Secret Exists? Yes
Token Exists? No
If you have just created a provider, you'll notice that it reports no tokens existing. Let's go ahead and create a token by initializing the OAuth2 sign-in flow.
Select the provider you're working on, which will open a browser window for you to complete the OAuth2 sign-in flow according to your provider's requirements. You'll know you're successful when your browser sends you to a oauth2.slack.com
page stating that your account was successfully connected.
Verify that a valid token has been created by re-running the external-auth add
command:
slack external-auth add
? Select a provider [Use arrows to move, type to filter]
> Provider Key: google
Provider Name: Google
Client ID: <your_id>.apps.googleusercontent.com
Client Secret Exists? Yes
Token Exists? Yes
If you see Token Exists? Yes
, that means a valid auth token has been created, and you're ready to use OAuth2 in your app! Exit out of this command flow by entering Ctrl+C
in your terminal — otherwise you'll be guided through the OAuth2 sign-in flow again.
Your custom functions can leverage your provider's token
by configuring it to receive a Schema.slack.types.oauth2
type as an input parameter to your function's definition.
Here's how that might look if we were to use the sample function from the starter template:
// functions/sample_function.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
export const SampleFunctionDefinition = DefineFunction({
callback_id: "sample_function",
title: "Sample function",
description: "A sample function",
source_file: "functions/sample_function.ts",
input_parameters: {
properties: {
message: {
type: Schema.types.string,
description: "Message to be posted",
},
// Define token here
googleAccessTokenId: {
type: Schema.slack.types.oauth2,
oauth2_provider_key: "google",
},
user: {
type: Schema.slack.types.user_id,
description: "The user invoking the workflow",
},
},
required: ["user"],
},
output_parameters: {
properties: {
updatedMsg: {
type: Schema.types.string,
description: "Updated message to be posted",
},
},
required: ["updatedMsg"],
},
});
export default SlackFunction(
SampleFunctionDefinition, // Define custom function
async ({ inputs, client }) => {
// Get the token:
const tokenResponse = await client.apps.auth.external.get({
external_token_id: inputs.googleAccessTokenId,
});
if (tokenResponse.error) {
const error =
`Failed to retrieve the external auth token due to ${tokenResponse.error}`;
return { error };
}
// If the token was retrieved successfully, use it:
const externalToken = tokenResponse.external_token;
// Make external API call with externalToken
const response = await fetch("https://somewhere.tld/myendpoint", {
headers: new Headers({
"Authorization": `Bearer ${externalToken}`,
"Content-Type": "application/x-www-form-urlencoded",
}),
});
if (response.status != 200) {
const body = await response.text();
const error =
`Failed to call my endpoint! (status: ${response.status}, body: ${body})`;
return { error };
}
// Do something here
const myApiResponse = await response.json();
const updatedMsg =
`:newspaper: Message for <@${inputs.user}>!\n\n>${myApiResponse}`;
return { outputs: { updatedMsg } };
},
);
To invoke your function that uses your token, pass an object with a credential_source
to specify which user's
token should be selected. For tokens added using slack external-auth add
, use credential_source: "DEVELOPER"
.
// Somewhere in your workflow's implementation:
const sampleFunctionStep = SampleWorkflow.addStep(SampleFunctionDefinition, {
user: SampleWorkflow.inputs.user,
googleAccessTokenId: {
credential_source: "DEVELOPER"
},
});
After deploying the above manifest changes, you have to select a specific account for each of your in-code workflows in this app. Assuming that you had run the slack external-auth add
command before to add an external account, use the command slack external-auth select-auth
as shown below:
slack external-auth select-auth
? Select a workspace <workspace_name> <workspace_id>
? Choose an app environment Deployed <app_id>
? Select a workflow Workflow: #/workflows/<workspace_name>
Providers:
Key: google, Name: Google, Selected Account: None
? Select a provider Key: google, Name: Google, Selected Account: None
? Select an external account Account: <your_id>@gmail.com, Last Updated: 2023-05-30
✨ Workflow #/workflows/<workspace_name> will use developer account <your_id>@gmail.com when making calls to google APIs
Slack will automatically inject your app's oauth2 token ID into the oauth2 input property; you do not need to provide a token ID here.
Multiple collaborators can exist for the same app and each collaborator can create a token using the slack external-auth add
command. To select the appropriate collaborator account to run a specific workflow, the same slack external-auth select-auth
command can be used. However, a collaborator needs to set up their own account using slack external-auth select-auth
command by invoking this command. i.e. a collaborator cannot use slack external-auth select-auth
to select auth for a workflow on behalf of another collaborator for the same app.
A collaborator can remove their account by running slack external-auth remove
command. This would automatically delete the existing selected auths for each of the workflows that were using it. Therefore, in such a case, slack external-auth select-auth
command would be needed to be invoked again before executing the relevant workflows successfully later.
If you ever want to force a refresh of your external token as a part of error handling, retry mechanism, or something similar, you can use the sample code below:
// Somewhere in your functions error handling and retry logic:
const result = await client.apps.auth.external.get({
external_token_id: inputs.googleAccessTokenId,
force_refresh: true // default force_refresh is false
});
If you ever want to delete your external token programmatically, you can use the sample code below. Bear in mind that once a token is deleted, all workflows that were previously using the token will no longer work.
This will not revoke the token from the provider's system. It will only delete the reference to the token from Slack and prevent it from being used within the external authentication system.
// Somewhere in your function:
await client.apps.auth.external.delete({
external_token_id: inputs.googleAccessTokenId,
});
If you'd like to delete your tokens and remove OAuth2 authentication from your Slack app, the following commands will allow you to do so:
Command | Description |
---|---|
$ slack external-auth remove |
Choose a provider to delete tokens for from the list displayed. |
$ slack external-auth remove --all |
Delete all tokens for the app by specifying the --all flag. |
$ slack external-auth remove --provider provider_name --<app_name> |
Delete all tokens for a provider by specifying the --provider flag. |
This will not revoke the token from the provider's system. It will only delete the reference to the token from Slack and prevent it from being used within the external authentication system.
You can view external authentication logs via slack activity
. These logs contain information about errors encountered by users during the OAuth2 exchange and workflow execution. Below are some common errors:
Error | Description |
---|---|
access_token_exchange_failed |
An error was returned from the configured token_url . |
external_user_identity_not_found |
The configured account_identifier was not found in user identity response. |
internal_error |
An internal system error happened. Please reach out to Slack if this occurs consistently. |
invalid_identity_config_response |
url in the configured identity_config returned an invalid response. |
invalid_token_response |
token_url returned an invalid response. |
missing_client_secret |
No client secret was found for this provider. |
no_refresh_token |
Token to refresh the expired access token does not exist. |
oauth2_callback_error |
The OAuth2 provider returned an error. |
oauth2_exchange_error |
There was an error while obtaining the OAuth2 token from the configured provider. |
scope_mismatch_error |
Slack was not able to find an OAuth2 token that matched the scope configured on your provider. |
token_not_found |
Slack was not able to find an OAuth2 token for this user and provider. |
Have 2 minutes to provide some feedback?
We'd love to hear about your experience building modular Slack apps. Please complete our short survey so we can use your feedback to improve.