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

inconsistencies for rodauth.authenticated_by with AJAX call when using JWT plugin #101

Closed
nicolas-besnard opened this issue Jun 11, 2020 · 2 comments

Comments

@nicolas-besnard
Copy link

nicolas-besnard commented Jun 11, 2020

Following this issue (janko/rodauth-rails#8), I discovered an issue when doing AJAX call when using the JWT plugin.

Thanks to @janko, I was able to reproduce the issue:

require "roda"
require "sequel"
require "capybara"
require "webdrivers/chromedriver"
require "bcrypt"
require "securerandom"
require "byebug"

DB = Sequel.sqlite
DB.extension :date_arithmetic
DB.create_table :accounts do
  primary_key :id
  String :email, null: false
  String :password_hash, null: false
end
DB.create_table :account_remember_keys do
  foreign_key :id, :accounts, primary_key: true
  String :key, null: false
  DateTime :deadline, null: false, default: Sequel.date_add(Sequel::CURRENT_TIMESTAMP, days: 14)
end
DB.create_table :account_otp_keys do
  foreign_key :id, :accounts, primary_key: true
  String :key, null: false
  Integer :num_failures, null: false, default: 0
  Time :last_use, null: false, default: Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, seconds: 600)
end

class App < Roda
  plugin :sessions, secret: SecureRandom.hex(32)
  plugin :render, layout: false

  plugin :rodauth, json: true do
    enable :login, :remember, :otp, :jwt

    jwt_secret SecureRandom.hex(32)
    account_password_hash_column :password_hash
    after_login { remember_login }
    after_load_memory { two_factor_update_session("totp") if two_factor_authentication_setup? }
  end

  route do |r|
    rodauth.load_memory
    r.rodauth

    if rodauth.logged_in?
      puts '  -- rodauth.authenticated_by'
      puts rodauth.authenticated_by.inspect
    end

    rodauth.require_authentication

    r.on "posts" do
      r.is do
        r.get do
          puts '  ---- IN /POSTS -- rodauth.authenticated_by'
          puts rodauth.authenticated_by.inspect
          rodauth.authenticated_by
        end
      end
    end

    r.root do
      <<-HTML
        <button class="ajax">CLICK</button>
        <div class="result"></div>
        <script>
          const button = document.querySelector('.ajax')
          button.addEventListener('click', () => {
            fetch('/posts', {
              credentials: 'include',
              headers: {
                'Content-Type': 'application/json'
              }
            })
              .then(data => data.text())
              .then(data => {
                const result = document.querySelector('.result')
                result.innerHTML = data
              })
          })
        </script>
      HTML
    end
  end
end

account_id = DB[:accounts].insert(email: "foo@bar.com", password_hash: BCrypt::Password.create("secret"))

totp = ROTP::TOTP.new(ROTP::Base32.random_base32.downcase)

DB[:account_otp_keys].insert(id: account_id, key: totp.secret)

Capybara.register_driver :chrome do |app|
  capabilities = Selenium::WebDriver::Remote::Capabilities.chrome("goog:chromeOptions": {
    args: %W[window-size=1680,1050]
  })
  driver_options = {
    browser: :chrome,
    clear_local_storage: true,
    desired_capabilities: capabilities
  }
  Capybara::Selenium::Driver.new(app, driver_options)
end

session = Capybara::Session.new(:chrome, App)

session.visit "/login"
session.fill_in "Login", with: "foo@bar.com"
session.fill_in "Password", with: "secret"
session.click_on "Login"

session.fill_in "Authentication Code", with: totp.now
session.click_on "Authenticate Using TOTP"

session.visit "/remember"
session.choose "Remember Me"
session.click_on "Change Remember Setting"

session.visit "/"
session.click_on "CLICK"

sleep 0.2

puts ' -- AJAX Response'
puts session.find('.result').text

The base.rb file define a session method that use the scope of the app

On the other hand, jwt.rb redefines this method in case of a JSON request (with Content-Type: application/json header). Maybe this method should call super in case it's a JSON request but their no JWT token? I tried that locally and it's working.

I'm not sure why the JWT plugin returns an empty hash instead of using super?

@jeremyevans
Copy link
Owner

If you only want to use the JSON API support if you have a JWT submitted, you could try this in your rodauth config:

  use_jwt? do
    jwt_token
  end

If you do this, when you want to use the JSON API, you'll have to submit a bogus Authorization header when first creating the session (e.g. when logging in). Otherwise, Rodauth will not operate in JSON API mode, it will use the normal session (generally stored in a encrypted cookie) instead of a JWT session.

Can you try the above and see if it fixes your issue?

Note that Rodauth does not support mixing the JSON API with normal HTML form use in the same session. There is nothing in Rodauth that will convert the normal session into a JWT session or vice-versa. I don't think that's an issue in your code, just something to be aware of.

@nicolas-besnard
Copy link
Author

Thanks @jeremyevans, you solution is working!

Instead of using a bogus Authorization header for login, I ended up doing:

use_jwt? do
  jwt_token || (json_request? && request.path == '/login')
end

I'll probably need to do the same for others routes.

I totally understand that Rodauth is not supporting mixing JWT and normal HTML, as it doesn't seem to make sense to have this supported :)

Thank again for your help and your amazing work on this gem!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants