#235 Devise and OmniAuth (revised)
Jul 13, 2012 | 10 minutes | Plugins, Authentication
In this episode I build upon a standard Devise setup and show how to add 3rd party authentication using OmniAuth. This includes support for user validations and is compatible with normal password authentication.
- source codeProject Files in Zip (88.8 KB)
- mp4Full Size H.264 Video (22.1 MB)
- m4vSmaller H.264 Video (11.8 MB)
- webmFull Size VP8 Video (14.1 MB)
- ogvFull Size Theora Video (27.6 MB)
Lets say I had an app on heroku, with omniauth configured to let users sign in with twitter. Would I need an SSL certificate on my site, or is it safe without one?
checking on one of my apps, it seems like the authentication goes through Twitter's secure connection.
I don't use it.
Thanks again, Ryan. You say that adding an authentications model is not always necessary to have users authenticate through multiple services. What is the alternative to an authentications model? If there is only one :provider and :uid field, how can you store provider ids and uids from multiple services in the user table?
Combine the definition together?
twitter_id, facebook_id, linkedin_id, plus_id work for me.
Performance-wise I would say it's just extraneous joins or database calls to get the info.
If they aren't authenticated with that service the value is null.
Normalization here is not necessary due to the has_many relationship being constrained.
Also, where do you define:
"TWITTER_CONSUMER_KEY" and "TWITTER_CONSUMER_SECRET"
Thanks. Any idea how I would define it on Heroku?
Sure. See their documentation about "Configuration and Variables": https://devcenter.heroku.com/articles/config-vars
you could also use the figaro gem where you define everything in an yaml file
The figaro solution is cleanest, a single source of authority, hides everything from being exposed on remote repositories, and IMO is easier than secrets.yml in rails 4. Figaro is the way to go.
In this episode it is defined in initializers/devise.rb.
That is where the constants are stated, but they are not defined there. I think Dominik is on the right track.
If you're using Pow.cx, you could define these in .powenv file. If you're using foreman, you could define it in .env file in your app directory
I get the following error when I try this on Ubuntu 10.04. Does anyone have any suggestions?
OAuth::Unauthorized (401 Unauthorized):
oauth (0.4.6) lib/oauth/consumer.rb:216:in
oauth (0.4.6) lib/oauth/consumer.rb:136:in
Had the same error...I ended up removing the ENV and  from the CONSUMER_KEY and CONSUMER_SECRET in the devise.rb initializer file. I also used http://127.0.0.1:3000/auth/twitter/callback as my callback URL in the Twitter App settings and it seemed to fix the error for me. In case it matters, in the Website field I used my production url...http://myappname.com. Hope this helps.
^^ If you just use 127.0.0.1:3000 for the callback, it works.
I was actually getting the same problem, and I had to remove the ENV from the KEY and SECRET inorder to get it working. Just adding the callback didn't work.
I added the callback AND removed ENV but I am still getting the same error...
Same deal with me. In devise.rb, I just use:
config.omniauth :twitter, "key-xxxxxxxxxxxxxxx", "secret-xxxxxxxxxxxxxxxx"
As all ways great episode!
It would be nice if you could make an episode on the topic of creating a oauth service with devise and doorkeeper.
Thanks Ryan, I have been waiting for this one for some time now.
Hi Ryan, great episode indeed, well worth $9 itself.
I'm pretty curious about how to implement authentication via multiple services too. Any episode taking that path soon?
normal registration form is also not showing the password field ..
how can I make sure its shown for users who want to register through site and not show the pass for user from oauth ..
in other words, please tell how to make it registrable + omniauthable
I also want to do this, I found this link but I haven't tested it yet, maybe it works for you
Great episode as always Ryan. +1 for a future episode that goes into setting up a separate model for managing identities (i.e. how to link multiple accounts to a local account and deal with the various edge cases). Keep up the great screencasts!
Great episode Ryan. The Devise Omniauth documentation isn't much detailed now and I was having trouble understanding the code samples.
In the episode, you mention that adding a separate Authentications model is 'a complexity that is not always needed'. If one requires to associate the user account with multiple providers, should we make a separate authentications model or add different keys to user model like twitter_uid, facebook_uid as Adam Sunderland mentions in an earlier comment?
I find my self combining this the OmniAuth part 1 lesson with this lesson in order to associate multiple accounts. It's located here: http://railscasts.com/episodes/235-omniauth-part-1?view=asciicast
Can you post how you managed to do this on pastebin or upload source to github ? I want to able able to associate multiple accounts but am kinda lost.
Thanks for another great episode, Ryan!
How would you do it if you also wanted to use the :confirmable module in Devise? This sends an email for confirmation on user creation, but it fails in the scenario where you don't require an email address.
Is there an easy way of not requiring email and only confirm it if provided?
Answering myself, there is an easy way, you just need to override confirmation_required? like this:
Devise is great :)
Thank you for answering yourself as this is just what I was looking for :)
Exactly what I needed. Thanks Jaime!
This is great! But I am having a bit of an issue. I can not seem to get it to bypass the password as easily as it should be.
I am allowing login via facebook using the information from this railscasts. My resource is :subscriber instead of :user, but the rest is pretty much the same.
and my model contains:
When I run debugger and interject immediately before
subscriber.persisted?I can see I have a provider and a uid for subscriber, but
subscriber.persisted?returns false because
What am I missing?
Nevermind... I found out my issue was that in the model I was still validating the presence of several fields (including the password).
What happens in the following cases?
It heavily depends on how you structure your application, but here's what I'm going to do with my app, where the email is the unique identifier of a user.
First case: the user who registered previously tries to log in via Omniauth which returns the same email. Then the validations that email should be unique fails. You should tell the user that an account with this email already exists, and ask him to enter the password for that account so you can link both (then you don't create a new user, but you add Omniauth's provider/uid to his old account).
Second case: the user tries to log in/sign up normally after registering via Omniauth. In this case he will be told that (log in) the password is incorrect or (sign up) the address is already in use. When this happens you should check to see if the provider attribute of the user with that email address is not-empty, and if so, advise the user to login using "provider" (i.e. Facebook).
I would like to implement your "first case" example (with facebook) but I have been running into some issues with how to extend Ryan's method. Any help in how to setup the correct controller actions to essentially let someone merge their facebook uid into an existing user record?
Thanks in advance
I am also interested in these two cases and the solutions proposed be David would work for me. I have tried a few approaches but, like Dan, have run into issues. If anyone has a code example for this it would be great.
I needed this as well. I've managed to make it work (with Facebook anyway - but it should be trivial to support other providers as well). Code at: https://gist.github.com/troelskn/6240837
Note that this is for Rails 4.
I am trying to do something different but I can't figure out what's wrong :
I want the user that are using facebook connect to create a password on my website when they first sign up using FB Connect,
So I what I did is let the error when there is no password and instead of redirecting to the new registration form I want them to be redirected to another form (new_facebook) that will let them enter their password, I want the form to be completely different so having another one seems to be the best how can I do that ?
Sounds as though you're phishing for their facebook password. I see where you may be coming from but answering this question would possibly lead to questionable behavior. Sorry, man.
Hi, I am having a problem when trying two different providers, Linkedin and Facebook. Hope you can help me.
For example once I logged using facebook, and logout from the site, then try to login using linkedin I got an error.
Here's my code.
I had this problem as well. The issue is that when Devise generates the migration for the users table, it includes a unique index on the email address. So you can't have more than one user with an email address of "", which is what you get when you get more than one non-local-auth user. Devise also sets up the schema so that email defaults to "", rather than null.
I solved it short-term like this:
Basically, instead of just indexing on the email address, I index on the tuple of email, provider, and uid, since I only plan on allowing one provider per account.
This isn't perfect since it would allow the same email to be used with a Twitter login and a Facebook login, for instance. Not sure what it does to Devise if you change the default for email to null.
simply remove the following method, if you want to use alias_method. or remove the alias_method call
Works great for me, but the cucumber test fails. I get “Password can't be blank” messages. I guess it is related with using “:without_protection => true” while creating new User.
As I’m using mongoid I’ve changed a bit “from_omniauth” method.
Seems like you missed 'provider' in your mock.
Thanks! It works now!
how could i use "Devise.friendly_token" instead of leaving a blank password?
Folks have asked about combining Devise and other service providers, and there are great turorials (if somewhat dated and/or buggy) out there: github.com/RailsAps
I used the example on rails3-mongoid-omniauth and combind with the railscasts to get Devise, omniauth-facebook and mongoid up and running for myself. Here's a pastebin devise, omniauth-facebood and mondoig
I only posted relivant parts in users.rb which required a deviation from Ryan's
method so things would work with mongoid.
The other area of change is in the authentication_controller.rb
went through some changes so that we could allow username/password registrations and logins, registrations and logins via facebook and to allow account linkage of existing accounts with 3rd party providers.
Also had to add a method:
in the controller, again to appease the mongoid gods since the Authentication can't be saved before the user record.
I went about making a authentications "table" to store the different 3rd party credentials in so users could use multiple providers to login or sign up with, however the only one I've implemented is facebook, so there may be changes you'd need to do in the user.new_with_session method to work with the omniauth.auth hash data.
HTML5 compatible browsers will interpret the type="email" attribute of the username input field and perform the associated email validation:
You can update the username input field to remove this validation. Change the username field to:
You can do the same thing in the
Get the following error when I try to sign in with twitter:
NoMethodError in OmniauthCallbacksController#twitter
undefined method `username=' for #User:0x007fc48595d160
Any help would be awesome!
To fix this you must add username to you User table. See this: http://goo.gl/dQiU5
How can we modify the omniauth_callbacks_controller if we want to add facebook authentications as well ?
cant you just do
alias_method :twitter, :all
alias_method :facebook, :all ?
I first implemented this episode and it work. However, I am using simple_for in my project, so I switched from 'form_for' o 'simple_form_for' in the devise new registrations view.
When the user log in using Twitter, for example, they are redirected to the new registration form as eexpected, since I my project requires email and Twitter does not provide user email address.
The project is that the registration form is not pre-populated with the first and last name on first pass. However, when you lick on the 'log in with Twitter' link again, then the correct behaviour (pre-populating the first name and last name fields) happens.
My question is, how do I avoid having to click the link twice in order to get the correct behaviour?
Never mind, I found the issue and fixed it.
Sorry to bother, but is there something wrong with this video? It doesn't load, and all the download links are 404s.
Could anyone let me know why I am receiving this error on postback?
Could not authenticate you from Twitter because "Invalid credentials".
Did you ever get this one figured out?
did you set environment varilables for TWITTER_SECRET and TWIITTER_KEY based on what you set up on the dev.twitter.com site?
remember this in user model
, :omniauth_providers => [:twitter]
I had to add a comma at the end of path_names in my routes.rb else the server kept giving me a syntax error at this point:
Great tutorial btw - thanks a ton!
I am getting a 404 not found on callback.
AbstractController::ActionNotFound (The action 'twitter' could not be found for Devise::OmniauthCallbacksController):
I have an application using devise for my authentication. How do I allow users to NOT have to input their current password every time they want to update their settings?
What happens if user signs up with facebook, then edits his password on the site? So now the user has a different password for facebook and the site. Doesn't that mean the password for facebook and the site will get out of sync and wouldn't be able to login?
I get this error when I try this tutorial:
undefined method `username=' for #User:0x40e1cb0
To fix this you must add username to you User table. See this: http://goo.gl/dQiU5
I tried with omniauth twitter but I am getting NoMethodError
undefined method `info' for nil:NilClass when click on "Sign in using Twitter" link. I have attached gist for Full trace of error.
Any idea, how to solve this problem.
AWESOME article ...worked like a charm except one change.
If you installed RoR using RailsInstaled make this SSL fix.
I used the same pattern to allow sign in with LinkedIn
Use this gem "omniauth-linkedin-oauth2"
and use this provider :
config.omniauth :linkedin_oauth2, ENV["LINKEDIN_KEY"], ENV["LINKEDIN_SECRET"]
Also you can get the email address from the session so that the user will be automatically created.
In user.rb model class self.from_omniauth method add this line
user.email = auth.info.email
Nice Video.. I am having a problem with devise raising an error for the email can't be blank sign up. After removing the need for a email, I keep getting the error, ' ERROR: duplicate key value violates unique constraint "index_users_on_email" DETAIL: Key (email)=() already exists. : INSERT INTO "users" ("created_at", "provider", "uid", "updated_at", "username") VALUES ($1, $2, $3, $4, $5) RETURNING "id""
Hi everyone, I got a question will trying to use omniauth-facebook with devise
I followed the video(just substitute twitter with facebook) and was able to get it work.
However, I would like to customize my routes a little bit, so this is what I did:
as I expected, in my url "users/..." was changed to "accounts/...", but when I tried to login via facebook, I got this error:
No route matches [GET] "/users/auth/facebook/callback"
after play around with it, I got around it by adding this line:
It works fine now, but I think there must be better solution.
Is there any way to change the returned callback url from
It turns out this is a stupid question...
I figured it out, in
facebook.js.coffeejust change the window location
If you are on Rails 4 and get a cryptic error message along the lines of
ArgumentError in Devise::RegistrationsController#new wrong number of arguments (2 for 1), just remove the
user.rb. Source: http://stackoverflow.com/questions/17105112/argumenterror-in-deviseregistrationscontrollernew-wrong-number-of-arguments
This was exactly the answer I came here looking for. Thank you so much, hopefully it works for me.
Hi, I'm encountering a redirect loop when logging in. Has anyone encountered this before? I'm using Rails 4 BTW. Thanks!
I'm getting a 500 error on my heroku site and a 401 error on my localhost. My site links to Twitter and allows log ins but returns a "Could not authenticate you from Twitter because "Invalid credentials"". I am 100% sure the keys are correct. Any advice?
Hey guys I'm getting an error out of the blue, but I'm not finding anything resembling it on the net.
I get an "undefined method `stringify_keys' for [:email, :current_password]:Array" error on this line "@user.update_without_password(devise_parameter_sanitizer.for(:account_update))" .
Only when I try to update user information. Anyone have any ideas?
Don't know if this will help you, but what fixed it for us was specifying a specific version of the devise gem.
in our gemfile:
gem 'devise', '~> 3.0.1'
I'm getting the same error Jonathan E. Emmett is getting. Three other developers, running (virtually) identical systems do NOT get the error. I have tracked it down (I think) to the fact that 'stringify_keys' is getting an array instead of a hash, but why is that happening? I'm running Ruby v. 1.9.3p392 Rails v. 4.0.0.
Hey kevin, have you guys been able to solve this? I think you are on to something withe versions, but I can't seem to work it out.
I document all of the challenges I worked through in this episode here: http://devetc.tumblr.com/post/64332554082/fumbling-my-way-through-devise-and-omniauth
In general they were
1) 401 issues related to OAuth
2) Issues with adding an extra option to a definition in the user.rb model file
3) Getting stuck at https://api.twitter.com/oauth/authenticate when trying to authenticate
Hi, are these instructions still current? Are they meant to work with Rails 4? I have devise, omniauth and have separate user and authentication models. I'm trying to follow along with this but am not getting the results being demonstrated. Is it possible that this is no longer current? Thank you, OH
Anybody managed to make this work for facebook? I'm getting an Invalid redirect_uri error in my production environment but works fine in dev
thx for your help
Hi, i'm having troubles with password_required and new_with_session
The session are always there, imagine this case, you sign up with twitter, the system asks you for a email, you regrets and want to use normal register, you can't because there isn't the password field and the provider info is still in cache, i need to know where to clear that session :/
Similar problem when you want to update the blank password with an error, it would f.object.encrypted_password.present? would return true in that case, so i add that has_no_password? method for that.
Finally i add a counter for clean the session, after a couple of refreshes or a couple of errors it will show the normal page
Put this in the for what it's worth category, for Postgresql, the
rails g migration add_omniauth_to_users provider uidshould be
rails g migration add_omniauth_to_users provider uid:integerotherwise I was getting a
Error: operator does not exist: character varying = integer.
I had to put several casts together to get an API providing OAuth2, and a devise client to use it with Omniauth, but good job putting the important bits out there.
If you use a session, then doesn't that mean the default devise signup can no longer be used until that session is deleted? I don't see where the deletion is handled. Since using a provider uses the new user registration path, that means the new user registration path can no longer be used to default devise login. Even refreshing with cache clearing doesn't clear a session.
In rails 4, you now need to change the where() method parameters explicitly as hashes. This won't work
You need to change it to
first_or_create does not execute the block code if it finds a record. I ending up doing this: