The best way to scale an application is to split the application business logic into different inter-communicable components. However, authenticating, authorizing and security raise concerns. OAuth comes to the rescue – and like a knight in shining armour – omniauth steals the show.
Omniauth is an awesome gem that allows you to authenticate using Open-Id based social networks. There are TONS of topics on this – the one I liked best was from RailsRumble. Devise was also integrating oauth and oauth2 into its authentication framwork when omniauth was released (in Oct, 2010). So, Devise dumped their oauth integration in their v1.2oauth branch and started integrating master with omniauth instead (:omniauthable Devise Module).
We wanted to solve these problems:
- A single User Manager application (which will authenticate ALL users with different roles)
- Different internal applications which talk to User Manager for authentication
- User should be able to login/sign-up via Social Networks like Twitter and Facebook.
- Single Sign On between all applications.
I found this wonderful post about how to implement an oauth provider in devise by Chad Fowler & Albert Yi. This does not use omniauth, though its almost there. So, I decided to merge these two approaches and what do you know — it works!
Our current setup:
- User Manager: devise + omniauth
- App1: omniauth + custom gem for omniauth custom strategy
- App2: omniauth + custom gem for omniauth custom strategy
I followed the User Manager setup must like the blog mentioned above for implementing an oauth provider. To start with, I created a new strategy for omniauth that would be invoked from App1 and App2. We added a custom gem for easy deployment — but the essence of the code is here:
require 'omniauth/oauth' require 'multi_json' module OmniAuth module Strategies class MyStrategy < OAuth2 def initialize(app, api_key = nil, secret_key = nil, options = {}, &block) client_options = { :site => 'http://myserver.local', :authorize_url => "http://myserver.local/auth/my_strategy/authorize", :access_token_url => "http://myserver.local/auth/my_strategy/access_token" } super(app, :my_strategy, api_key, secret_key, client_options, &block) end protected def user_data @data ||= MultiJson.decode(@access_token.get("/auth/my_strategy/user.json")) end def request_phase options[:scope] ||= "read" super end def user_hash user_data end def auth_hash OmniAuth::Utils.deep_merge(super, { 'uid' => user_data["uid"], 'user_info' => user_data['user_info'], 'extra' => { 'admin' => user_data['extra']['admin'], 'first_name' => user_data['extra']['first_name'], 'last_name' => user_data['extra']['last_name'], } }) end end end end
Now, in my config/initializer/omniauth.rb I can add
Rails.application.config.middleware.use OmniAuth::Builder do provider :my_strategy, APP_ID, APP_SECRET end
So, now we have the ‘OAuth Consumers’ or ‘OAuth Client’ (whichever you prefer to call it) for App1 and App2. The next step was to create the ‘Oauth Provider’ or ‘OAuth Server’. This we call the User Manager. You can follow instructions in the blog post mentioned above. I have skipped the details for the sake of brevity. In brief:
- Create the standard devise User model and migration.
- Create the Auth Controller actions (as show in the code snippet below)
- Create the AccessGrant model (and if required the Authentication model)
- Register the client applications (key and secret) via rails console on User Manager.
class AuthController < ApplicationController before_filter :authenticate_user!, :except => [:access_token] skip_before_filter :verify_authenticity_token, :only => [:access_token] def authorize # Find the Application using params[:client_id] # redirect to params[:redirect_uri] end def access_token # Check the params[:client_secret] # return Access Token end def user # Create the hash # return json hash end
Note: we are using a few of Devise before filters to authenticate the users before authorizing any application!
My User model had these Devise modules loaded:
class User < ActiveRecord::Base devise :database_authenticatable, :registerable, :token_authenticatable, :recoverable, :timeoutable, :trackable, :validatable
Now, the trick was to configure the User Manager with the standard omniauth providers. Here is the configuration:
Rails.application.config.middleware.use OmniAuth::Builder do provider :twitter, ApplicationConfig['TWITTER_APP_ID'], ApplicationConfig['TWITTER_APP_SECRET'] provider :facebook, ApplicationConfig['FACEBOOK_APP_ID'], ApplicationConfig['FACEBOOK_APP_SECRET'] end
Now, when a user logs in or signs up from App1:
- the request is redirected to the UserManager via omniauth route ‘/auth/my_strategy’.
- The User Manager signup can further redirect it to Twitter or Facebook using ‘/auth/twitter’ or ‘/auth/facebook’ and get the user to login / signup.
- The request is redirected back to App1 via User Manager oauth provider callback uri.
This takes care of a single authentication system for the entire environment. Now we need to handle single sign on.
DO WE REALLY NEED TO? 🙂 This is where devise+omniauth combo rocked! I had the custom omniauth strategy configured on App2 also. Now, if a user had logged in App1 and got authorized. In the same session (depends on how you have devise configured — I had :timeoutable and :token_authenticatable) when the user accesses App2:
- I added a standard before filter called ‘login_required’ which redirects to User Manager via ‘/auth/my_strategy’ if there is no current_user.
- User Manager checks and finds a valid token and returns this to App2.
- The user is automatically signed into App2.
Aim achieved! Seamless single-sign-on between multiple applications.
Update 1
I have extracted the code for this and have open-sourced it (finally)
The provider is at https://github.com/joshsoftware/sso-devise-omniauth-provider
The client is at https://github.com/joshsoftware/sso-devise-omniauth-client
I have updated the README for detailed instructions.
Update 2
Added account linking support. So if a user registers via Twitter and later tries to login via Facebook – he can link both these user accounts!
Update 3
Github code base is now updated to Rails 3.1.3. Thanks @robzolkos for helping out. The Rails30 branch is available for older the version.
Feel free to ask questions and give feedback in the comments section of this post. Thanks and Good Luck!
Also have a look at Ruby CAS
http://code.google.com/p/rubycas-client/
http://code.google.com/p/rubycas-server/
@Anon – The cool thing about omniauth is that seamlessly integrates with Ruby CAS too !
This post is totally awesome ! However it’s quite hard to follow for me (not comfortable with oauth and devise). Do you think it could be possible to put the corresponding apps in a github repository for testing things in details ? In any cases, many thanks for this great post !
@Sylvain,
Sure, I’ll do that. I’ll update the post with the github URL asap.
Hei Gautam, just for you don’t forget to put on github.
Tks for this useful information
Guys,
I finally found some space and pushed the code to github!
The provider is at https://github.com/joshsoftware/sso-devise-omniauth-provider
The client is at https://github.com/joshsoftware/sso-devise-omniauth-client
I have updated the README for detailed instructions.
Hi Gautam,
first let me thank you for this blog post, and for the link to the github repository.
I’m having a few small problems getting this working correctly. Firstly I installed your app and ran it on both client and provider sides. If I register, then I’m logged in across multiple clients. However if I log out, then I’m unable to log back in, and am simply redirected to the provider log-in page.
I will continue to investigate.
thanks again
Paul
Hey Paul,
Whats the logs say? I am pretty sure you have a filter_chain_halted somewhere in the request. Do paste the log output here, it would be easy to investigate and tell you whats going wrong.
Cheers!
– Gautam
Hi Gautam, thanks very much for your reply.
I have determined that it’s failing on the first line of ‘sessions/create’ in devise:
resource = warden.authenticate!(:scope => resource_name, :recall => “#{controller_path}#new”)
as for the server output, it’s nothing unusual, but I’ve posted it here:
http://pastie.org/1563391
cheers
Paul
just one more thing, the problem still exists with the provider app in standalone mode. thanks again
Gotcha!
I have updated the source code and pushed the chantges!
The config/initializers/devise.rb had the authentication key set as ‘username’. I commented that line to ensure that by default it was email.
The omniauth continues to work as it bypasses the default authorization.
Thanks for finding the bug.
– Gautam
perfect!, thank you..
Tks again Gautam
This is an awesome piece of work! I’ve been thinking about implementing something very much like this myself for a while. I too decided to go with Omniauth and Devise on the server and Omniauth on the client. I was planning to implement CAS between the client and server, but OATH should do fine. Any reason why you chose OAUTH as your internal protocol instead of CAS or OpenID?
Martin
Martin,
Thanks – your comment very comforting! 🙂
I used OAuth2 because its the latest standard – and probably the lastest and most comprehensive. You can use OpenID and CAS too but then why not move among the latest stuff 😉
Big thanks for this nice tutorial and the demo code!
But i can only get the client working with omniauth version ‘0.1.6’. If I use the current omniauth version (0.2.1) i always get ‘message”=>”invalid_credentials’
Is there an update needed for the provider strategie?
Ok…i found the problem! 🙂 … I just needed to change the ‘access_token’ to ‘oauth_token’ in the ‘user’ model! *damn*! 🙂
Server file: app/models/user.rb
FROM:
self.token_authentication_key = “access_token”
TO:
self.token_authentication_key = “oauth_token”
It´s needed because Omniauth version >= 0.2.1 is using the >= ‘OAuth2 draft 10’ specification where it has to be called ‘oauth_token’.
Good to hear this and thanks for updating it here.
Thanks for this great post.
But I have the following error message in my client: invalid_credentials
Im using omniauth 0.2.5
You figured out how solve this problem?
You already changed
self.token_authentication_key = “access_token”
to
self.token_authentication_key = “oauth_token”
in your ‘app/models/user.rb’ file? This fixed my problem..
Best regards
Rouven
I did this but still get `invalid_credentials`
Hi, Excellent work josh, i already use and works great, but i have a question, it is possible do logout in all clients at same time?
JHafliger
Yes – you can. Keep the secret token (config/initializers/secret_token) the same for all the clients. It will destroy the rack-session for all the SSO clients.
Do check the github issues for the repositories – its mentioned there.
I’ve tried going down the road discussed on the client repo issue list, but things seem odd. I’m using Apache locally with three domains, all in my hosts file:
sso.domain.dev
client1.domain.dev
client2.domain.dev
Everything works great until I try to tackle sign off. Adding the common secret token has zero effect (I believe this is because the token is only used for signed cookies and doesn’t actually have anything to do with which session is being used). Adding the session store :host just starts breaking things. I used sso.domain.dev for the :host across all three apps (provider + 2 clients). Doing so prevents any kind of log in to the provider. If I access the provider directly (not part of an oauth redirect) I’m just continually prompted to log in (no error messages shown). From my dev log it looks like login is successful, but seems like it can’t save the user into the session with the :domain set.
I’m pretty sure I’m just doing something wrong, but do you (or anyone else) have any more info on the topic or have you run across any “gotchas” that need to be taken into account?
Solved the issue. Apparently the :domain doesn’t like subdomains. All had to be set to ‘.domain.com’ instead of ‘sso.domain.com’
One concern though, doesn’t this mean that now actual sessions are shared between apps? So if you put something else in a session you can run the risk of overwriting data from another app using the same SSO service?
@Cliff
Session sharing and SSO are different things. SSO requires sessions sharing.
Session sharing is a common practice from very early versions of Rails. Since all apps on the wild-carded sub-domains are supposed to be trusted it is not a security issue. Furthermore, you never store sensitive information in the session cookies. Furthermore, these are usually encrypted cookies.
@Gauntam – Thanks for the reply. Maybe I’m misunderstanding what you’re saying so let me explain a bit more.
I guess I mentally just separate the idea of SSO (Singe Sign On) from session sharing. SSO (to me) simply means a single authentication mechanism with nothing to do with session sharing. I can use a single sign on service like OpenID and that is obviously not sharing a single session between every OpenID enabled site.
Now my issue emerges when trying to sign OFF. Would I be correct that the idea of SSO in and of itself does not handle or support a single sign OFF mechanism because to sign off you must end the session (not the scope of SSO)? I think the discussion so far has eluded to the idea that single sign on and off are both a unified idea but in reality Single Sign On is authentication and Single Sign OFF is session sharing. This is closer to my understanding of the separation between those two groups of functionality.
My concern over session sharing is not security, but practical limitations. Rails has a limit to the cookie session store and moving to another session store starts to impose limitations on the physical network of the machines (domains must be on the same box, or must access a shared DB server, etc…). It also rules out sessions from other legacy platforms or applications. Then you have my original concern which is simply overwriting data. If domain aaa.doman.com writes session[:next_url] = ‘/some/path/value’ and domain bbb.domain.com writes session[:next_url] = ‘/other/path/value’ then the value will be wrong when aaa.domain.com tries to read it.
In some situations, session sharing is probably ideal, but in mine, not so much. I have the advantage of starting with the requirement that all of my user models for all domains be held in a common DB table (same user data across all domains). I’m thinking a better approach is to have any explicit logout flag the user’s record and let me check user.last_logout for a date in the past and if it’s found, abandon the session manually.
@Cliff
There are server-side sessions and client-side sessions. When using devise the “logout” method will logout the user by destroying the warden.session and thereby destroying the cookie (client side session) too.
So, when you logout, it is genuinely a logout (part of the authentication process) and this *has* to be sent to the provider app after which the user is signed out. Since all the apps share the same key from the session_store.rb, their client session is also destroyed.
Now, if the client cookies were configured to work across multiple domains, the user is auto-logged of from all of them.
You may be leaning towards a CAS setup for your work (IMO)
Hi gautam, this looks like a great way to accomplish what I want to.
I think I’ve set everything up correctly, I’ve created one provider and one client (to begin with). On the provider, I can sign up and sign in and everything works smoothly.
When I access the client app it redirects to the provider to sign in, as expected. However, when I try to sign in (via the client app):
1. I fill out the sign in form
2. Get redirected back to localhost:3001/auth/josh_id (client)
3. See an error message from the browser saying that there have too many redirects – it appears to get stuck in a loop of some sort.
I do get signed in (because I’m signed in when I go to the provider site) but something isn’t working right with the redirection back to the client app.
I’ve changed the database from mysql to postgresql, but I don’t think that should have had an effect. Thanks for any help 🙂
Cheers 🙂
Here’s the last part of the development.log of the provider, if that helps 🙂
http://www.pastebin.com/AA4itVML
I noticed that the access_token is empty in some of the requests, but I don’t know if that’s intentional.
Cheers
@Jørgen I have seen this looping happen when the client application is not registered with the provider. You need to do that via rails console.
In all likelyhood, login_required routine redirects back to /auth/josh_id but since the client is not registered with the provider, its returning ‘Invalid Credential’.
In case you have registered the application, do let me see some code, I shall help you resolve the problem.
Thanks for the quick reply, and you were correct of course, I had forgotten to add the Client to the Provider 🙂
I’m sure I’ll have more questions as I play with this some more, thanks again for taking the time to respond 🙂
Cheers!
Hi again Gautam, another question 🙂
I’ve got sign in working across 3 domains, one is the provider app, the other two point to the same client app. (my goal being to show slightly different content on the different domains but just using one app to keep things simple)
Now, I need to tackle single-sign-off. I’ve followed the instructions in the issue list, but it didn’t seem to work.
Here’s what I did:
1. Changed the secret token to be identical on the provider and client
2. Altered the session_store files:
This is the client:
OauthClientDemo::Application.config.session_store :cookie_store, :key => ‘_oauth-client-demo_session’
if ENV[‘RAILS_ENV’] == ‘production’
OauthClientDemo::Application.config.session_store :cookie_store, :key => ‘_myapp_session’, :domain => ‘.psdrefill.com’
else
OauthClientDemo::Application.config.session_store :cookie_store, :key => ‘_myapp_session’, :domain => ‘.rails.localhost’
end
The provider:
OauthProviderDemo::Application.config.session_store :cookie_store, :key => ‘_oauth-provider-demo_session’
if ENV[‘RAILS_ENV’] == ‘production’
OauthProviderDemo::Application.config.session_store :cookie_store, :key => ‘_myapp_session’, :domain => ‘.psdrefill.com’
else
OauthProviderDemo::Application.config.session_store :cookie_store, :key => ‘_myapp_session’, :domain => ‘.rails.localhost’
end
These are my three sites:
http://www.psdrefill.com (provider)
http://www.uirefill.com (client – app1)
http://www.shaperefill.com (client – app1)
I assumed that the provider-domain-URL should go in the provider and client’s :domain variable in the files above, but then visiting the clients sent the system into a loop again.
Then I changed the URL to be the url of the client (uirefill.com) and then the uirefill.com works fine in terms of SSO, but going to shaperefill.com (the same client app, just different domain) starts the loop again. If I sign out at the provider and go to the uirefill.com site I’m still logged in, so signing out does not seem to be working
Oh, all three sites are on Heroku, by the way.
Thanks for any help, I really appreciate it 🙂
Cheers,
Jørgen
Try setting the :domain => :all in session_store – that should make it work.
From what I see, this would work in development (as you have the same host for all apps) but in production you can specify only :domain.
When you use subdomains, you should set it to ‘.psdrefill.com’
When you use different domains, the easiest fix is :all
Thanks Gautam, but it didn’t seem to help 😦
When I sign in I get the Heroku error “The change you wanted was rejected. Maybe you tried to change something you didn’t have access to.”
My config line now looks like this:
OauthClientDemo::Application.config.session_store :cookie_store, :key => ‘_rnapp1234_session’, :domain => :all
– does the key matter? I’ve used the same key in both apps
Thanks 🙂
This is usually a coding error – especially when security exceptions (like invalid authenticity tokens or forgery issues) are raised.
Try this:
– Restart heroku apps
– Clear cache and the cookies (maybe the session key is conflicting)
CHECK LOGS ! 🙂
Hmm, I’ve emptied the cache, restarted the apps and tried other browsers as well. Single sign-out still isn’t working.
Are there other ways of triggering sign out in all of the apps/domains?
I had an idea to just have iframes pointing to each domain’s /logout on the page the user comes to when they sign out on one of the domains – but it isn’t very elegant 🙂
Jørgen,
Hang on! So the heroku error is not happening now? Thats good.
Check your configuration or paste a snippet (pastie link) of the 3 apps and their config/sess* files.
Thanks Gautam, here is the code: http://pastie.org/2036269
I managed to find out when the heroku error occurs. It happens when I sign in as a user that has not previously been signed in. If I sign up as a new user (and automatically get logged in) I don’t get the error. Weird!
🙂
I hope you have SAME secret_token for all the apps (provider and clients). You will find that in config/initializers/secret_token.rb
I just checked out the URLs mentioned above – you have the iframe solution working — its not elegant — can you revert it so that I can see the problem happening.
I’ve reverted to the old code. I’m using the same secret_token in both the provider and client apps 🙂
I also encountered a redirect loop after setting up and configuring the client and provider apps. The client app was registered with the provider. The issue turned out to be related to a change in the oauth2 gem (http://bit.ly/iQc9VW) as explained in this blog post: http://ryanbigg.com/2011/04/whodunit-devise-omniauth-oauth-or-github/
I also encountered a redirect loop in client side and checked it carefully. As tjstankus said, from version 0.2.1, OmniAuth depended on OAuth 0.2.0(bump from 0.1.1). There is a change in Oauth0.2.0(changed @token_param from ‘access_token’ to ‘oauth_token’), ref: https://github.com/intridea/oauth2/blob/v0.2.0/lib/oauth2/access_token.rb. That maybe the problem root. One solution I took is specifying exactly (gem ‘omniauth’, ‘0.2.0’). They works like past. Maybe someone solve this problem in future. I will appreciate it greatly!
I’m trying to use this and getting stuck in a redirect loop:
https://github.com/blueblank/sso-devise-omniauth-provider/issues/2
At this point the client gets to
/auth/generic_id/callback?code=40b17b43c40e2db9088ac41ba4fff53c&response_type=code
then
/auth/failure?message=invalid_credentials
After going through these comments:
– changed access_token to oauth_token in the model
– double checked the client is registered with the provider
still looping, though
Just commented on issues/2 in github. I think the application is not registered properly – at least the logs imply so. can you recheck that?
So how do I register the application properly? I thought I was but there is something I’m missing.
Nothing out of the ordinary:
APP_ID = ‘YE0NYveQGoFsNLX220Dy5g’
APP_SECRET = ‘aqpGBedDnHFyp5MmgT8KErr9D015ScmaY8r3vHg5C0’
Provider console> ClientApplication.create( :name => “MyApp”,
:app_id => APP_ID,
:app_secret => APP_SECRET
)
Client configuration: /config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :josh_id, APP_ID, APP_SECRET
end
Route: /auth/josh_id
Hi Gautam. I tried the provider app in your Github repo on rails 3.09 just by changing the gemfile.
It works properly almost all on rails 3.09, but I got error
‘undefined local variable or method `save_referrer’ ‘
in
before_filter :save_referrer, :only => :edit
of RegistrationsController.
Commented out this line, it works without error, but it could cause security holes.
I tried to search the method ‘save_referrer’ in files and web, i couldn’t get useful info about it.
Please show me where is the method defined in?
Thanks for your great works.
Hi Dora,
I think you found a bug. save_referrer is a stagnant method and we can remove it now.
I was trying stunt to save ‘return path after login’ but I you dont really need it for ‘edit’. So, just remove it. Registrations#edit would be used for things like profile management.
Thanks a lot.
This SSO solution is the best for now.
I hope you keep updating for rails 3.1.0 and more.
Cheers 🙂
Hello Gautum.
I had a question about putting the provider under a suburi for passenger. I cannot figure out how to get the path to be /suburi/auth/strategy instead of just /auth/strategy. I do not know where this path is coming from. Is there a way to fix this?
I read about a possible bug in omniauth and a quick fix was to override the request_path or add a path_prefix. But how do you set these?? My clients are also on suburi and their paths seem to work ok but the provider always tries to use /auth/strategy which is wrong.
Any help would be greatly appreciated!!
Thanks.
Hi Mike,
I’ll check on override of omniauth methods — it seems the way to go — but I guess you would also have to configure provider routes.
Can you paste a snippet of what your config — I can try to simulate it.
Thanks for replying. I am trying to deploy your provider under apache/passenger and the routes file is your basic setup. I have tried to wrap the routes in
scope “/suburi”
do
but that doesn’t seem to help. It actually looks like a request is made to “/suburi/auth/strategy” which is correct but then an error is thrown:
ActionController::RoutingError (No route matches “/auth/strategy”)
That path seem to be hardcoded within omniauth or devise.
I notice that only your client has an omniauth.rb in the initializers directory. Can the provider also have this file? We may be able to set some things in there.
Thanks.
Hi Mike,
The provider is an omniauth client too – infact thats how oAuth via twitter and facebook kick in for clients too.
I wonder if we can configure our provider route with the specific suburi – hmm. need to think about this 🙂
superb work.
can u please give some guidelines on how the flow will work if the client opens the provider in a lightbox for login/signup.
As I see it, there are plenty of redirections involved in OAtuh – 3 way handshake – so this would not be a good choice for a lightbox.
I’ll try some stunts and try to come up with a solution.
Well I have tried adding suburi to the routes but something in omniauth still tries to use /auth.
We need a way to configure omniauth to prefix /auth with /suburi. Not sure how. Thanks.
FYI, I got around this by putting the provider on “/” and all of the clients on “/suburi” and using virtual hosts in Apache.
Mike! This is cool. Can you post some gist for this work-around.
You could also show how to sign requests to provider’s API with the user that is signed in.
Hello,again.
The newest omniauth virsion 0.3.0 causes errors on this app. (gem ‘omniauth’ in gemfile without virsion)
The client app requests ‘http://localhost:3000/oauth/token?….’, but the server app has no route for the URI.
I added route.rb in the server app
match ‘oauth/token’ => ‘auth#access_token’
then the rooting error is gone, but the assumed query strings are not on the URI request(from client app). So the browser error ‘Request Roop’ happens.
The users who started or re-installed recently and got errors had better check the virsion of omniauth in gemfile.lock after bundle,
or you can easily avoid the errors by specifying the virsion of omniauth in gemfile before bundle.
I tried 0.2.6 in the client app, and it works good again.(on rails 3.0.9)
Don’t work hard to fix it. Relax.
OmniAuth1.0 will coming soon,and the protocol of oauth2 itself may still change.
Regards.
Now that i have an oauth provider and six apps using it. I am wondering how to allow 3rd party app’s to access the api’s of all my six applications using a single oauth access token from thesame oauth provider that provides the single sign-on for the six applications. Something like using one facebook or google access-token to access all the various api’s provided by facebook and google.
Thanks.
@Ed,
That is exactly what the oauth provider is 🙂
All you need to do is the application registration process (automate it or do it manually). For example, you have to register applications with Twitter and FB before using it. When these applications are successfully registered, they get a token and secret. These are exactly the same as the Client records in this database for the Oauth provider.
If the Oauth provider generates these application tokens for 3rd party apps – they can login and then you can choose using some authorization tools like cancan along with Rabl to control the API access.
Thanks Gautam for the guidance and for the many great blog posts.
Hi Gautam,
First thanks for this great explanations, it helped me a lot figuring out the right set up for my auth system! I have one question in my mind for which I can’t find a good answer ; let’s say App1 and App2 need to access the UserManager’s User model to display & edit the current_user profile (first_name, last_name…), how would you implement such interaction? (JSON API, synchronisation of the User model in AppX and UserManager, user profile’s page hosted on the UserManager…) Again thank you very much!
@Bastien, If you synchronize data across your apps, you would end up with some crazy amount of complexity. The User model should *always* reside on the UserManager (I am guessing this is the omniauth provider for App1 and App2). In case you need to show / edit the user details, you can do this:
1. Post-authentication, you send the user.json request (as it shows in the sample app) and return the current details of the user – cache this information and show it in App1 or App2.
2. When you want to edit a user, simply redirect the user to /users/:id/edit Since there is SSO in place, you can easily move between applications and you can set a return_url in the session if required.
Thanks for your quick reply, it makes more sense now and SSO enables a seamless access to the UserManager’s data. ‘Step 2.’ would mean that the /users/:id/edit ‘s view is common to all Apps. If we wanted App1 and App2 to have a different design to show / edit the user profile I’m guessing that ‘Step 2.’ would not achieve that, am I right?
If you want a different view for App1 and App2, you can fetch the user data from UserManager using user.json and then render the info in your own way specific for App1 or App2.
In case of edit, you should set the form url to post the data to UserManager app and send a redirect_url as one of the parameters, so the views are consistent after update.
Great post and great feedback to readers with questions! Thanks for sharing the implementation on Github.
Excellent guide!
Question for you guys:
Say we have 3 Apps.
App1 is the user manager/oauth provider
App2 is a main app and oauth client
App3 is a utility app and 2nd oauth client
This solution works great for this situation.
How would one manage the case in which the user is logged in and using App2, but now App2 wants to communicate with App3 (at the backend level) to get some information?
I’m talking about the situation in which App3 has a REST API that App2 wants to call. Since it’s not going through the user’s browser I’d imagine there’s something special that needs to be done?
Thanks!
@DB, Your question is actually not related to SSO – its about how 2 applications can talk securely.
Have a look at https://blog.joshsoftware.com/2011/12/23/designing-rails-api-using-rabl-and-devise/This talks about how we can build APIs and how we can communicate securely between apps.
The “api key” can be the client id in this case. @gautamrege
Is there a setup that app1 holds the user model and gives authentication services to app2?
@gady – not in this repos. But like I said in the earlier comment, you can use RABL to build an API service for this – though it would be a little network intensive.
Man, you rock!
Many thanks for this great post, it sown some thoughts in my head 🙂
Hi,
Thanks for your post. Currently I am using the rails version 2.3.5 upgrading the those apps is big deal . To implement SSO as you mentioned need to upgrade the rails or still can use with some modification?
Thanks
thil
@thil My earliest implementation was 3.0.3 🙂 So, I recommend you check on forks on the github repos to see if someone has done this for Rails 2.3 – I personally have not tried it.
Omniauth and Devise are not dependent on any version of Rails (or its dependencies), so I think this should work if we downgrade to Rails 2.3. Just ensure that you are using Rack > 1.0, I’m pretty sure that is required.
If you fork and have a setup for this, I would be glad to merge that into a branch.
I tried changing:
self.token_authentication_key = “access_token”
to
self.token_authentication_key = “oauth_token”
But I still get the `invalid_credentials` error. Did anyone find a fix by any chance?
@Drew
You might want to take a look at this ticket: https://github.com/joshsoftware/sso-devise-omniauth-provider/issues/12 I guess the solution is present here.
Hi – first of all, thanks for the great piece of software !
fyi – I had to change the database.yml to work with rails 3.2.6 a little bit.
the replacement for device:
## Database authenticatable
t.string :email, :null => false, :default => “”
t.string :encrypted_password, :null => false, :default => “”
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, :default => 0
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
thx a lot,
andy
could you send me a pull request please
if a user with unique user_id is logged in different browser at the same time (ie logged in in firefox,IE,chrome and in safari). and if I use sign_out @user then it will log out the user only from the respective browser from where sign_out @user is called , same user on the other browser are still logged in. Any solution?
Since we are using the cookie_store (default) the other user will still be logged in from the other browser. If you change this to the session_store it should solve your problem.
Have a look at http://problemateek.blogspot.in/2012/07/using-activerecord-session-store-in.html and http://api.rubyonrails.org/classes/ActiveRecord/SessionStore.html
Do update the post here too with your findings.
i m using device’s sign_out
Hi gautam ,
thx for ur last post.
nw i m stuck in another issue. its like i m using SSO and der is session expired time out set on sso portal den a request is send to my dependant application which uses SSO portal fog logging in dat is der any sesssion related to dat user is active or nt if no session for dat user den session gets expired from SSO portal and if any session related to dat user is still active on dependant application den it wont allow to destroy the sso portal session of dat user. so when i get a specific requst from sso portal i want to knw details of how many session are active for dat user on my dependant application?
@abhishek,
I tried my best to understand what you have written – I could not understand anything. Could you comment again in plain english — not sms style please 🙂
I have a web app where i have used devise and omniauth for authentication and now i want to use this for iphone app. Now the user can send the email and password if he has signed up using devise and it returns the authentication token and email id of the user where the user can use the token and make the subsequent requests but how can i allow the facebook users to make requests similarly. I have searched and could not find how to do this? Only thing i found useful is
http://stackoverflow.com/questions/4623974/design-for-facebook-authentication-in-an-ios-app-that-also-accesses-a-secured-we but still do not know how to do the one that is explained there.
So pls help me
@Logesh This blog post talks about exactly this but using a web client, not a phone client. If your iphone app is a web-client, you can configure the Provider to talk to social networks like twitter and facebook as shown in this post and the users can sign_up / sign_in using them — and get their tokens setup via Omniauth. You cannot get users to sign up on the provider via email and the app their FB token – it will be rejected.
On the other hand, you may have to get signed-in users to “add FB authentication” to your app and then save the long-lived token in the database. That however has its own complexities.
Not sure if this helps but if you give me some more details, I could investigate.
I have problems when overriding passwords controller in devise. I do not want to sign in the user after password is changed so i thought of overriding the password controller and i tried as follows and got an error. I could not identify what the problem is, so please help me. The following is the passwords_controller.rb
class PasswordsController :edit
def new
super
end
def create
super
end
def edit
self.resource = resource_class.new
resource.reset_password_token = params[:reset_password_token]
end
def update
self.resource = resource_class.reset_password_by_token(resource_params)
if resource.errors.empty?
flash_message = resource.active_for_authentication? ? :updated : :updated_not_active
set_flash_message(:notice, “New password has been saved”)
redirect_to new_user_session_path
else
respond_with resource
end
end
protected
# The path used after sending reset password instructions
def after_sending_reset_password_instructions_path_for(resource_name)
new_session_path(resource_name)
end
# Check if a reset_password_token is provided in the request
def assert_reset_token_passed
if params[:reset_password_token].blank?
set_flash_message(:error, :no_token)
redirect_to new_session_path(resource_name)
end
end
# Check if proper Lockable module methods are present & unlock strategy
# allows to unlock resource on password reset
def unlockable?(resource)
resource.respond_to?(:unlock_access!) &&
resource.respond_to?(:unlock_strategy_enabled?) &&
resource.unlock_strategy_enabled?(:email)
end
end
and my routes is
devise_for :users, :controllers => { :passwords => ‘passwords’ }
and the error i get is
NameError in PasswordsController#update
undefined local variable or method `resource_params’ for #
@logesh – Devise signs out a user when the password is changed. This is the default behavior and you don’t need to do anything special !
But when i click the forgot password it asks for email id and after giving the email id it sends the reset password instruction to the mail and when clicking on the link it asks for new password and confirm password and after giving it i logs in the user and i want to avoid this.
@logesh – Ah! now I understand what you are doing 🙂
Not tested this yet but try changing the redirect_to line to:
redirect_to new_session_path(resource_name)
If that still fails change it to:
respond_with resource, :location => new_session_path(resource_name)
If that fails, I’ll try some stunts locally and let you know
My update method is as follows and it does go for redirect and it shows error in the first line itself
NameError in PasswordsController#update
undefined local variable or method `resource_params’ for #PasswordsController:0x000001034501b0>
def update
self.resource = resource_class.reset_password_by_token(resource_params)
if resource.errors.empty?
flash_message = resource.active_for_authentication? ? :updated : :updated_not_active
set_flash_message(:notice, “New password has been saved”)
redirect_to new_user_session_path
else
respond_with resource
end
end
@logesh – can you paste the passwords_controller file to a gist — I can check it out. Hope you have inherited it from Devise::PasswordsController
I have pasted the passwords_controller file in
Public Clone URL: git://gist.github.com/4124530.git
Your code seems correct – there does not seem to be any reason resource_params should not be found. Can you check the rest of your code for existence of another PasswordsController class?
Also, you don’t need to copy paste all the data into your PasswordsController… it should ideally contain only the ‘update’ method — you don’t need to copy the rest of the code. I’ll play around with a sample app and check this.
Finally it works. I am using devise version 1.5.3 and it does not provide resource_params method so i copied the following from devise version 1.5.3 and it works. self.resource = resource_class.reset_password_by_token(params[resource_name])
lol – ironically, I was reading this post http://stackoverflow.com/questions/8809681/cannot-override-devise-passwords-controller and the solution is exactly about 1.5.3 and upgrading to 2.1. I assumed that since the controller is getting overridden, you were on the latest devise version.
Anyway, thanks for updating the post — it should prove helpful to others.
I use devise and so in my route file i have devise_for :users, so when i click on the sign up link the url looks like
http://localhost:3000/users/sign_up
so i thought of removing users from link and just sign_up would be enough so i changed my routes as
devise_for :users, :path => ‘signup’, :path_names => {:sign_up => “new”} which now makes the url as
http://localhost:3000/signup/new
and similarly added
devise_for :users, :path => ”, :path_names => {:sign_in => “login”, :password => “password”}
and the url for sign in changed to
http://localhost:3000/login
but when clicking on forgot password link the url is
http://localhost:3000/signup/password/new
but i want it to be
http://localhost:3000/password/new How can i do this? How should i change my routes to get it?
@logesh,
You have 2 paths defined. The way I see it:
devise_for :users, :path => “”
This gives the routes:
HOWEVER, I would not recommend this — see “/edit(.:format)” .. really ??
Hello,
I’m having some difficulties getting this configured into one of my apps. I have a main application which also serves as the provider. Then I have another app that serves as the client. I have the josh_id.rb file configured with the proper domains. In the client’s application controller I have the login_required method and the before_filter and when the user goes to the index of the client, it will redirect to /auth/joshid. The method in the auth_controller that properly gets called the is access_token. This method properly authenticates the application and authenticates the access grant. However, my provider application says on the user method that access is denied and responds back to the client application that the user needs to log in. In the database, Access Grants table has the proper user id of the user I used to log into the system. I’m not sure what is wrong. Do you have any advise?
Thanks.
@KyleSziv
From the looks of it, it seems that when you call /user.json method, you are not passing the access_token. That’s why its giving an error.
This is the request that should go the provider for /user.json:
/auth/josh_id/user.json?oauth_token=#{access_token.token}”
If this is correct, some code snippets and logs snippets would help debug this .(pastie or gist).
@Gautam
It appears that I don’t make it to user.json at all. Maybe these code snippets will make more sense than me describing it:
https://gist.github.com/1d9dcceebeb0e50046f4
The josh_id.rb file is the same as what you have in our example on github (except it uses my provider’s url).
Any help is much appreciated. Thanks.
@KyleSziv — found something interesting that may be the cause of the invalid credentials.
https://github.com/intridea/omniauth-oauth2/pull/18 talks about “state” parameter to prevent CSRF.
I saw the ‘state’ parameter in your callback and started investigating a little. I see that you are using the omniauth-oauth2 (v1.1.1) gem for the OAuth2 strategy. I wonder thats what’s causing the trouble – not sure yet though.
Can you downgrade omniauth to v1.1.0 and see if the problem persists? If it doesn’t, it means we need to upgrade our provider code to handle the “state” and in general upgrade to omniauth v1.1.1 🙂
It appears that solved a lot of the issues I was experiencing. Now it just gives a MySQL error saying that authorization_token column is not in the user table. I added an additional line in the authorize method and it allowed me to the client.
I think it’s at a pretty good point of success. I really appreciate all your help with this. 🙂
Nice!
However that does also means that an upgrade to the latest omniauth breaks the system and we have to adapt to that.
I have file an issue#19 on github, so that we can fix it.
Do provide more details there that could help.
Is there any way so that i can store each user’s id against his session_id in new table called user_session in rails 3???
I have a user model and language model where the language model contains list of languages and the user model contains list of users and i want the user to select a language and that would be users native language. I am using this for iphone app so when the user calls an api the list of languages will be sent and from that the language id has to be passed and it should assign the language for the user. How can i do this? Thanks in advance
Hi.
After running “rake db:migrate” I get this output:
C:\Sites\sso-devise-omniauth-provider>rake db:migrate
rake aborted!
uninitialized constant Devise::Models::TokenAuthenticatable
C:/Sites/sso-devise-omniauth-provider/app/models/user.rb:7:in ‘
C:/Sites/sso-devise-omniauth-provider/app/models/user.rb:1:in’
C:/Sites/sso-devise-omniauth-provider/config/routes.rb:2:in block in ‘
C:/Sites/sso-devise-omniauth-provider/config/routes.rb:1:in’
C:in execute_if_updated’
C:/Sites/sso-devise-omniauth-provider/config/environment.rb:5:in )>’
Tasks: TOP => db:migrate => environment
(See full trace by running task with –trace)
I could not found any solution for it.
I will appreciate if you could please guide me through this.
Regards.
Ehsan.
Argh!
Devise has deprecated TokenAuthenticatable. Thanks for the comment, I need to update the code. In the mean time, have a look at https://gist.github.com/josevalim/fb706b1e933ef01e4fb6 and http://stackoverflow.com/questions/18931952/devise-token-authenticatable-deprecated-what-is-the-alternative
I will update the code soon. In the mean time, if you fix it, do send me a pull-request 🙂
> New comment on your post “Multiple Applications with Devise, Omniauth and Single Sign On” > Author : Ehsan (IP: 217.235.149.168 , pD9EB95A8.dip0.t-ipconnect.de) > E-mail : e_p_m_157@yahoo.com > URL : > Whois : http://whois.arin.net/rest/ip/217.235.149.168
Hi,
I tried to implement https://github.com/gonzalo-bulnes/simple_token_authentication to original code but can’t make it, does anyone have done this? Thanks!
I’ll take a look.
Can the client App access the Google OAuth tokens so that it can then access e.g. the YouTube API as the given user for displaying the users videos? (I only need read-only access to things – and only if the user want’s to allow us to access it. Same for Flickr, Dropbox, Google Drive etc.)
Nope .. that would be a breach of token authentication, right? The provider authenticates with other 3rd party providers and gives its own token.
Hey Gautam!!.. Your tutorial was just awesome it solved my half of the problem. Now i switched rails 3 to rails 4. Getting some errors ,I have sent you a E-mail.please revert me
Hey Gautam,
The blog you mentioned for oauth provider is not available now… Can you help me with some directions to implement oauth provider similar to that??
@Gautam, Very nice article. I understood how SSO works with custom provider.
But I’m not getting how can I register my application. I mine how can I get app_id and app_secret keys
Thanks