riboseinc / rails_encrypted_token

encrypted token-based CSRF for Rails
MIT License
0 stars 0 forks source link

Implement integration with Devise Token Auth #2

Open ribose-jeffreylau opened 6 years ago

ribose-jeffreylau commented 6 years ago

Server side

Devise Token Auth provides a token-based authentication mechanism for Rails. It differs from this project in that it doesn't use the encrypted token pattern, but it provides a greater level of integration with Rails w.r.t. actual interactions with the app's User object, which is desired.

We'd like to make Devise Token Auth and Rails Encrypted Token work side-by-side:

This task entails creation of a dummy app that has both gems configured, enabling CSRF protection from pre-login to post-login.

Operationally, the hand-over stage seems like the most uncertain point of this integration, and thus will naturally require thorough integration RSpec tests.

Client side

On the client side, tests could be done using one of the JS libraries for devise token auth, e.g., j-toker, in combination with csrf-et. The test suite itself can be PhantomJS or any other appropriate frameworks.

ronaldtse commented 6 years ago

Thank you @ribose-jeffreylau !

Appreciate your help @skalee !

skalee commented 6 years ago

I am not sure if I understand purpose of this gem.

As far as I understand, you want two separate token-based authentication logics: one for anonymous users, and another one for registered ones. But I wonder, what's the reason for such complication? Why not guest user records, for example? And if user records are not a good option for particular scenario, why not attaching Devise to another model as well?

Furthermore, I've noticed that this gem depends on rotp gem which provides implementation of HMAC-Based One-Time Password Algorithm (RFC). However, this gem is not actually used, and removing it from doesn't cause specs failure. It looks like there is some home-made implementation (I'm not sure yet whether it's complete or not, neither if it's a full blown HOTP). That brings a question: is this that home-made implementation a temporary or permanent solution? Should this gem follow RFC 4226 at all?

Sorry for answering that late. My last week was kind of odd, and I couldn't spend much time on any work.

cc @ronaldtse @ribose-jeffreylau

ronaldtse commented 6 years ago

The ROTP inclusion was a mistake, the scheme does not depend on ROTP. The purpose of this mechanism is to replace the typical CSRF protection.

CSRF protection usually depends on a session, which requires the server to store state. This mechanism uses an encrypted challenge and response to achieve verification and therefore does not require any additional state per user (other than what the server already had) and is therefore more scalable.

This mechanism is purely aimed for sessionless verification, and not to replace user authenticated sessions. Therefore we to allow a “transition” between the two mechanisms. I.e. the login page will require the first token to be sent with user credentials for session establishment, then the session token (token stream) will be used for subsequent authentications (for API).

skalee commented 6 years ago

Just to confirm: this gem has nothing to do with tracking particular user, it's only a stateless CSRF protection, correct?

Also, how does it relate to Devise? I suppose that all form submissions should be protected, not only Devise login forms. Am I correct?

ronaldtse commented 6 years ago

Yes, correct. 👍

And indeed -- all unauthenticated form submissions should also be protected. Thanks!

ribose-jeffreylau commented 6 years ago

Hi @skalee , do you need more information to work on this? Thanks!

skalee commented 6 years ago

Not yet, thanks!

skalee commented 6 years ago

The whole idea and algorithm are clear to me. Only one technical decision is puzzling. The algorithm differs from the Encrypted Token Pattern described in OWASP wiki.

They recommend:

  1. Build a three-element tuple of user id (which we need to skip as the user is not necessarily authenticated), a random nonce, and a timestamp.
  2. Encrypt the tuple with symmetric cipher, using a server's secret key.
  3. The encryption output is a CSRF token.

We do:

  1. Build a one-element tuple containing the timestamp.
  2. Encrypt the tuple with symmetric cipher, using a key derived from server's secret key, and a random "nonce" (which is a different concept than in OWASP's wiki, and which is called a tweak in FFX terminology).
  3. The CSRF token is a tuple consisting of the encryption output, and that "nonce".

I don't know what are security implications, I believe both approaches are good enough. But I wonder why we do it different?

cc @ribose-jeffreylau

ronaldtse commented 6 years ago

The OWASP "Encrypted Token Pattern" is for a user activities "post-login" (authentication of a valid user session), where the user already has state stored at the server:

The Encrypted Token Pattern leverages an encryption, rather than comparison, method of Token-validation. After successful authentication, the server generates a unique Token comprised of the user's ID, a timestamp value and a nonce, using a unique key available only on the server.

In our algorithm, the steps, more accurately, are listed below (there is no state stored at the server):

  1. Client generates a random nonce and sends to server.

  2. Server uses the server's time in form of a timestamp and performs token = E_t(client_nonce), and returns token to the client. The encryption function E is itself a time-sensitive function (TOTP generator), for example, E_t(x) = E(server_timestamp + 10 seconds, x).

  3. When the Client submits the tuple <token, client_nonce> to the Server, the Server is able to determine validity of the token.

