janko / rodauth-rails

Rails integration for Rodauth authentication framework
https://github.com/jeremyevans/rodauth
MIT License
571 stars 40 forks source link

undefined method flash= #66

Closed andyrue closed 2 years ago

andyrue commented 2 years ago

I'm trying to use rodauth-rails in a rails api app. I've noticed a couple things about generating the rodauth install using the --json flag.

If I specify the generator to use --json it still enables :jwt in the rodauth_app file and includes a jwt_secret. I then get errors because it's expecting the jwt gem to be installed, which I'm not needing. The documentation makes it sound like I should only be getting all the jwt configurations if I specified --jwt with the generator.

Once I remove the jwt_secret and the enable :jwt, I stop getting jwt errors and start getting undefined method flash=' for #<ActionDispatch::Request:0x00007fabaea1fd90> when I try to post to /login. Since this is an api, I don't have a flash, and from reading through previous issues it sounds like this was fixed in version 0.4. I'm on 0.17.

This is on a fresh rails 6.1.4.1 api app. The only changes I have made are adding rack-cors, enabling cookies and session cookies in application.rb, and adding include ActionController::RequestForgeryProtection with protect_from_forgery with: :null_session in the application controller.

On a side note, how can I inspect a request being sent to a rodauth route? I can't find a place to put a byebug call. My client is sending an X-CSRF-Token header with a good value, but I'm still getting Can't verify CSRF token authenticity. so I'm needing to troubleshoot that further, but that's not your problem. :-)

Thank you!

janko commented 2 years ago

If I specify the generator to use --json it still enables :jwt in the rodauth_app file and includes a jwt_secret. I then get errors because it's expecting the jwt gem to be installed, which I'm not needing. The documentation makes it sound like I should only be getting all the jwt configurations if I specified --jwt with the generator.

I agree the behaviour should be as you described. The JWT configuration is the default for API-only Rails apps, given that this Rails configuration doesn't include the cookie session middleware by default AFAIK (so cookie-based session authentication wouldn't work). However, specifying --json should override it.

Once I remove the jwt_secret and the enable :jwt, I stop getting jwt errors and start getting undefined method flash=' for # when I try to post to /login. Since this is an api, I don't have a flash, and from reading through previous issues it sounds like this was fixed in version 0.4. I'm on 0.17.

When you removed :jwt, have you added back :json to the list of enabled Rodauth features? That's what provides JSON API support to Rodauth, doing this such as overriding Rodauth methods that deal with flash messages to instead add those messages in the JSON response. Also make sure to set only_json? to true.

On a side note, how can I inspect a request being sent to a rodauth route? I can't find a place to put a byebug call. My client is sending an X-CSRF-Token header with a good value, but I'm still getting Can't verify CSRF token authenticity. so I'm needing to troubleshoot that further, but that's not your problem. :-)

The route block defined in RodauthApp is executed before each request (RodauthApp is called as middleware), so you can inspect the request there. The Rack::Request object is accessible via the request method.

Thanks for the general feedback, I will try to improve documentation around JSON API support.

andyrue commented 2 years ago

Thank you for your reply. :json was also included during the original generation along with :jwt so I didn't need to add it in. only_json? true was also added with the generation correctly.

This is the top portion of my rodauth_app.rb file. I have also tried passing json: true as a parameter into configure() which I think I saw referenced in a past issue, but didn't make a difference.

class RodauthApp < Rodauth::Rails::App
  configure() do
    # List of authentication features that are loaded. 
    enable :create_account, :verify_account, :verify_account_grace_period,
      :login, :logout, :json,  <------- Removed :jwt from here.
      :reset_password, :change_password, :change_password_notify,
      :change_login, :verify_login_change,
      :close_account

    # See the Rodauth documentation for the list of available config options:
    # http://rodauth.jeremyevans.net/documentation.html

    # ==> General
    # The secret key used for hashing public-facing tokens for various features.
    # Defaults to Rails `secret_key_base`, but you can use your own secret key.
    # hmac_secret "blahblahblah"

    # Set JWT secret, which is used to cryptographically protect the token.
    # jwt_secret "blahblah" <----- Commented Out

    # Accept only JSON requests.
    only_json? true

