thanikkal
thanikkal

Reputation: 3356

Rails: Devise Authentication from an ActiveResource call

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 then redirect the user to app2's authenticated pages.

going from app1 to app2 would invariably ask the user to log in.

I was looking for a way to avoid this login step in app2, instead make the user log in during the first active resource call to create user, and somehow get the authentication token written.

Authentication is done using Devise. Is there anything built into Devise that support this?

Is passing around the authentication token the way to go?

Upvotes: 2

Views: 1854

Answers (1)

yemartin
yemartin

Reputation: 73

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.

Upvotes: 7

Related Questions