skalee commented 6 years ago

I believe neither approach actually requires storing anything on a server.

So, the intention is to generate a nonce client side? Consider scenario in which user types in "…/login" into browser's address bar. A page is rendered, and this page has a login form which should have a valid CSRF token included in the form's hidden input field. That means that nonce should be known to server before anything gets executed client-side.

Alternatively, a CSRF token could be obtained with an AJAX request. This solution is less generic, as it requires JavaScript, but definitely doable. On the other hand, it allows for using tokens which expire within seconds, as there is no trouble in updating them very frequently. Is it the desired use case?

ronaldtse commented 6 years ago

Yes, the CSRF token is to be fetched via an AJAX request. The intention is exactly as you described -- a token fetch immediately prior to the using of the token so we can keep the token validity very short 👍

skalee commented 6 years ago

Thanks!

One final thing — I understand that JavaScript client need to be done as well. Should it auto-refresh the token in some time intervals, or should it fetch it on form submit event? The former allows to submit form immediately, therefore is probably user-friendlier, but the latter reduces server load in an obvious way.

skalee commented 6 years ago

Nonce should come from a generator which is suitable for cryptography. I want to use getRandomValues() from the Web Cryptography API. Is it okay to support only browsers which are listed here? Or should I look for a different tool?

skalee commented 6 years ago

Wait a sec… what's the point of CSRF protection for unauthenticated users?

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated. CSRF attacks specifically target state-changing requests, not theft of data, since the attacker has no way to see the response to the forged request. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker's choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application.

(https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF))

As you wrote that:

Yes, correct. 👍 And indeed -- all unauthenticated form submissions should also be protected. Thanks!

skalee commented 6 years ago

And if I understand correctly, you are going to authenticate requests with tokens, not cookies. Is CSRF any threat to you, actually? How a request could be forged unless this token is added by browser behind scenes, like if you decide to store the token in a cookie?

CSRF is an attack that tricks the victim into submitting a malicious request. It inherits the identity and privileges of the victim to perform an undesired function on the victim's behalf. For most sites, browser requests automatically include any credentials associated with the site, such as the user's session cookie, IP address, Windows domain credentials, and so forth. Therefore, if the user is currently authenticated to the site, the site will have no way to distinguish between the forged request sent by the victim and a legitimate request sent by the victim.

(https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF))

ronaldtse commented 6 years ago

A couple posts come up on Google:

For example, the "login" and "feedback" forms are unauthenticated but still important.

Yes we authenticate requests with tokens (via JS), but there are also secrets stored in the cookie. You're right that if we do all these things the better protection we have.

On the other hand, @ribose-jeffreylau will let you know that we need extra help with hacking the token generation/verification methods of devise_token_auth, basically bringing it inline with what this gem does.

cc: @erikbor any further thoughts for @skalee here?

skalee commented 6 years ago

Hmm, okay, I see your point. I agree that pre-login CSRF protection is a good idea.

ronaldtse commented 6 years ago

That's great . Just a heads up that the reason why @ribose-jeffreylau will be asking you with fixing up the token methods in devise_token_auth is due to a flaw discovered in its token scheme. Just as you asked long ago, we should just use 1 set of tokens (i.e. this gem).

The only change between the unauthenticated mode vs authenticated mode, is the parameters given to token generation.

  1. The unauthenticated token in this gem is generated via token = E_t(client_nonce)
  2. The authenticated token should be generated via token = E_t(client_nonce, client_server_shared_per_session_secret). @ribose-jeffreylau will confirm you what exactly is client_server_shared_per_session_secret`.

Then it's already good to go. Thanks!

skalee commented 6 years ago

Consider following scenario:

  1. Victim visits some unsafe site which allows for XSS.
  2. A malicious script generates a random nonce.
  3. A malicious script makes an AJAX or PJAX request to some remote server controlled by attacker. AJAX requests are possible because responses from attacker's server have liberal CORS headers.
  4. Remote server is not a browser, therefore can make arbitrary request. In this case, it obtains a valid CSRF-protection token for given nonce.
  5. A valid token is returned to the malicious script.
  6. A request is made on behalf of his victim. The request easily bypasses CSRF protection, because the token is valid for any user.

I think that this is the reason why the Encrypted Token Pattern on OWASP was mentioning user_id. Token must be issued for specific user, basing on some information stored in a cookie, otherwise such protection can be bypassed. IMO it doesn't actually matter if user_id matches user's identifier in the database. It can be a random number, as long as it identifies user (authenticated or anonymous), and is stored in a cookie. I think that session identifier will work as well.

ronaldtse commented 6 years ago

@skalee for your reference @ribose-jeffreylau and @PeterTKY are standardizing the scheme here: https://github.com/riboseinc/ros-encrypted-tokens. I've also pushed Jeffrey's changes that are being used internally.