Rails: Devise Authentication from an ActiveResource call

后端 未结 1 432
执念已碎
执念已碎 2021-02-06 19:05

My two rails applications(app1, app2) are communicating using active resource.

app1 calls app2 create a user inside app2. app2 would create the user and would like app1

相关标签:
1条回答
  • 2021-02-06 19:37

    You are trying to implement a form of Single Sign-On service (SSO) (sign in with app1, and be automatically authenticated with app2, app3...). It is unfortunately not a trivial task. You can probably make it work (maybe you already did), but instead of trying to reinvent the wheel, why not instead integrate an existing solution? Or even better, a standard protocol? It is actually relatively easy.

    CAS server

    RubyCAS is a Ruby server that implements Yale University's CAS (Central Authentication Service) protocol. I had great success with it.

    The tricky part is getting it to work with your existing Devise authentication database. We faced the same problem, and after some code diving, I came up with the following, which works like a charm for us. This goes in your RubyCAS server config, by default /etc/rubycas-server/config.yml. Of course, adapt as necessary:

    authenticator:
      class: CASServer::Authenticators::SQLEncrypted
      database:
        adapter: sqlite3
        database: /path/to/your/devise/production.sqlite3
      user_table: users
      username_column: email
      password_column: encrypted_password
      encrypt_function: 'require "bcrypt"; user.encrypted_password == ::BCrypt::Engine.hash_secret("#{@password}", ::BCrypt::Password.new(user.encrypted_password).salt)'
    
    enter code here
    

    That encrypt_function was pain to figure out... I am not too happy about embedding a require statement in there, but hey, it works. Any improvement would be welcome though.

    CAS client(s)

    For the client side (module that you will want to integrate into app2, app3...), a Rails plugin is provided by the RubyCAS-client gem.

    You will need an initializer rubycas_client.rb, something like:

    require 'casclient'
    require 'casclient/frameworks/rails/filter'
    
    CASClient::Frameworks::Rails::Filter.configure(
      :cas_base_url  => "https://cas.example.com/"
    )
    

    Finally, you can re-wire a few Devise calls to use CAS so your current code will work almost as-is:

    # Mandatory authentication
    def authenticate_user!
      CASClient::Frameworks::Rails::Filter.filter(self)
    end
    
    # Optional authentication (not in Devise)
    def authenticate_user
      CASClient::Frameworks::Rails::GatewayFilter
    end
    
    def user_signed_in?
      session[:cas_user].present?
    end
    

    Unfortunately there is no direct way to replace current_user, but you can try the suggestions below:

    current_user with direct DB access

    If your client apps have access to the backend users database, you could load the user data from there:

    def current_user
      return nil if session[:cas_user].nil?
      return User.find_by_email(session[:cas_user])
    end
    

    But for a more extensible architecture, you may want to keep the apps separate from the backend. For the, you can try the following two methods.

    current_user using CAS extra_attributes

    Use the extra_attributes provided by the CAS protocol: basically, pass all the necessary user data as extra_attributes in the CAS token (add an extra_attributes key, listing the needed attributes, to your authenticator in config.yml), and rebuild a virtual user on the client side. The code would look something like this:

    def current_user
      return nil if session[:cas_user].nil?
      email = session[:cas_user]
      extra_attributes = session[:cas_extra_attributes]
      user = VirtualUser.new(:email => email,
        :name => extra_attributes[:name],
        :mojo => extra_attributes[:mojo],
      )
      return user
    end
    

    The VirtualUser class definition is left as an exercise. Hint: using a tableless ActiveRecord (see Railscast #193) should let you write a drop-in replacement that should just work as-is with your existing code.

    current_user using an XML API on the backend and an ActiveResource

    Another possibility is to prepare an XML API on the users backend, then use an ActiveResource to retrieve your User model. In that case, assuming your XML API accepts an email parameter to filter the users list, the code would look like:

    def current_user
      return nil if session[:cas_user].nil?
      email = session[:cas_user]
      # Here User is an ActiveResource
      return User.all(:params => {:email => email}).first
    end
    

    While this method requires an extra request, we found it to be the most flexible. Just be sure to secure your XML API or you may be opening a gapping security hole in your system. SSL, HTTP authentication, and since it is for internal use only, throw in IP restrictions for good measure.

    Bonus: other frameworks can join the fun too!

    Since CAS is a standard protocol, you get the added benefit of allowing apps using other technologies to use your Single Sign-On service. There are official clients for Java, PHP, .Net and Apache.

    Let me know if this was of any help, and don't hesitate to ask if you have any question.

    0 讨论(0)
提交回复
热议问题