anycable / anycable-rails

AnyCable for Ruby on Rails applications
https://anycable.io
MIT License
495 stars 35 forks source link

Cannot call handle_open to handle JWT expiration #174

Closed fs-ses-junya-wako closed 2 years ago

fs-ses-junya-wako commented 2 years ago

Tell us about your environment

Ruby version: 2.7.2

Rails version: 6.0.3

anycable gem version: 1.2.3

anycable-rails gem version: 1.3.4

grpc gem version: 1.47.0

anycable-go version: 1.2.1

What did you do?

I try to refresh my authentication tokens (JWT) by using following tokenRefresher:

anycable/anycable-client: AnyCable / Action Cable JavaScript client for web, Node.js & React Native

And try to respond token_expired message from server side in case of auth error:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def handle_open
      super
    rescue VerificationFailed
      logger.error 'An expired JWT token was rejected'
      close(reason: 'token_expired', reconnect: false) if websocket.alive?
    end
    ...
end

anycable/anycable-rails-jwt: JWT identification helpers for AnyCable

What did you expect to happen?

I expect handle_open method is called when the websocket connection establishes. and to catch VerificationFailed and respond token_expired message if token auth fails.

What actually happened?

But handle_open method was not called.

I changed my code the following, it is OK to catch VerificationFailed and respond token_expired message. (but if I use websocket.alive? in connect , undefined methodalive?' for #` occurs.)

    def connect
      self.current_user = find_verified_user
    rescue VerificationFailed
      logger.error 'An expired JWT token was rejected'
      close(reason: 'token_expired', reconnect: false)
    end
palkan commented 2 years ago

I expect handle_open method is called when the websocket connection establishes.

The #handle_open is only called when you're connecting via Action Cable server.

If you're connecting via anycable-go, you should enable JWT identification (e.g., anycable-go --jwt_id_key=<some key>). In this case no RPC is performed at all, anycable-go authenticate the connection on its end.

If you're not using AnyCable JWT feature, and using a custom token-based auth instead, you should use the #connect method, right. The only change required is to add reject_unauthorized_connection after the #close call:

def connect
  self.current_user = find_verified_user
rescue VerificationFailed
  logger.error 'An expired JWT token was rejected'
  close(reason: 'token_expired', reconnect: false) if anycabled? || websocket.alive?
  reject_unauthorized_connection
end

Note that I added if anycabled? || websocket.alive?. This is to make this code backward-compatible with Action Cable server in case you use it in other environments (and don't want it to raise exception when tryin to close non-alive socket).

fs-ses-junya-wako commented 2 years ago

If you're connecting via anycable-go, you should enable JWT identification

Oh, I didn't know that.

If you're not using AnyCable JWT feature, and using a custom token-based auth instead, you should use the #connect method, right.

In our case, we're using a custom token-based auth. It worked perfectly with your code!

I could understand very clearly. Thanks @palkan !