Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: @slack/oauth: add support and examples for CSRF mitigation #1013

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

losandes
Copy link

@losandes losandes commented May 7, 2020

Summary

NOTE that #1014 presents an alternative solution to this PR

Adds support, and examples for mitigating Cross Site Request Forgery (CSRF) by synchronizing all 3 parties (user, Slack, app) in the OAuth flow. Uses the synchronizer token pattern (STP) and a JWT embedded in a cookie to bind the token to the device.

  • Adds new makeInstallUrl function that returns the URL that is produced, and the token that is used in the state parameter, and the synchronizer value that is in the token
  • Adds a 256 bit cryptographically random byte array (synchronizer) to the JWT body, so it can be timing safe compared with the same array stored on a device when verifying the redirect from Slack
  • Updates the examples to add an http-only, secure cookie to the device when starting the OAuth flow, and then parses the cookie so the synchronizer can be compared with the JWT
  • Updates examples to recommend using secrets of at least 256 bits
  • Reduces the lifetime of JWT tokens, so they can't be used forever
  • Adds support for configuring the lifetime of JWT tokens

Concerns / Limitations

This leaves responsibility for synchronizing the device to the consumer of this library. Since the library already deals with req, and res, it might make sense to support device synchronization as part of the library's features. That's a heavier lift, so this PR is intended to support conversation about the best way to promote CSRF mitigation when using this library.

Also, the README doesn't discuss CSRF at all. Even though it's addressed in the examples, it might be worth adding content to the README. For instance, we might want to warn against using the same token in the cookie, and the state param, since that makes it easier to spoof a synchronized device.

Requirements (place an x in each [ ])

* Updates the version to 1.1.0
* Moves TypeScript dependencies that were in the production dependencies
to the devDependencies
If the JWT doesn't expire, it can be used any time.

* Adds configuration option to limit the lifetime of the state token
* Adds default lifetime of 3 minutes
Adds documentation to the README to describe the default state lifetime,
and how to override it.
* Replaces generateInstallUrl with makeInstallUrl, which returns both
the url, and the token that was generated
* Adds support for passing the token in with the options to
callbackHandler, so it can be bound to the device in a cookie, and
compared to the token that we received from Slack
* Adds support for injecting the web client, so additional test paths
can be evaluated
* Improves test coverage
* Fixes the mock web client responses (appId should be app_id)
Because the generateInstallUrl returned a string, it wasn't extensible,
so I had to either (a) introduce a breaking change, or (b) introduce a
new function. I chose the latter, and updated the documentation to
demonstrate (b) in the examples.
* Adds dependency on `cookie` library
* Uses new makeInstallUrl function to get the redirect url, and token
* Adds the generated token to a secure, http-only cookie before
redirecting to Slack OAuth
* Parses the cookie from the headers when Slack redirects back to the
app
* Ensures the cookie exists before calling handleCallback, so there
isn't a wait-for-expiration gap
* Passes the token to handleCallback so it can be timingSafeCompared to
the JWT that Slack send back in the query string
* Adds dependency on `cookie` library
* Uses new makeInstallUrl function to get the redirect url, and token
* Adds the generated token to a secure, http-only cookie before
redirecting to Slack OAuth
* Parses the cookie from the headers when Slack redirects back to the
app
* Ensures the cookie exists before calling handleCallback, so there
isn't a wait-for-expiration gap
* Passes the token to handleCallback so it can be timingSafeCompared to
the JWT that Slack send back in the query string
My intention was to only send the random byte array as the value
for the OAuth state param, and for the JWT to only exist in the
cookie. However, that represents a potentially complex and/or
breaking change. However, if the same JWT is used as the state
param and in the cookie, and adversary who is able to capture the
redirect url can easily spoof a synchronized device. So the examples
sign JWTs specifically for the device, and provide the synchronizer
to the callbackHandler for comparison.
A test breaking change was introduced in master. This changes the test
to evaluate the new behavior.
@CLAassistant
Copy link

CLAassistant commented May 7, 2020

CLA assistant check
All committers have signed the CLA.

@codecov
Copy link

codecov bot commented May 7, 2020

Codecov Report

Merging #1013 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##           master    #1013   +/-   ##
=======================================
  Coverage   94.40%   94.40%           
=======================================
  Files          12       12           
  Lines         768      768           
  Branches      173      173           
=======================================
  Hits          725      725           
  Misses         14       14           
  Partials       29       29           
Flag Coverage Δ
#eventsapi 89.61% <ø> (ø)
#interactivemessages 95.04% <ø> (ø)
#webapi 96.36% <ø> (ø)
#webhook 87.50% <ø> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4521cb9...d96d522. Read the comment docs.

@stevengill stevengill added the pkg:oauth applies to `@slack/oauth-helper` label May 18, 2020
@clavin clavin changed the base branch from master to main July 8, 2020 02:21
@dinvlad
Copy link

dinvlad commented Sep 22, 2020

This is really nice, along with #1014, thank you for these detailed examples!

I'd like to understand the pros/cons of various options for the cookie and the state:

  1. JWT+synchronizer cookie, synchronizer state (docs: updates oauth docs with rfc-6819 examples #1014)
  2. JWT+synchronizer cookie, JWT+synchronizer state (feat: @slack/oauth: add support and examples for CSRF mitigation #1013)
  3. Synchronizer cookie, JWT+synchronizer state (which from your note above, is less secure than 2, since it doesn't have a timing component on the cookie, besides browser expiration)
  4. Encrypted versions of these (with or without JWT signing)

Is it purely a matter of taste, or are there security benefits to any of these approaches?

Also, just to be a little pedantic here - strictly speaking, the OWASP guide on STP says that

CSRF tokens should not be transmitted using cookies.

and notes that STP is more of a stateful protocol, where the CSRF token should be stored in user session on the server (instead of a cookie). For stateless apps, it's more appropriate to use Encryption, Hash, or Double Submit Cookie TP. The last one seems closest to what we're using here (though we're using HMAC+cookie+state instead of cookie+cookie). In any case, whichever terminology is used, they seem to recommend also encrypting the CSRF cookie to enhance the security of this solution. Any thoughts about the necessity of it?

Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg:oauth applies to `@slack/oauth-helper`
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants