How can I share user sessions across multiple domains using Rails? - ruby-on-rails

Is anyone aware of any gems, tutorials, or solutions enabling a user to sign in to a website at one domain and automatically given access to other partner domains in the same session?
I have two rails apps running, let's call them App-A and App-B. App-A has a database associated with it, powering the registration and login at App-A.com. I'd now like to give all of those users with App-A.com accounts access to App-B.com, without making them reregister or manually login to App-B.com separately.
Thanks in advance for any help!
--Mark

You can set the same session_key in both apps. In appA environment.rb change the session_key, like this
Rails::Initializer.run do |config|
...
config.action_controller.session = {
:session_key => '_portal_session',
:secret => '72bf006c18d459acf51836d2aea01e0afd0388f860fe4b07a9a57dedd25c631749ba9b65083a85af38bd539cc810e81f559e76d6426c5e77b6064f42e14f7415'
}
...
end
Do the same in AppB. (remember to use the very same secret)
Now you have shared sessions. Let's say you use restfull_authentication, wich sets a session variable called user_id. When you authenticate in appA it sets the user_id in the session. Now, in appB you just have to verify if user_id exists in the session.
This is the overall schema, you can elaborate more using this idea.

If you want to create single sign-on solution for your applications then I recommend to take a look at RubyCAS solution. It could be used also to provide single sign-on for other non-Rails applications as well as you can integrate authentication with LDAP or other authentication providers.

Related

Rails & Devise - Autologin Across Subdomains

