show view before validation OmniAuth - ruby-on-rails

I want to create a login for social networks, for example, Twitter, Facebook, etc.
But there are social networks like twitter that do not give me the email.
How can I do so before saving the user to display a form view where to add the email and then save all data of Twitter + email?

I will assume that you know how to setup omniauth, so I will skip straight to the relevant parts.
In the callback, if user is found for specified uid and provider, you sign him in and redirect. Otherwise, you need to store omniauth payload in the session, and redirect to a form where user can add an email:
def twitter
if User.find_by(provider: auth.provider, uid: auth.uid)
# sign in user and redirect
else
session[:omniauth] ||= {}
session[:omniauth][auth.uid] = auth
redirect_to new_omniauth_email_request_path(ominauth_uid: auth.uid)
end
end
private
def auth
request.env['omniauth.auth']
end
When you add this to routes.rb:
resources :omniauth, only: [], param: :uid do
resource :email_request, only: [:new, :create]
end
You will have these two new routes available:
omniauth_email_request POST /omniauth/:omniauth_uid/email_request(.:format) email_requests#create
new_omniauth_email_request GET /omniauth/:omniauth_uid/email_request/new(.:format) email_requests#new
You need to create EmailRequestsController and implement new and create actions. Don't forget to clear the session after the user is successfully created:
def create
# ...
#user = User.create do |user|
user.email = params['email']
user.uid = params['omniauth_uid']
user.name = session[:omniauth][user.uid].name
# assign whatever else you may need from session[:omniauth][user.uid]
end
session[:omniauth].delete #user.uid if #user.persisted?
# ...
end
Please note that if you plan to support multiple omniauth providers, that on the "middle step" the user may provide an email that already exists in the database (for example the user has already authenticated via some other omniauth that passed the email). This case needs to be handled.

Related

Devise ignoring customized create method

Users have to confirm their email account via a link sent to them after registration through devise's confirmable.
Right now if a user who has not confirmed their account gets the unhelpful error:
Your email or password is incorrect.
I have tried adding the below to the SessionsController:
def create
user = User.where(email: params[:user][:email]).take
unless user.confirmed?
super
end
flash[:alert] = "Your account has not been confirmed."
end
My routes are
devise_for :users, path: 'users', controllers: {
sessions: "users/sessions"
}
However, signing in as any user (confirmed or not), seems to bypass this entire method. (Even if I put in binding.pry.)
UPDATE:
Devise only ignores the unique create method. If I put in a binding.pry in any other method (new or delete), devise will catch it.
The sessions create doesn't get called if the user isn't confirmed, so this is why you're not able to reach the create method.
Try this
class User < ActiveRecord
protected
def confirmation_required?
false
end
end
This will allow the user to proceed to the create action in sessions_controller and you can test there if the user is actually confirmed.

Create password and authentication for existing users

I have rails app with:
Admin table with Devise authentication
User table with email and name without authentication (but session to remember them)
User can browse anywhere but now on certain pages I would like to enhance it and add authentication - allow user to create password and only with password it will be accessible but I am quite lost what is the best way to do it with the current setting?
I allow users to add their details like name and email and I am creating a cookie to remember them without any authentication or password:
UsersController
def create
user = User.find_or_create_by(email: params[:user][:email])
cookies.permanent.signed[:user_id] = user.id
session[:user_id] = user.id # for users/edit temporary
render json: user
end
Let's say I have this following method in User:
before_filter :authenticate_user!, only: :your_order
def your_order
end
If User will visit this page and didn't set up password before, how can I prompt him to create one and how can I require for him to login after with Devise? I am thinking of more solutions but none of them are perfect.
As per the specifications given the below mentioned criteria might help you.
def your_order #before_filter
if user.password.present?
# authenticate using valid_password? method of devise
else
#redirect user to say set_password
end
end
def set_password
#set the user password in this method and after successful completion redirect to login page where before filter your_order will be called
end

How to use Google Oauth2 for both signing in users and creating new user accounts in a rails app?

