I'm using the omniauth-facebook gem with devise. It was working until recently. I also recently upgrated to Rails 5.0.1 from Rails 4, but I'm not sure that's the cause.
I currently have 0 users, and I'm logged into Facebook. But when I try to sign up for my app with Facebook on localhost, I get this error:
NoMethodError in RegistrationsController#facebook
undefined method `provider' for nil:NilClass
Here is my User model. I marked the line that the error highlights.
User.rb
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, :omniauth_providers => [:facebook]
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user| #ERROR
#data = auth.info
user.name = #data.name
# ...
end
end
RegistrationsController
def facebook
#user = User.from_omniauth(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, :event => :authentication
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
Also, here's my link:
<%= link_to "fb", user_facebook_omniauth_callback_path(:facebook, thing: #thing.id, degree: #degree, :format => :js) %>
The HTML Output:
<a href=\"/auth/facebook/callback.js?thing=2\">fb<\/a>
And the path:
localhost:3000/auth/facebook/callback.js?thing=2
So the problem is that request.env["omniauth.auth"] is nil for some reason. I can't find any traces of similar errors in any documentation.
Anyone encounter this before or have any thoughts?
To authenticate via facebook all you need is to put a link from your Site to facebook like this:
www.yoursite.com/auth/facebook
and then set up a route to receive the callback from Facebook with the authentication hash:
#routes.rb
get 'auth/facebook/callback' => 'sessions#create_facebook'
Can you specify how the output of this line looks like or why you are passing other information ?:
<%= link_to "fb", user_facebook_omniauth_callback_path(:facebook, thing: #thing.id, degree: #degree, :format => :js) %>
EDIT
auth/facebook/callback is a get request. Facebook sends you the users authentication hash there. Only facebook itself should use that route. When you want to authenticate your link has to be:
localhost:3000/auth/facebook
They way you have it, omniauth is expecting facebook's authentication hash but receives "?thing=2" which results in a failed authentication. Omniauth tries to extract the information from "?thing=2" which is not a hash and when you try to access auth[provider], auth is empty and therefore provider is not defined either which results in :
undefined method `provider' for nil:NilClass
I had the same issue and solved it by removing :omniauthable from User model
Related
I have a Rails 5 site which consists of 2 parts:
Admin area
API-only client area
I'm using Devise for both parts and https://github.com/lynndylanhurley/devise_token_auth gem for the API frontend.
The problem is about using the omniauth authentication. When I omniauth authenticate into the admin area - everything is ok - I get back some successful HTML-response.
But the problem is that I'm getting the same HTML-response in the API-area - but I need some JSON-response - not HTML one.
Here is my code:
config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: { sessions: 'users/sessions', :omniauth_callbacks => 'users/omniauth_callbacks' }
namespace :api do
mount_devise_token_auth_for 'User', at: 'auth', controllers: { sessions: 'api/users/sessions', :omniauth_callbacks => 'api/users/omniauth_callbacks' }
end
end
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules.
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable,
:omniauth_providers => [:facebook, :vkontakte]
include DeviseTokenAuth::Concerns::User
devise :omniauthable
def self.from_omniauth_vkontakte(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.extra.raw_info.first_name.to_s + "." + auth.extra.raw_info.last_name.to_s + '#vk.com'
user.password = Devise.friendly_token[0,20]
end
end
end
app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def vkontakte
#user = User.from_omniauth_vkontakte(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Vkontakte") if is_navigational_format?
else
session["devise.vkontakte_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
config/initializers/devise.rb
Devise.setup do |config|
config.omniauth :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"], provider_ignores_state: true
config.omniauth :vkontakte, ENV["VKONTAKTE_APP_ID"], ENV["VKONTAKTE_APP_SECRET"]
end
Gemfile
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-vkontakte'
Gemfile.lock
devise (4.3.0)
devise_token_auth (0.1.42)
Here's my log:
Started GET "/api/auth/vkontakte" for 127.0.0.1 at 2017-06-20 17:34:23
+0300
Started GET "/omniauth/vkontakte?namespace_name=api&resource_class=User" for
127.0.0.1 at 2017-06-20 17:34:23 +0300
I, [2017-06-20T17:34:23.237270 #15747] INFO -- omniauth: (vkontakte) Request phase initiated.
Started GET "/omniauth/vkontakte/callback?code=0b8446c5fe6873bb12&state=52254649eb899e3b743779a1a4afc0304f249a6dd90b4415" for 127.0.0.1 at 2017-06-20 17:34:23 +0300
I, [2017-06-20T17:34:23.672200 #15747] INFO -- omniauth: (vkontakte) Callback phase initiated. Processing by Users::OmniauthCallbacksController#vkontakte as */* Parameters: {"code"=>"0b8446c5fe6873bb12", "state"=>"52254649eb899e3b743779a1a4afc0304f249a6dd90b4415"}
I guess that the problem is about a so-called "callback" url. I don't understand where it is set. It is obvious from the log that at the end of the auth process the GET "/omniauth/vkontakte/callback..." query is called. And probably it is called always - no matter if I initiated the oath sequence from admin or api client area.
I use Chrome Postman to make the API query http://localhost:3000/api/auth/vkontakte - and I get the HTML-response back ("successful login etc.") - but I need surely some JSON-response.
Is there a way to dynamically change the callback path depending on some precondition?
Is the callback query somewhat different depending on from where the oath procedure was initiated?
EDIT1:
This is not a single problem here unfortunately. Looks like the oauth is simply not implemented in the https://github.com/lynndylanhurley/devise_token_auth gem. So, even if I succeed to switch the oauth login procedure to the JSON way - how do I login the user the devise_token_auth-way - generating 3 tokens etc...? The app/controllers/users/omniauth_callbacks_controller.rb needs to be totally reimlemented.
You can render json from your OmniauthCallbacksController based on some extra parameter provided when your request a connection from the API for example.
These extra parameters will be availables in this hash request.env["omniauth.params"].
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def vkontakte
#user = User.from_omniauth_vkontakte(request.env["omniauth.auth"])
if #user.persisted?
sign_in #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Vkontakte") if is_navigational_format?
if request.env["omniauth.params"]["apiRequest"]
render status: 200, json: { message: "Login success" }
else
redirect_to after_sign_in_path_for(#user)
end
else
session["devise.vkontakte_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
You can this extra parameters by calling the auth helper with additional parameters, they will be passed to your OmniauthController : user_vkontakte_omniauth_authorize_path(api_request: true) (Or whatever your route helper is)
I ended up implementing my own oauth callback procedure - instead of using one from the devise_token_auth gem.
The devise_token_auth gem does contain the oauth authentication - but it appears to be not working properly.
Here are my code changes:
config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: { sessions: 'users/sessions', :omniauth_callbacks => 'users/omniauth_callbacks' }
namespace :api do
mount_devise_token_auth_for 'User', at: 'auth', controllers: { sessions: 'api/users/sessions'}
end
end
app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
include DeviseTokenAuth::Concerns::SetUserByToken
def vkontakte
#user = User.from_omniauth_vkontakte(request.env["omniauth.auth"])
namespace_name = request.env["omniauth.params"]["namespace_name"]
if #user.persisted?
if namespace_name && namespace_name == "api"
#client_id = SecureRandom.urlsafe_base64(nil, false)
#token = SecureRandom.urlsafe_base64(nil, false)
#user.tokens[#client_id] = {
token: BCrypt::Password.create(#token),
expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i
}
#user.save
#resource = #user # trade-off for "update_auth_header" defined in "DeviseTokenAuth::Concerns::SetUserByToken"
sign_in(:user, #user, store: false, bypass: false)
render json: #user
else
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Vkontakte") if is_navigational_format?
end
else
session["devise.vkontakte_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
The inclusion of include DeviseTokenAuth::Concerns::SetUserByToken provides 5 auth headers in response:
access-token →BeX35KJfYVheKifFdwMPag
client →96a_7jXewCThas3mpe-NhA
expiry →1499340863
token-type →Bearer
uid →376449571
But the response still lacks these headers (available at a common sign-in):
Access-Control-Allow-Credentials →true
Access-Control-Allow-Methods →GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Origin →chrome-extension://aicmkgpgakddgnaphhhpliifpcfhicfo
Access-Control-Max-Age →1728000
I don't know whether they are important and if yes - how to provide them.
PS The same identical approach works with Facebook too.
I've spent the last day trying to fix this issue and it's driving me nuts.
Last night, I had facebook login working on my site and was retrieving basic user info. My problems started when I added :scope => 'user_birthday' to config/initializers/omniauth.rb which now looks like this
Rails.application.config.middleware.use OmniAuth::Builder do
provider :facebook, "APP_ID", "APP_SECRET", :scope => 'user_birthday'
end
For reference, I've removed the line config.omniauth :facebook, "APP_ID", "APP_SECRET" from config/initializers/devise.rb
I spent hours trying to get this to work but had to give up eventually. Then this morning I ran it again and it worked. Overjoyed, I tried to add another parameter to :scope but now the whole thing is broken again. I can get it to work if I remove the :scope but when I put it back in it fails every time (even if it's just :scope => 'user_birthday' like I had working first thing this morning).
To locate the problem, I put debug code in omniauth_callbacks_controller.rb and it now looks like:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
# You need to implement the method below in your model (e.g. app/models/user.rb)
#user = User.from_omniauth(request.env["omniauth.auth"])
puts "start before persist debug"
puts #user.birthday
puts #user.persisted?
puts "end before persist debug"
if #user.persisted?
puts "Start persisted debug"
puts request.env["omniauth.auth"]
puts "End debug"
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
puts "Start unpersisted debug"
puts request.env["omniauth.auth"]
puts "End debug"
redirect_to new_user_registration_url
end
end
end
This debug clearly shows that facebook is returning the necessary information but the app is failing because .persisted? is returning false and so I get re-directed to the new_user_registration page which returns the following:-
NoMethodError in Devise::Registrations#new
Showing /home/action/workspace/cloudapp/app/views/devise/shared/_links.html.erb where line #23 raised:
undefined method `omniauth_authorize_path' for #<#<Class:0x007f3aeffff038>:0x007f3aefffdf08>
I can't for the life of me figure out why .persisted? is returning false. I'm using Nitrous.io for development with a Heroku postgresql database. I've confirmed there are no users in the database by running
rails c
User.all
This returns:
User Load (89.4ms) SELECT "users".* FROM "users"
=> #<ActiveRecord::Relation []>
I have a feeling the problem is in models/user.rb but I can't figure out how to debug it to see if it's finding a user and therefore not persisting or trying to create one and failing. Does anyone know a simple way to debug this?
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.info.email
user.password = Devise.friendly_token[0,20]
user.name = auth.info.name # assuming the user model has a name
user.birthday = auth.extra.raw_info.birthday
# user.image = auth.info.image # assuming the user model has an image
end
end
I've gone over everything about 50 times and am close to giving up.
The only thing I can think of is that where(provider: auth.provider, uid: auth.uid) is returning something (which it shouldn't because my database is empty). Would there possibly be an index that exists somewhere outside my database and that's what it's searching?
Please, for my sanity, can anyone help? If you need more info I'll gladly provide it
Edit 1
Just tried the following and it works which make me more confused than ever:
Delete the app from my facebook account as I'm testing using that account
Try to log in with facebook with :scope => 'user_birthday' left in. Facebook lists the permissions sought as your public profile and birthday. Accept and get sent back to my site which fails as per above (even though the info is definitely being sent back)
Remove :scope => 'user_birthday' and try log in using facebook again. Get directed to facebook which lists permission sought as your public profile and email address. Accept and get directed back to site which now works and also has the user birthday stored and accessible because I had the permisision from facebook from number 2 above.
I'm completely at a loss now
To find out about why is the object not being saved. You need to puts the errors.
puts #user.errors.to_a
And to check the content of the auth
puts request.env["omniauth.auth"]
I had the same problem and follow the answer above and I put "#user.errors.to_yaml" on my code to I see where was the error and I found.
I am using "devise" and "omniauth-facebook" too. The default scope of the omniauth-facebook is "email". However, I put on the scope the properties: "user_about_me, user_status, user_location, user_birthday, user_photos". I need to add "EMAIL" on the scope to devise to use on creation of the 'user'. I discover this when I saw my error: "email don't be blank".
Summary:
If you insert properties on the scope, ALWAYS put "email" too.
Facebook not always returning email for user
from facebook developers https://developers.facebook.com/bugs/298946933534016
Some possible reasons:
No Email address on account
No confirmed email address on account
No verified email address on account
User entered a security checkpoint which required them to reconfirm
their email address and they have not yet done so
Users's email address is unreachable
You also need the 'email' extended permission, even for users who have a valid, confirmed, reachable email address on file.
User entered a security checkpoint which required them to reconfirm their email address and they have not yet done so
Users's email address is unreachable
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
puts request.env["omniauth.auth"] # check if request.env["omniauth.auth"] is provided an email
if request.env["omniauth.auth"].info.email.present?
#user = User.from_omniauth(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Facebook") if is_navigational_format?
else
session["devise.facebook_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
else
redirect_to new_user_registration_url, notice: "Can't sign in to your #{request.env["omniauth.auth"].provider} account. Try to sign up."
end
end
end
My User.rb:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,:confirmable,:token_authenticatable,
:recoverable, :rememberable, :trackable, :validatable, :authentication_keys => [:name]
My routes:
devise_for :users, :controllers => { :sessions => "sessions", :confirmations => "confirmations", :passwords => "passwords", :registrations => "registrations" }
My ConfirmationsController is a standard controller but with different redirect.
I have link on my email like:
/users/confirmation?confirmation_token=167bad44a15e02b0bd570b51e1bf927b88368d8855d92b9833a24017a2bad4be
In database user has
confirmation_token:167bad44a15e02b0bd570b51e1bf927b88368d8855d92b9833a24017a2bad4be
But when i click on that link i only see page with:
Resend confirmation instructions
Confirmation token is invalid
What i dont do - what else i have to set.
CONFIRMATIONCONTROLLER:
def resource_params
params.require(:user).permit(:confirmation_token)
end
private :resource_params
def show
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
if resource.errors.empty?
set_flash_message(:notice, :confirmed) if is_navigational_format?
sign_in(resource_name, resource)
session['new_user'] = true
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
else
respond_with_navigational(resource.errors, :status => :unprocessable_entity){ render :new }
end
end
protected
# The path used after resending confirmation instructions.
def after_resending_confirmation_instructions_path_for(resource_name)
new_registration_path(resource_name)
end
I say "standard controller" because when i remove it and do not use custom controller problem is that same.
Which version of devise are you using? If you're on 3.1.0 or higher, this behavior is expected:
CHANGELOG.md
The tokens that are stored in the database are not supposed to match the tokens that you send in the confirmation e-mails. See devise/lib/devise/models/confirmable.rb, which now contains the following:
def confirm_by_token(confirmation_token)
original_token = confirmation_token
confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
As you can see, the token that you pass in via query string params is consumed by the Devise.token_generator, and the result of that operation is what's compared with the token in the database to discover the user record.
It looks like it's temporarily possible (in 3.1 but not 3.2) to turn this off by setting
config.allow_insecure_token_lookup = true
in your devise initializer. But the default behavior has been changed to make devise more secure. See this blog post for a complete rundown of the security improvements in devise 3.1, including this change.
You can use the solution below (setting config.allow_insecure_token_lookup = true) but, according to the Devise changelog, this will be only available temporarily.
The problem likely arose because you ran the devise generator to dump all of their views into yours back before they made these changes. Then you updated your Devise gem and all the back end stuff changed but your views didn't. Now all your devise-related views and mailers are out of date and won't work with the new token styles.
You can see the new emails at: https://github.com/plataformatec/devise/tree/master/app/views/devise/mailer. The major change is changing this line:
<p><%= link_to 'Confirm my account', confirmation_url(#resource, :confirmation_token => #resource.confirmation_token) %></p>
to
<p><%= link_to 'Confirm my account', confirmation_url(#resource, confirmation_token: #token) %></p>
The main difference is #resource.confirmation_token becomes just #token.
I change #resource.confirmation_token to #token then it works.
In a recent project, facebook Users can login using their Facebook UID to upload picture submissions based on file uploads or uploads from their personal albums etc.
Everything works quite nice on my local system in the development environment. Login via Facebook, Logout, Upload - all great.
In production though I'm facing a unknown and hard to debug problem. It seems that every once in a while (actually reproducable when uploading a new Submission to the system) the session is lost, the picture is NOT uploaded and the facebook user is logged out (!).
I'm using devise and omniauth. Omniauth is integrated into Devise.
Following is all the code that touches Devise/Omniauth or the User.
app/models/user.rb
class User < ActiveRecord::Base
devise :omniauthable, :rememberable, :omniauth_providers => [:facebook]
def self.create_with_omniauth(auth)
u = User.find_by_uid(auth["uid"])
return u unless u.nil?
create! do |user|
user.provider = auth["provider"]
user.uid = auth["uid"]
user.name = auth["user_info"]["name"]
user.email = auth['user_info']['email']
end
end
def after_signin_path
'/competition'
end
end
Database contains all needed fields for :rememberable, I hope.
app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
# You need to implement the method below in your model
#user = User.create_with_omniauth(env["omniauth.auth"])
if #user.persisted?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
#user.update_attributes!(:current_auth_token => env["omniauth.auth"]['credentials']['token'], :last_language => I18n.locale.to_s, :updated_at => Time.now, :remember_created_at => Time.now)
sign_in_and_redirect(:user, #user)
else
redirect_to '/competition'
end
end
protected
def after_omniauth_failure_path_for resource
'/competition'
end
end
config/initializers/devise.rb
OmniAuth.config.full_host = "http://#{APP_CONFIG[:domain]}"
Devise.setup do |config|
config.mailer_sender = "devise#myapp.host.com"
require 'devise/orm/active_record'
config.stretches = 10
config.encryptor = :bcrypt
config.timeout_in = 3.days
config.pepper = "2a4b8b2ed9e12e553a7a542176f2ace1af62c062f3ba203a590b8b6307f33042b394922807a840004a3dcdf1c4e97ae085fe2c29654ddaeab7c60f431a8078abb"
config.omniauth :facebook, APP_CONFIG[:facebook_app_id], APP_CONFIG[:facebook_app_secret], {
:scope => "email,user_photos,user_photos,publish_stream,offline_access",
:client_options => {
:ssl => {
:ca_file => "/etc/pki/tls/certs/ca-bundle.crt"
}
}
}
end
There are no auth-related methods in application_controller.rb.
routes.rb:
The interesting part below:
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
match '/logout_fb' => 'start#logoutfb'
authenticate :user do
get '/users/connect/:network', :to => redirect("/users/auth/%{network}")
end
Somehow I cannot get to understand the authenticate block, which according to another post should be helpful.. ideas on this too?
So many theories:
One is that the facebook function in the omniauth_callbacks_controller runs aside of the users' session, and hence sign_in_and_redirect won't work. So I had the idea of redirecting to another page like '/auth?uid=xxx' but this sounds both wrong, insecure and not stable.
Any help or hints are appreciated!
A bit of a long shot but try turning off protect_from_forgery - I had some issues with sessions disappearing and it turned out to be the issue discussed here https://github.com/intridea/omniauth/issues/203
In my config/initializers/omniauth.rb, I had to add the following:
OmniAuth.config.full_host = "http://yourdomain.com" # Or have an environment specific URL.
You are using devise but you are not using it's own helpers. For instance, you've defined your own current_user method. To be honest, I can't see any obvious mistakes you've made, so it's just a desperate tip.
what kind of a session store do you use locally and what in production?
When you say "facebook user is logged out", this user is still logged in to facebook, but lost his session at yourapp.com ?
Are you sure that user.id is never nil or that you anywhere else than in .destroy set session[:user_id]= some_nil_variable ?
I'm using omniauth and devise to log users in with their facebook acounts, everything works however when I try to pull their email out of the hash I get this error:
NoMethodError in AuthenticationsController#create
undefined method `id' for "/":String
Here's the full log of the error : http://pastie.org/1698569
The error goes away and it lets me log in just fine after I refresh!
EDIT: It turns out that its line 22 in my authentications controller
sign_in_and_redirect_to(:user, root_path)
For some reason after running this method I can't sign_in the :user
def apply_facebook(omniauth)
if (extra = omniauth['extra']['user_hash'] rescue false)
self.email = (extra['email'] rescue '')
end
end
However, if I don't run that method, then it can sign_in_and_redirect_to just fine
Here's my controllers/model http://pastie.org/1698453
Really appreciate any help
You can't use root_path as the second parameter for sign_in_and_redirect . Here are some available ways you can use it:
sign_in_and_redirect :user, #user # sign_in(scope, resource)
sign_in_and_redirect #user # sign_in(resource)
sign_in_and_redirect #user, :event => :authentication # sign_in(resource, options)
sign_in_and_redirect #user, :bypass => true # sign_in(resource, options)
Since your second parameter isn't either a resource or options (it's a string), you're getting an error. You need to change it to:
sign_in_and_redirect(:user, user) # based on your pastie
If you want to customize the return path to force it to go to a different URL after sign-in, you can do something like this in your ApplicationController:
def after_sign_in_path_for(resource)
"/go/to/this/path"
end