Building a Slack app that authenticates into external system using OAUTH

Viewed 727

I am in the process of building a small test slack app and not clear on the architecture that is needed for authentication. This will be a NodeJS application that lives on Heroku.

When a user uses a /slash command, it is going to invoke logic that will query an external CRM system and return data. In order to authenticate into this external system, it would need to send the user through an OAUTH flow so that the access to the data is governed by the invoking users' permissions.

My confusion is to how to handle or persist these auth tokens/refresh tokens that we get back from the user authenticating during this process.

Example Steps:

  1. Runs /user bob@gmail.com
  2. Checks to see if user has authorized on this external system
  3. If not, take the user through the external systems oauth flow
  4. After authentication, we have the token that can be used to make callouts to the external systems API as that user.
  5. Makes callout and returns data

How would I persist or check for the slack users auth/refresh token when they run the command to see if we already have it? If the token already existed, I wouldn't need to send them through the OAUTH flow again.

My Thoughts on the approach:

It almost seems like there needs to be a data store of some type that contains the slack user ID, auth token, and refresh token. When the user invokes the command, we check to see if that user is in the table and if so, we use their token to make the API call.

If they don't exist, we then send them through the OAUTH flow so we can store them.

Final Thoughts:

In terms of security, is having a table of tokens the correct way to do this? It almost seems like it's the equivalent of storing a plain text password if someone were to get that token.

Is there a better way to handle this or is this a common approach?

1 Answers

Your approach is right and the docs in slack API points out to this article that describe your use case where the third party is the Salesforce CRM.

In terms of security, is having a table of tokens the correct way to do this? ...

Yes, an attacher may steal your db data. To avoid that you can store the tokens as encripted string. In this way, a malicious user should:

  • steal your data from the db
  • steal your source code to understand what type of algorithm you are using to encript the tokens and the logic behind it

The approach is to spread all the info to get the clear token across system assuming one or more systems can be compromised, not everyone!

Is there a better way to handle this or is this a common approach?

Usually the AES-256 is used, in detail aes-256-gcm or aes-256-cbc. There are some thread off in performance and use cases you must deal with in order to prefer one or another.

Node.js supports both and an example logic could be:

const crypto = require('crypto')

const algorithm = 'aes-256-gcm'
const authTagByteLen = 16
const ivByteLen = 64
const keyByteLen = 32
const saltByteLen = 32

const oauthToken = 'messagetext'
const slackUserId = 'useThisAsPassword'

const salt = crypto.randomBytes(saltByteLen)
const key = crypto.scryptSync(
  Buffer.from(slackUserId, 'base64').toString('base64'),
  Buffer.from(salt, 'base64').toString('base64'),
  keyByteLen)

const iv = crypto.randomBytes(ivByteLen)
const cipher = crypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagByteLen })

let encryptedMessage = cipher.update(oauthToken)
encryptedMessage = Buffer.concat([encryptedMessage, cipher.final()])
const storeInDb = Buffer.concat([iv, encryptedMessage, cipher.getAuthTag()]).toString('base64')

/***
 *
 */

const storeInDbBuffer = Buffer.from(storeInDb, 'base64')
const authTag = storeInDbBuffer.slice(-authTagByteLen)
const iv2 = storeInDbBuffer.slice(0, ivByteLen)
const toDencryptMessage = storeInDbBuffer.slice(ivByteLen, -authTagByteLen)
const decipher = crypto.createDecipheriv(algorithm, key, iv2, { authTagLength: authTagByteLen })
decipher.setAuthTag(authTag)
const messagetext = decipher.update(toDencryptMessage)
decipher.final()
const clearText = messagetext.toString()

console.log({
  oauthToken,
  storeInDb,
  clearText
})

Notice:

  • the SALT logic will generate a new "storeInDb" string at every run without compromising the future reads
  • you may use the slack-user-id as password, so the attacher should know this information too
  • you must store the salt too or write an algorithm to generate it form the user id for example
  • the salt may be stored in a (redis) cache or other Services like S3, so the attaccher should break this other system to parse the tokens!

GCM example extrated by my module You may find a CBC example here

Related