Create password and authentication for existing users - ruby-on-rails

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

Related

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.

How do I generate a temporary page like confirmation page in rails?

I am using devise and want to redirect users to a confirmation page upon signup, this is what I am doing right now:
users/registrations_controller.html.erb
class Users::RegistrationsController < Devise::RegistrationsController
def confirm_email
end
private
def after_inactive_sign_up_path_for(resource)
users_confirmyouremail_path
end
end
config/routes.rb
devise_scope :user do
get 'users/confirmyouremail' => 'users/registrations#confirm_email'
end
I have no problem with redirecting the page after signup. However, I think it is quite weird that anyone can visit the page with url like `host.com/confirmyouremail' and see the confirmation page. Are there any ways I can write a route that will use random code that is allow only for one time visit? Thanks in advance.
Maybe something like this:
before_action :authenticate_user!
def confirm_mail
redirect_to root_path if current_user.confirmed
...
end
You are storing in the database if the user has already confirmed his account. If his account is confirmed then he won't be able to access this page. You can redirect to whatever page you want. A user without any account won't be able to access this page because of the before action
In case the user is not logged in when he accesses this confirm_mail page you have different possibilities. You could use a session or a cookie:
# after sign up:
session[:confirm] = true
# alternatively a cookie
cookies[:confirm] = true
Then in the confirm mail action:
def confirm_mail
if session[:confirm].blank? # or cookies[:confirm].blank?
redirect_to root_path
end
# otherwise delete the field from the session
session.delete(:confirm)
# alternatively the cookie
cookies.delete(:confirm)
end
Another way would be by using a Token. You create a new model like ConfirmMailToken. Then on sign up you create a new token and redirect the user to the confirm page with the token as a URL param. Then in the confirm_mail action you check if a token is available and delete it if it is. This way you ensure that the page is only shown after redirect.

show view before validation OmniAuth

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.

Prevent ActiveAdmin Logins with Pundit Policy

I have ActiveAdmin and Pundit setup and working. My User model has a role attribute (rails 4.1 enum). How can I only allow a user with the admin role to log into /admin?
Here is another way of doing this, without touching ActiveAdmin::BaseController.
In your AA initializer, add
# config/initializers/active_admin.rb
config.on_unauthorized_access = :user_not_authorized
And implement ApplicationController#user_not_authorized
# app/controllers/application_controller.rb
:
def user_not_authorized(exception)
sign_out # or user will end up in a redirection loop
flash[:error] = "Access has been denied because ..."
redirect_to new_user_session_path
end
Finally, update the policy for AA pages (or dashboards, or whatever)
# app/policies/active_admin/page_policy.rb
module ActiveAdmin
class PagePolicy < ::ActiveAdminPolicy
def show?
User.roles[user.role] >= User.roles['manager']
end
end
end
Done, only managers and above will be able to login.
I was facing the same issue, here is how I solved it:
Roles are defined in User#role.
class User < ActiveRecord::Base
enum role: [ :client, :reseller, :manager, :admin, :super ]
end
Here's how to restrict access to AA's /admin all together for users who are clients or resellers.
First, add a before_filter to AA's BaseController, as described here.
# lib/active_admin/deny_unprivileged/usage.rb
module ActiveAdmin
##
# Only users showing a minimum user level (role)
# should be allowed to access the administration interface.
module DenyUnprivilegedUsage
extend ActiveSupport::Concern
included do
before_filter :deny_unprivileged_usage
end
private
##
# Deny access to /admin, unless user is, at least, a manager.
#
# This does the same as Devise::Controllers::Helpers#sign_out_and_redirect,
# but adds a flash message explaining why access has been denied.
#
# See Devise::Controllers::Helpers#sign_out_and_redirect
def deny_unprivileged_usage
if current_user and not User.roles[current_user.role] >= User.roles['manager']
resource_or_scope = current_user
scope = ::Devise::Mapping.find_scope!(resource_or_scope)
redirect_path = new_user_session_path
::Devise.sign_out_all_scopes ? sign_out : sign_out(scope)
flash[:error] = "!!!"
redirect_to redirect_path
end
end
end
end
Create an initializer.
# config/initializers/active_admin_extensions.rb
require 'active_admin/deny_unprivileged_usage'
ActiveAdmin::BaseController.send(:include, ActiveAdmin::DenyUnprivilegedUsage)
Restart your server.
Now, a user who is not at least a manager will be denied access to /admin. The user will actually login successfully, however, after login, the filter defined above will sign out the user right away and redirect back to the login page.
The deny_unprivileged_usage method is almost a copy of Devise::Controllers::Helpers#sign_out_and_redirect - except that it adds a flash message, explaining to the user why login has been denied. Without this change, the user would only get back to the login page, not knowing why login has been denied.
Update
On second thought, if a user is registered, it is hard to imagine that they should never be able to login.
Thus, maybe it would be cleaner to implement role-based access restrictions using different authorization adapters and policies for different namespaces. As described here.
Then, /reseller would be able to login and, say, change their password, while /manager would be presented with much more functionality. This way, policies and AA resources would be clearly separated.

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