Setup
I have a Rails application where users register for an account, and a subdomain is created for them. They can then proceed to the subdomain and log in with their credentials. The workflow looks something like this:
User visits base domain fills out a form that with email/username/password and subdomain fields
From the submitted info, the server creates an account in the global/public database. Server then creates a database that will be specific to that particular subdomain/account, and stores the user record in it.
User is redirected to their subdomain, and asked to log in.
(note: to implement the separate "databases", I'm using postgres schemas, but that should be irrelevant.)
The question
My question involves step 3. I would like to redirect the user to their subdomain and log them in automatically instead of asking them to log in. However, I do not want to share a single session across all of the subdomains.
I would like to somehow securely transmit auto login request.
Possible Solution
I have considered using a single-use, random token that I would store in a cookie and in the users table. After the user successfully creates an account, he would be redirected to the subdomain. At that point the token would be consumed/destroyed and the user would be automatically logged in.
I would also need to have a short window for the token to be used before expiring.
Thoughts? Thanks!
I had the same issue, the possible solution you suggest does not work because the session is not shared between subdomains.
I solved it the following way (same idea you propossed, different implementation):
Create a new model (I called it LoginKey) that contains the user_id and a random SHA1 key.
When the user is authenticated at the parent domain (for example: mydomain.com/users/sign_in), a new LoginKey is created and the user is redirected to the corresponding subdomain to an action that I called login_with_key (for example: user_subdomain.mydomain.com/users/login_with_key?key=f6bb001ca50709efb22ba9b897d928086cb5d755322a3278f69be4d4daf54bbb)
Automatically log the user in with the key provided:
key = LoginKey.find_by_login_key(params[:key])
sign_in(key.user) unless key.nil?
Destroy the key:
key.destroy
I didn't like this solution 100%, I tried out a lot of different approaches that do not require a db record to be created, but always faced security concerns, and I think this one is safe.

Sharing session across rails apps on different subdomains

I am trying to implement a single-sign-on solution for multiple rails (v3.2) apps hosted at different subdomains of example.com
One app serves as an identity provider, uses devise for auth, and sits at users.example.com
The other apps rely on the identity provider for authentication, use devise+omniauth, with domains of [app1.example.com, app2.example.com, and example.com].
This blog entry inspired much of my implementation: http://blog.joshsoftware.com/2010/12/16/multiple-applications-with-devise-omniauth-and-single-sign-on/
I have it working fine, but the problem remains that the sessions are not shared so after I log in on the identity provider, I still have to make a call from each of the other apps to authenticate and I need this to be seemless to the user.
I tried using the same secret token at secret_token.rb, same session key at session_store.rb and :domain => :all (also tried '.example.com' and 'example.com' as values). Still no luck.
Doing the above, I see in a session.inspect that after login on the identity provider the session variable "warden.user.user.key" is populated. When I immediately go to the app on app1.example.com, the session.inspect shows the same session_id and _csrf_token but the "warden.user.user.key" variable is now missing.
I feel like I am missing something silly.. Any ideas what that may be?
I think there is another SO question about getting a single cookie to work across subdomains that would answer yours:
https://stackoverflow.com/a/10403338/2573896
Also, I can imagine that using a memcached cluster with dalli and memcached as your session store would work as well:
http://awesomerails.wordpress.com/2011/08/23/rails-3-memcached-session-store/
For the purpose of your application, the first solution makes more sense though.

Two different authorization schemes in a Rails 3.1 web service

I'm creating a web service with Rails 3.1 that requires authenticated user accounts for creating/managing content. It also requires an authorization scheme for transient 'users' accessing the content - they do not have accounts, but will simply provide a password furnished to them by the user who created the content in their requests.
I'm thinking the best strategy is to keep the two separate, not creating accounts for the transient users, representing them as a separate model associated with the content.
My question is whether this is something I should build from scratch, or whether I can get sufficient leverage from one of the existing authentication gems for it. And if the latter, how I would go about configuring it to manage two different strategies.
If understand right, you will have regular account users and temporary account generated by users to share access to whatever.
I don't think something for this specific purpose exist.
My think using a solid and confortable Auth Manager gem will be require to secure both user and tmp_account access.
The reste, ie managing user-tmp_account relation and managing life time + right of the tmp_account, could be done without pain manually.
I personally build up something similar with the gem Devise.
Turns out I don't really need an authentication gem. While the implementation isn't finished, it appears a combination of Rails 3.1's has_secure_password and CanCan will work well for this.
Here's Ryan Bate's tutorial for using has_secure_password: http://asciicasts.com/episodes/270-authentication-in-rails-3-1
The idea is to use has_secure_password on both the User and Content models, and implement current_user such that it creates a transient User when the password is provided, setting a password property on that transient user.Then the implementation of the init method in CanCan's Ability class will verify the transient user's password against the content in a can block.

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?
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.

Rails - Multiple top level domains and a single session/cookie

I've been struggling with this for quite awhile and haven't been able to
find a solution. I need a user to be able to view multiple top level
domains with a single login.
My understanding is that this needs to be set in environment.rb and
called with before_dispatch. This is what I've come up with:
require 'activesupport'
require 'dispatcher'
module ActionController
class Dispatcher
def set_session_domain
ActionController::Base.session_options.update :session_domain => "#{#request.host}"
end
before_dispatch :set_session_domain
end
end
However, this does not seem to be working when I try and pull the values
from session[:session_domain].
Any help is greatly appreciated.
This one is a bit tricky. Since cookies can only be assigned to (and retrieved from) the current domain ("forms.example.com", say) and parent domains (".example.com", but not ".com"), but NOT to other domains ("othersite.com"), you'll have to find yourself another solution. This has nothing to do with Rails, but with how cookies work.
EDIT: Sessions rely on a client-specific handle, stored in a cookie, which is why sessions also don't work cross-domain.
This site has one possible solution for creating a cross-domain cookie, and it's the cleanest way I know of, although it may have some security implications. A more complicated version would have the servers communicate directly through some secure channel.
If you're looking for a more general-purpose single-login service, try implementing some form of OpenID.
For sub-domains in Rails 2.3
ActionController::Base.session = { :domain => ".mydomain.com" }
For top-level domains try this middleware.
I've been playing with the above middleware at the moment and it does not quite work as expected. If you do use the middleware you do not need the above code as it handles sub-domains as well.
You will probably need something like RubyCAS if you want authentication across domains regardless of whether they're top-level or subdomains.
Your question is not really precise enough IMHO. Do you want a single cookie for all Rails apps you have or is it within the context of a single one? If the former, you want to look at solutions using database-backed sessions or something along the line of RubyCAS to implement the CAS protocol.
Both Keltia and zuk are right, Answer is rubyCAS, We have do that integration and it allows
SSI - Single sign -in
You sign to one site and you are automatically signed to the other
SSO - Single Sign Out
You sign out from one site and automatically you signed out from the other
For us this is a proven solution and not a hard one to implement
we are using it in
http://www.cabslk.com and www.ticketslk.com
cheers,
Sameera

Resources