Devise/OmniAuth: different logic for for registration and login - ruby-on-rails

The default devise authorization path (i.e. user_omniauth_authorize_path mentioned here) appears to be designed to work for both OmniAuth registration and login. By the time auth is received in the OmniauthCallbacksController, your rails app typically uses the auth information to create a new user or update an existing user regardless of whether the user intended to login or register with a service (i.e. Facebook).
My stakeholders have requested different behavior for auth and login. If a user clicks "Register with Facebook" and there's no account with their Facebook email, they want to go ahead and create an account with that email. However, if a user clicks "Login with Facebook" and there's no account with their Facebook email, they want to present the user with an error message explaining "The Facebook account you are using does not match an account in our records. Please sign in using your barre3 email and password"
The place for this logic seems to be in the OmniauthCallbacksController's Facebook method. What's the cleanest way to pass the user's intent ('login' or 'register') into this callback method?

You can get back parameters from the authorization to indicate the user's intent by adding some parameters on the authorization URL.
For example:
- if devise_mapping.omniauthable?
- resource_class.omniauth_providers.each do |provider|
= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider, {intent: :sign_in})
(This creates a URL like: http://whatevs.dev/users/auth/facebook?intent=sign_in)
Then, in the callbacks controller (whatever you name it):
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
if env["omniauth.params"]["intent"] == "sign_in"
# Don't create a new identity
else
# Normal flow, such as:
#user = User.from_omniauth(request.env["omniauth.auth"])
sign_in_and_redirect #user
end
end
end

If a user clicks "Register with Facebook" and there's no account with their Facebook email, they want to go ahead and create an account with that email
This assumption is not valid since Facebook can be created with just a phone number. Even user has an email, extra permission is required to get the user email from Facebook.
Your application should validate facebook_uid returned by Facebook API instead of email.
What's the cleanest way to pass the user's intent ('login' or 'register') into this callback method?
For OmniAuth there is no difference between the 'login' or 'register'. All it does is try to authenticate the user with the provided Facebook token.
One clean way to differentiate is to separate on the controller level. If user tries to login in, call SessionsController#create, if user tries to sign up, call UsersController#create.

I have also researched too much for the answer but didn't get any satisfactory answer so I took a step forward and create functionality to work as required. #Jason answer helps me to reach here.
views/users/sessions/new.html.erb
<% if devise_mapping.omniauthable? %>
<% resource_class.omniauth_providers.each do |provider| %>
<%= link_to omniauth_authorize_path(resource_name, provider, {intent: :sign_in}) do %>
<i class="fa fa-<%= provider.to_s.split('_')[0] %>"></i>
<% end %>
<% end %>
<% end %>
views/users/registrations/new.html.erb
<% if devise_mapping.omniauthable? %>
<% resource_class.omniauth_providers.each do |provider| %>
<%= link_to omniauth_authorize_path(resource_name, provider, {intent: :sign_up}) do %>
<i class="fa fa-<%= provider.to_s.split('_')[0] %>"></i>
<% end %>
<% end %>
<% end %>
controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider)
class_eval %Q{
def #{provider}
if request.env["omniauth.params"]["intent"] == "sign_up"
#user = User.from_omniauth_sign_up(request.env['omniauth.auth'])
elsif request.env["omniauth.params"]["intent"] == "sign_in"
#user = User.from_omniauth_sign_in(request.env['omniauth.auth'])
end
if #user.present?
sign_in_and_redirect #user, event: :authentication
set_flash_message(:notice, :success, kind: "#{provider}".capitalize) if is_navigational_format?
else
flash[:errors] = {:email => "is not registered with us."}
session["devise.#{provider}_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
}
end
[:google_oauth2, :facebook, :linkedin].each do |provider|
provides_callback_for provider
end
end
models/user.rb
def self.from_omniauth_sign_up(auth)
# check if email address obtained from auth server is in User table
user = User.where(email: auth[:info][:email]).first
if user.present?
user.first_name = auth.info.first_name
user.last_name = auth.info.last_name
user.image = auth.info.image
user.provider = auth.provider
user.uid = auth.uid
user.save
else
user = User.new(provider: auth.provider, uid: auth.uid)
user.email = auth.info.email
user.username = "#{auth.uid}-#{auth.provider}"
user.password = Devise.friendly_token[0,20]
user.first_name = auth.info.first_name
user.last_name = auth.info.last_name
user.image = auth.info.image
user.status_id = 2
user.skip_mobile_validation = true
user.skip_confirmation!
user.save
end
user
end
def self.from_omniauth_sign_in(auth)
if user = User.where(email: auth[:info][:email]).first
user
end
end

Related

Login a User via email - Devise

Within my application a User is created after a successful transaction
def create_real_user
return unless current_or_guest_user.is_guest?
generated_password = Devise.friendly_token.first(8)
#user = User.new(
is_guest: false,
first_name: params[:first_name],
last_name: params[:last_name],
email: params[:email],
password: generated_password,
password_confirmation: generated_password,
braintree_id: #result.transaction.customer_details.id
)
#user.save(validate: false)
RegistrationMailer.welcome(#user, generated_password).deliver_now
end
And as you can see an email is sent out and it advises that a password has been set for them but if they wish to change it then visit a link
Your current password is <%= #password %> but if you would like to change it then please visit <%= link_to 'Change my password', edit_user_registration_path(#user.id) %>
So when clicking this i get to the login screen but i would like to have the user automatically signed in and taken straight to the page where they can edit their password.
How would i go about this?
Thanks
There isn't a quick fix for this but it's possible.
Include a route for this special login case...
resources :users do
member do
get 'token_link'
end
end
This gives you a new helper method `token_link_user_path
You would need to create a token field in your User table when you create the record, and set it to some random value via a before_create
class User
before_create :generate_token
def generate_token
user.token = SecureRandom.urlsafe_base64(nil, false)
end
...
end
In your email include the link...
link_to 'access your account', token_link_user_path(#user.token)
In your User controller...
def token_link
#user = User.find_by(token: params[:id])
#user = nil if #user && #user.created_at < Time.now - 1.day
unless #user
flash[:error] = 'Invalid log in'
redirect_to root_path
end
#user.update_attribute(:token, nil)
... do here any processing, renders, or redirects you'd like
end
Note how we wipe out the token_link after it's been used, to prevent it from beign used twice. Along the same lines, we check that it's not older than a day since the record's been created.

registration of new users with Devise and omniauth-google-oauth2

I've trying to manage user sign up with google account for my rails 4.0.0 app. Devise works perfectly. And there is working sign in with Google Account for existing users. But I have some difficulties with new user registration using Google Oauth 2. For example: i've got google account "example#google.com". It's logged in on my current PC. And when I try to sign up with this account to my app it generates blank register form. If I dont manually provide email, login, full name, etc. - I've got error message that they "cannot be blank". I guess solution is create default value to text fields to fetch user details.
So, my question is how can I provide values for variables in view that equals variables from google account?
Email field in form_for in new user registration:
= f.email_field :email, :autofocus => true, :value => 'how can i put auth.info.email here?'
omniauth_callbacks_controller.rb:
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
flash.notice = "Signed in Through Google!"
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
flash.notice = "You are almost Done! Please provide a password to finish setting up your account"
redirect_to new_user_registration_url
end
end
end
omniauth method from user model:
def self.from_omniauth(auth)
if user = User.find_by_email(auth.info.email)
user.provider = auth.provider
user.uid = auth.uid
user
else
where(auth.slice(:provider, :uid)).first_or_create do |user|
user.provider = auth.provider
user.uid = auth.uid
user.full_name = auth.info.name
user.email = auth.info.email # THIS (user.email) value i want to provide to my registration form as default value
user.birthday = auth.info.birthday
user.avatar = auth.info.image
end
end
end
I had the same problem with GitHub you can take a look at my user model
https://github.com/flower-pot/pastebin/blob/master/app/models/user.rb

NoMethodError in SessionsController#create using Omniauth Facebook authentication railscast 360

Hi there I am currently following railscast 360 on adding Facebook authentication to my webapp. It was all going well until I started getting this error:
"NoMethodError in SessionsController#create
undefined method `name=' for #"
app/models/user.rb:6:in block in from_omniauth'
app/models/user.rb:3:intap'
app/models/user.rb:3:in from_omniauth'
app/controllers/sessions_controller.rb:3:increate'
I have looked at a few different answers on this website but a lot of them seem to be for the Twitter authentication I am just trying to get the Facebook working.
I have created an app on the Facebook Developers and followed the tutorial for the rail cast fully. I would appreciate any help with this greatly.
My code so far is
user.rb:
class User < ActiveRecord::Base
def self.from_omniauth(auth)
where(auth.slice(:provider, :uid)).first_or_initialize.tap do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name
user.oauth_token = auth.credentials.token
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.save!
end
end
end
application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
private
def current_user
#current_user ||= User.find(session[:user_id]) if session[:user_id]
rescue ActiveRecord::RecordNotFound
end
helper_method :current_user
end
session_controller.rb
class SessionsController < ApplicationController
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
def destroy
session[:user_id] = nil
redirect_to root_url
end
end
This is my code inside my application.html.erb:
<div id="user_nav">
<% if current_user %>
Signed in as <strong><%= current_user.name %></strong>!
<%= link_to "Sign out", signout_path, id: "sign_out" %>
<% else %>
<%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %>
<% end %>
Would really appreciate any help.
Thanks
Going off of your call stack trace (it is in reverse order of calls), it seems like your session_controller calls user model, and in particular the user block.
Looking at your code, no error stands out, but the stack trace is flagging line 6 and mentions that it does not understand the method: undefined method 'name='. If I had to take a guess, it is likely that there is no name column in the table - this may not solve your issue, but try the following:
$ rake db:migrate
and
$ rails c
Then in the rails console, try checking the fields of the User object.
> User.new
Hopefully you'll know whether name is listed in the object or not.

Editing Users With Devise and Omniauth

I'm working through the Railscast on implementing Devise and OmniAuth (along with the Devise documentation) -- currently, I've got a site going where visitors can sign up using their facebook accounts or by filling out a form.
I'm running into trouble when users that sign up via OmniAuth try to edit their profiles, though. Devise looks for the user's current password when they submit changes to their profiles, but those that logged in with facebook don't know their passwords (they're set automatically in the user model):
def self.find_for_facebook_oauth(auth, signed_in_resource=nil)
user = User.where(:provider => auth.provider, :uid => auth.uid).first
unless user
user = User.create(first_name:auth.extra.raw_info.first_name,
last_name:auth.extra.raw_info.last_name,
provider:auth.provider,
uid:auth.uid,
email:auth.info.email,
password:Devise.friendly_token[0,20]
)
end
user
end
When a user edits his information, the app should not require password confirmation if he set up his account through OmniAuth. The tutorial suggests that the handy password_required? method will help me achieve this outcome. Specifically, adding this method to the user model means that it should only return true if the user didn't sign up through OmniAuth (the provider attribute would be nil in that case):
def password_required?
super && provider.blank?
end
Thus, a piece of code like:
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %>
<%= devise_error_messages! %>
<%= render :partial => "essential_user_info_inputs", :locals => { :f => f } %>
<%= render :partial => "inessential_user_info_inputs", :locals => { :f => f } %>
<% if f.object.password_required? %>
<%= render :partial => "password_inputs", :locals => { :f => f } %>
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password %>
<% end %>
<%= f.submit "Update" %>
<% end %>
would theoretically only display password inputs when needed. It also suggests that Devise has built in logic saying that OmniAuth users don't need to use passwords to edit their accounts. I have no idea if this is true, but the tutorial kind of makes it look like that. But when an OmniAuth user tries to edit his account, I get "Current password can't be blank." Same thing with non-OmniAuth users (this makes sense, since the password fields don't show up on those users' edit pages either).
Some poking around confirms that the password_required? method is returning false, both when the user signed up through OmniAuth and through the site's regular user signup. Even when I change it to simply run the superclass method, it returns false.
Any ideas of what's going on with the password_required method? I can't find anything about it anywhere, but I feel like that's what's tripping things up right now.
Update:
This is now working, but not using the method outlined in the Railscast, which relies on requires_password? method, a topic that I still know nothing about. Instead, I implemented the solution outlined here, as suggested here. So I am now only requiring passwords to update non-OmniAuth accounts with the code:
class Users::RegistrationsController < Devise::RegistrationsController
def update
#user = User.find(current_user.id)
email_changed = #user.email != params[:user][:email]
is_facebook_account = !#user.provider.blank?
successfully_updated = if !is_facebook_account
#user.update_with_password(params[:user])
else
#user.update_without_password(params[:user])
end
if successfully_updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
redirect_to root_path
else
render "edit"
end
end
end
The easiest way is to overwrite the update_resource method in your RegistrationsController. This is advised by devise in their own implementation of the controller:
# By default we want to require a password checks on update.
# You can overwrite this method in your own RegistrationsController.
def update_resource(resource, params)
resource.update_with_password(params)
end
So the solution is to overwrite this method in your own controller like this:
class Users::RegistrationsController < Devise::RegistrationsController
# Overwrite update_resource to let users to update their user without giving their password
def update_resource(resource, params)
if current_user.provider == "facebook"
params.delete("current_password")
resource.update_without_password(params)
else
resource.update_with_password(params)
end
end
end
I've added an update to the link below that includes my solution to the Devise/ OmniAuth change user profile/password issue and collected some helpful links:
stackoverflow - Allowing users to edit accounts without saving passwords in devise
I saw this used somewhere.
def update
params[:user].delete(:current_password)
params[:user].delete(:password)
params[:user].delete(:password_confirmation)
if current_user.update_without_password(params[:user])
redirect_to somewhere_wicked_path, notice => "You rock"
else
render 'edit', :alert => 'you roll'
end
end
use something like this in your update method in your controller. Pretty sure that method is in Devise too.

How to let user activate account through devise confirmations email link locally

I am trying to confirm user accounts in test mode while using devise and rails. Here's the scenario. I changed the devise confirmation email that is sent into a partial that is rendered after a user has successfully signed up. Problem is it shows the following error "undefined method email". Here's my confirmation partial.Welcome <%= #resource.email %>!
You can confirm your account through the link below:
<%= link_to 'Confirm my account', confirmation_url(#resource, :confirmation_token => #resource.confirmation_token) %>. And below is my application_helper in order to access the resource details. def resource_name
:user
end
def resource
#resource ||= User.new
end
def devise_mapping
#devise_mapping ||= Devise.mappings[:user]
end</pre>
Is #resource actually the user in application_helper? I know if it is nil it will be assigned a new User, but what is it before it assigns it?
I suspect #resource isn't a User, but something else (a something that doesn't have an email field).

Resources