I'm working on google authentication for a rails app. Currently using the omniauth-google-oauth2 gem to implement Google auth. I've managed to have users sign in using google. However, I'd also like users to be able to sign up using google. My problem is that I've matched the google callback URL to a particular controller action (sessions#create).
Is it possible to choose between 2 redirect URIs based on whether users are signing in or signing up? Currently, my only idea is to create new google client credentials to be used for sign up, I hope there is a better way.
You don't need to have 2 redirect uris, you just need to do some more work when receiving the callback. For instance:
class SessionsController < ApplicationController
...
def create
email = auth_hash['info']['email'] # assuming your omniauth hash is auth_hash and you're requiring the email scope
#user = User.find_by(email: email) if !email.blank? # assuming your user model is User
if #user
login_user(#user) # use your login method
elsif !email.blank?
#user = User.new(name: auth_hash['info']['name'], email: email)
unless #user.save!(validate: false) # validate false because I'm enforcing passwords on devise - hence I need to allow passwordless register here)
# deal with error on saving
end
else
# deal with no found user and no email
end
end
protected
def auth_hash
request.env['omniauth.auth']
end
end
I've written all steps but the creation process can be shortened to:
#user = User.create_with(name: auth_hash['info']['name']).find_or_initialize_by(email: email)
#user.save! if #user.new_record?
if #user
login_user(#user)
else
# deal with no user
end
Nonetheless, you can't be sure the user is going to give you scope access to the email, so personally I think the first version, even if a bit lengthier is more robust. Then on the shorter version there's also the problem of, if #user is false, why is so? And will require you to add more logic to figure out why is that, whereas in the first one it's much easier to apply the correct response to each situation.

Rails: How to authenticate user on mobile native with Devise+Omni-facebook+Devise_token_auth after obtaining the access token

How do I use facebook access token on Devise + Omniauth-facebook + Devise_token_auth?
Is there a way to hook the access token into the omniauth-facebook somehow and get the user signed up/logged in? Or any other solution is welcome.
I am able to obtain the access token from facebook graph API on my mobile side.
I've tried to place the access token on the callback url as such:
/omniauth/facebook/callback?code=ACCESS_TOKEN
but it simply returns
{"errors":["Use POST /sign_in to sign in. GET is not supported."]}
I have looked at:
Rails API: Authenticate users from native mobile apps using username/password or facebook token
Suggested solution to look at Warden, but I don't think it needs to be so complicated.
omniauth for rails and ios authentication
Suggested solution is a gem, but the gem is no longer maintained.
Rails Devise OmniAuth Facebook Login from iOS
Suggested solution affected mobile side development, which I'm hoping not to.
I decided to try something and it seems to work.
I've taken codes from #JeremyMoyers here. It's really an ad-hoc solution but it seems to do the trick.
I've overwritten DeviseTokenAuth controller action for omniauth_success:
Routes:
config/routes.rb
namespace :api do
scope :v1 do
# mount_devise_token_auth_for 'User', at: 'auth'
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
omniauth_callbacks: 'omniauth_callbacks',
}
end
end
Controller:
app/controllers/omniauth_callbacks_controller.rb
class OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
def omniauth_success
# get_resource_from_auth_hash
# Remove original get_resource_from_auth_hash and implement custom get_resource
get_resource_from_uhash(params[:access_token],params[:expires_in])
create_token_info
set_token_on_resource
create_auth_params
# Skip confirmation is done at custom get_resource
# if resource_class.devise_modules.include?(:confirmable)
# don't send confirmation email!!!
# #resource.skip_confirmation!
# end
sign_in(:user, #resource, store: false, bypass: false)
if #resource.save!
update_auth_header
yield #resource if block_given?
redirect_to #resource
# render_data_or_redirect('deliverCredentials', #auth_params.as_json, #resource.as_json)
else
redirect_to rooth_path
end
end
protected
def get_resource_from_uhash(access_token, expires_in)
graph = Koala::Facebook::API.new(access_token)
profile = graph.get_object('me?fields=email,name,id,picture')
# logger.info(profile)
#resource = User.where({
uid: profile['id'],
provider: 'facebook'
}).first_or_initialize do |user|
user.provider = 'facebook'
user.uid = profile['id']
user.name = profile['name']
user.oauth_token = access_token
user.password = Devise.friendly_token[0,20]
user.image = profile['picture'] if profile['picture'].present?
user.oauth_expires_at = Time.at(expires_in)
user.skip_confirmation!
user.save!
end
if #resource.new_record?
#oauth_registration = true
set_random_password
end
#resource
end
end
Let me know if there are any security concerns for this.

Devise/Omniauth - How to deal with a Provider that doesn't include an email

I have a Rails app that uses Devise and Omniauth to handle authentications. As prescribed I am receiving callbacks from the providers in my omniauth_callbacks_controller, where I check if the authentication already exists, and whether a user with the email given by the provider already exists, creating a new user if necessary.
I need a valid email for each user. My problem comes with callbacks from Twitter. Twitter doesn't provide an email for its users, so I can't create a valid user. To solve this I store the provider's data in a session and I send the user the new registrations page, asking them to submit their email address so I can create a valid user. When this form is submitted I hit a problem. The form has created a new user, but it is perfectly possible that a user with that email already exists (in which case I would need to add the authentication to that user).
At the moment I am checking to see if a user with the same email as the new user already exists. If so, I am ignoring the new user and applying the authentication to the user that already exists. However this feels really hacky.
How should I be doing this?
class Users::RegistrationsController < Devise::RegistrationsController
def build_resource(*args)
super
if session[:omniauth]
#If a user with this email already exists then use them instead
existing_user = User.find_by_email(#user.email)
if(existing_user)
existing_user.email = #user.email
#user = existing_user
end
#If there is a session available, we know it contains data for adding an authentication
#user.apply_omniauth_data_as_authentication(session[:omniauth])
#Ensure validations are passed on to next page
#user.valid?
end
end
Here you go. I'll try and remember where I got the fix from and post a link if I do:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
# This controller handles callbacks from Omniauth providers. If a user already exists with this provider we sign them in.
# Otherwise we either add an authentication to the current user, or create a new user if one doesn't exist.
def facebook
authorize
end
def twitter
authorize
end
private
def authorize
omniauth_data = request.env["omniauth.auth"]
#Check to see if we have an authentication for this provider already stored for any User
authentication = Authentication.find_by_provider_and_uid(omniauth_data['provider'], omniauth_data['uid'])
#If an authentication already exists, sign the owning User in
if authentication
flash[:notice] = "Signed in successfully with " + omniauth_data['provider'].titleize
sign_in_and_redirect(:user, authentication.user)
#Otherwise if there is a current User, add a new authentication for them
elsif current_user
current_user.authentications.create(:provider => omniauth_data['provider'], :uid => omniauth_data['uid'], :token => omniauth_data['token'])
flash[:notice] = "Authentication successful"
redirect_to user_profile_url
#Otherwise we check if a user exists with the email address provided by the provider (if provided at all))
else
email = omniauth_data['info']['email']
user = User.find_by_email(email)
#If no user exists, create a new one
if(!email || !user)
user = User.new
end
user.apply_omniauth_data_as_authentication(omniauth_data)
#If they save successfully (meaning we have enough data from the authorisation) sign them in
if user.email?
#We got all we needed from the provider so we don't need them to confirm'
user.skip_confirmation!
user.save!
flash[:notice] = "Signed in successfully with " + omniauth_data['provider']
sign_in_and_redirect(:user, user)
#Otherwise we need to have the visitor manually submit missing information
else
#save the omniauth data in a session so we can add the authentication once registration is complete
flash[:alert] = "Please complete registration"
session[:omniauth] = omniauth_data.except('extra')
redirect_to new_user_registration_url
end
end
end
end
I like the solution used by this link http://www.orhancanceylan.com/rails-twitter-and-facebook-authentications-with-omniauth-and-devise/
It uses the provider credentials to register in the normal devise process (as well as filling out provider and uid attributes on the user or on a separate table). Thus, when a new user tries to register using a provider, they will be required to put in an email and password, creating less headaches down the road (like wanting to log in the normal way, but not having a password to type in, and then you having to handle that).
While this solution somewhat negates the convenience of registering with social media the FIRST time, it creates long term beneficial effects with minimal effort.
If you absolutely do not want this kind of requirement for fear of losing signups or having your website seem less polished, find another solution and do all the work to handle those downstream cases.

Resources