Securely authorizing a Slack app without cookies

How we built a Slack app for 1Password

The new 1Password app for Slack automatically posts messages in Slack when events happen on your 1Password team, like when new people join, or when someone signs in on a new device.

1Password itself, however, is different from most other sites in how it handles authenticated sessions. It turns out that this difference is an important detail when building a Slack app.

No cookies, no easy answer

If you store session information in cookies, translating your service to a Slack app is made simpler. Cookies are included in HTTP 302 redirects, so you can use them to find the session and associated account on your server when Slack calls your redirect_uri.

Because we don’t want other processes on a client system to have access to 1Password session encryption keys, we don’t use cookies to store sessions on disk. Instead, we store the session encryption keys in memory on the client. That sounds great. And it is: it’s really secure! But it presented a fun puzzle to solve when creating the 1Password Slack app. How do we securely authorize the request on our server without being handed the session information in a cookie?

Problems identifying the account

Slack’s APIs allow us to specify an optional state parameter. At first, this appeared to be the obvious solution. It’s defined as “a value that’s unique to the user you’re authenticating.”

But 1Password won’t just trust a request implicitly; we need to verify it.

Problems verifying the request

An attacker could include any state parameter they choose. Our account IDs aren’t secrets, so it would be trivial for someone to obtain one. If we then naively saved the integration details to an account based on the state parameter, then all the messages from that 1Password account would be sent to the attacker’s Slack account, and all that information would be leaked.

That’s why the state parameter should never be relied on exclusively. It should only be used to verify a request. Because we don’t have a cookie to help identify the account, the state parameter wouldn’t have anything to verify.

Cookies would have let us identify the account and verify the request easily, but because we don’t use cookies to store sessions on disk, we had to find alternate solutions. We already have a solution in 1Password for authentication, and it turns out that we could use it here too. It’s called authenticated encryption, it solves the problems of verification and account identification — and it’s far more secure than using cookies.

Authenticated encryption to the rescue

1Password uses SRP to authenticate you when you sign in. This means that the client and server both end up with the same session encryption key, which the server caches for the duration of the session. A session ID and account ID are also stored along with the session encryption key:

type Session struct {
SessionID string
AccountID string
EncryptionKey WebCryptoKey
}

Identifying the account

To identify the Slack user’s 1Password account:

  1. The 1Password client includes the session ID in cleartext as part of the redirect_uri:
https://my.1password.com/api/v1/slack/redirect/GX6NAAEK7BCGFIL4ZEUMMWP4CQ

The session ID is a unique identifier that’s unrelated to the session encryption key, so it isn’t and doesn’t have to be a secret.

2. The 1Password client encrypts the account ID with the session encryption key then passes it to Slack’s authorize API in the state parameter:

https://slack.com/oauth/authorize?
scope=chat:write:bot,channels:read&
client_id=36986904051.273534103040&
redirect_uri=https://my.1password.com/api/v1/slack/redirect/GX6NAAEK7BCGFIL4ZEUMMWP4CQ
&state=<encrypted blob>

3. Slack asks the user to authorize. After a successful authorization, Slack calls the redirect_uri with the state parameter.

4. The 1Password server uses the session ID from the redirect_uri to look up the cached session.

At this point, we know which 1Password account this request is intended for.

Verifying the request

Here’s where it gets fun. Because the session ID isn’t secret, we can’t trust it. The 1Password client has to prove that it actually has the session encryption key.

To verify the request:

  1. The 1Password server decrypts the state parameter it just received using the session encryption key from the cached session.
  2. The server can then verify that the account ID is the same in both the decrypted state parameter and the cached session.

Because the request from the client is encrypted and authenticated using the secret session encryption key, the server knows that it can trust that message as authentic. Then we can save the integration details for the 1Password account matching that account ID.

Oh, and because you get to specify the channels where you want to receive alerts and notifications, 1Password won’t post any messages in Slack without your explicit instruction.

1Password :heart: Slack

Slack’s well-designed API gave us the flexibility we needed to make the redirect URL work, despite the high security needs of 1Password. As we’ve shown here, there is always a way to build apps in a secure way, and it’s worth taking the time to do so.

Sometimes it presents a bit of a puzzle, but working through problems is what we developers enjoy. Your customers will appreciate your efforts, and you’ll feel more confident in what you’ve built.

We’re thrilled that we were able to build an awesome Slack app that brings 1Password and Slack together in such a simple, beautiful way.