Thank you for the route block tip. That worked great.

P.S. Thanks for your work on this and also Shrine which is a great Gem!

janko commented 2 years ago

Regarding the undefined method flash= error, it's likely happening because rodauth-rails loaded the Rails flash integration into the Rodauth app, but ActionDispatch::Flash isn't loaded. rodauth-rails skips loading the flash integration when the Rails app is API-only: https://github.com/janko/rodauth-rails/blob/7ea8058081f1ac1db1731800534ee717a8bbed4a/lib/rodauth/rails/app.rb#L15-L18

Rails doesn't load ActionDispatch::Flash only when the app is API-only (code), so I don't know how it happened that rodauth-rails detected that your app wasn't API-only, but ActionDispatch::Flash wasn't loaded 🤷🏻‍♂️

andyrue commented 2 years ago

It's strange. Everything related to Flash in the rodauth_app file is commented out. Is there another place I should be checking? It seems to be coming from the r.rodauth line in the routes block. That wasn't clear before, but it is now.

NoMethodError (undefined method `flash=' for #<ActionDispatch::Request:0x00007fd61a1059d0>):
app/lib/rodauth_app.rb:132:in `block in <class:RodauthApp>'

Line 132 is the r.rodauth inside the route block.

I did discover that if I remove protect_from_forgery from my application_controller.rb I no longer get the flash error, but instead I get:

NoMethodError (undefined method `new' for nil:NilClass):
  app/lib/rodauth_app.rb:132:in `block in <class:RodauthApp>'

I created another fresh app and installed rodauth-rails from master. The --json update worked as expected this time. Thanks for fixing that!

janko commented 2 years ago

It's strange. Everything related to Flash in the rodauth_app file is commented out. Is there another place I should be checking?

The issue is that rodauth-rails activated flash integration when it seems like it shouldn't have. The error is most likely not coming from Rodauth but from rodauth-rails.

It seems to be coming from the r.rodauth line in the routes block. That wasn't clear before, but it is now.

The r.rodauth call routes and processes Rodauth requests, so most Rodauth exceptions will originate from there. You likely have some backtrace filter that doesn't display the backtrace from gems, so that's why it appears as the first line.

I did discover that if I remove protect_from_forgery from my application_controller.rb I no longer get the flash error, but instead I get:

I'm missing a full backtrace for this.

I created another fresh app and installed rodauth-rails from master.

Would you mind sharing it? Being able to look at a Rails app that exhibits this error would guarantee me fixing it 😃

andyrue commented 2 years ago

Of course, I'm sorry I didn't do this sooner. https://github.com/andyrue/rodauth-test

After starting the server, do a Post request to http://localhost:3000/login and you should see the error in the console.

Thank you for looking!

Oh, and the state that it's in will give you the undefined method flash= error. If you comment out protect_from_forgery in the rodauth_controller.rb file, it will give you undefined method 'new' for nil:NilClass.

janko commented 2 years ago

Thanks for sharing, I figured out what was the issue.

Firstly, when both ActionController::RequestFromForgery was included and protect_from_forgery was registered, the undefined method flash= error you're seeing is coming from ActionController::RequestFromForgery, whose :null_session strategy relies on flash being loaded. This is not related to rodauth-rails, you get the same exception when making a request to a regular Rails controller, even when rodauth-rails is not loaded at all. One solution for this is adding ActionDispatch::Flash to your middleware stack.

Secondly, rodauth-rails wasn't currently handling the scenario when ActionController::RequestForgeryProtection was included by protect_from_forgery wasn't registered (that's the undefined method 'new' for nil:NilClass error). I will fix this in the upcoming commit.

Thirdly, rodauth-rails wasn't handling ActionController::API without ActionController::RequsetForgeryProtection combined with the JSON Rodauth feature. It's not secure to have session-based authentication without CSRF protection, but rodauth-rails shouldn't be the one complaining about it. So, I will also update rodauth-rails to skip checking CSRF when the rails controller doesn't have it loaded.

andyrue commented 2 years ago

Thank you! This makes so much sense and is much clearer now.