I have just finished the Michael Hartl tutorial. I am trying to implement omniauth-facebook to sign-up and sign-in users. I am having trouble creating the master variable to use in the create action in the sessions controller. My assumption is that I should put in an if else statement to see if the master is either signing on through facebook or through the default form? Or should I use and or statement:
(master = Master.find_by(email: params[:session][:email].downcase) || Master.from_omniauth(env["omniauth.auth"])) ?
Here is the omniauth-facebook sessons controller create action code:
def create
user = User.from_omniauth(env["omniauth.auth"])
session[:user_id] = user.id
redirect_to root_url
end
And here is my current sessions controller create action code:
class SessionsController < ApplicationController
def new
end
def create
master = Master.find_by(email: params[:session][:email].downcase)
if master && master.authenticate(params[:session][:password])
sign_in master
redirect_back_or master
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
.
.
.
end
Whilst I might be late by one year to answer this question, I still think that new viewers with the same sort of problem like you had a year ago might benefit from the following answer
As I understand it the actual question is:
How does one integrate Facebook Authentication with an Authentication system that is built from Scratch?
Since you said you used Michael Hartl's tutorial I went ahead and adapted my answer to an authentication system written by Michael himself. Now let's go over each piece of code one step at a time.
1
First of all we need to create two additional columns to our existing Users table for the provider and uid as follows...
rails g migration add_facebook_auth_to_users provider uid
...and add omniauth-facebook gem to the Gemfile...
Gemfile
gem 'omniauth-facebook', '2.0.1'
Don't forget to run bundle install and migrate your database after.
2
Next we would need to go ahead and edit our Sessions controller...
sessions_controller.rb
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
if user.activated?
log_in user
params[:session][:remember_me] == '1' ? remember(user) : forget(user)
redirect_back_or user
else
message = "Account not activated. "
message += "Check your email for the activation link."
flash[:warning] = message
redirect_to root_url
end
else
flash.now[:danger] = 'Invalid email/password combination'
render 'new'
end
end
def create_facebook
user = User.from_omniauth(env["omniauth.auth"])
log_in user
redirect_back_or user
end
Sessions controller above contains two create actions. First one is the untouched method taken straight from the book, when the second one is a piece of code I used specifically for Facebook authentications.
Line user = User.from_omniauth(env["omniauth.auth"]) has two purposes. Create a user IF his/her unique facebook uid is not in the database ELSE log this person in the app if the uid exists. More on this later.
3
Next lets create a working .from_omniauth method that you briefly showed inside of your code in your question...
user.rb
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
user.provider = auth.provider
user.uid = auth.uid
user.name = auth.info.name unless user.name != nil
user.email = SecureRandom.hex + '#example.com' unless user.email != nil
user.activated = true
user.password = SecureRandom.urlsafe_base64 unless user.password != nil
user.save!
end
end
Here we try to find a user that matches the given provider and uid values from the hash and then we call first_or_initialize on the result of this. This method will either return the first matching record or initialize a new record with the parameters that were passed in. We then call tap on this to pass that User instance to the block. Inside the block we set various attributes on the user based on values from the OmniAuth hash.
You might be interested as to why did I put the unless statement after some of the attribute initializations. Well consider the situation whereby the user wants to update the profile, like change the name or smth, and then logs out. When that user finally decides to log back in, the .from_omniauth method will overwrite the User's in question update to the original facebook values, unless we stop it from doing so.
Also notice the use of SecureRandom library. Inside of the traditional authentication that was used by Michael Hartl inside his book, he introduces validations to email and password submissions. Emails must be neither blank nor taken. Likewise, a password has to be greater than 6 characters in length. Since emails have to be present and unique I decided to create dummy emails using SecureRandom.hex + #example.com. This will create a random hexadecimal string, like 52750b30ffbc7de3b362, and append it to #example.com, hence generating the unique dummy email. Same goes for the password, however, I preferred to generate a random base64 string using SecureRandom.urlsafe_base64. The most important thing to remember that Facebook users don't need to know this information to login since that is the whole point of using Facebook authentication. This allows them to add this information with real data later on if they desire to do so.
4
Now would be a good time to add the button on the main application page for users to let them actually log in into the app...
_header.html.erb
<% if logged_in? %>
.
.
.
<% else %>
.
.
<li><%= link_to "Sign in with Facebook", "/auth/facebook", id: "sign_in" %></li>
<% end %>
5
So, now we can send a request to facebook. One problem remains that we cannot actually handle the callback. To do so we need to do the following...
Add get 'auth/:provider/callback' => 'sessions#create_facebook' to the routes.rb file
Use convenient Javascript SDK provided by Facebook. Let's create a custom javascript file that uses this SDK...
app/assets/javascripts/facebook.js.erb
jQuery(function() {
$('body').prepend('<div id="fb-root"></div>');
return $.ajax({
url: window.location.protocol + "//connect.facebook.net/en_US/sdk.js",
dataType: 'script',
cache: true
});
});
window.fbAsyncInit = function() {
FB.init({
appId: 'your_facebook_app_id_goes_here',
version: 'v2.3',
cookie: true
});
$('#sign_in').click(function(e) {
e.preventDefault();
return FB.login(function(response) {
if (response.authResponse) {
return window.location = '/auth/facebook/callback';
}
});
});
return $('#sign_out').click(function(e) {
FB.getLoginStatus(function(response) {
if (response.authResponse) {
return FB.logout();
}
});
return true;
});
};
6
And that is it, except for one small caveat...
If user decides to go and edit the profile, he/she would see our dummy email displayed right in the middle.This happends because Rails is smart enough to autofill User's information.In this case this, frankly, is quite embarassing. Good thing, however, that it is an easy fix. Just set the value to nil inside the form, like so...
<%= f.email_field :email, value: nil, class: 'form-control' %>
Hopefully this helps people who would like to go the route of building authentication systems from scratch :)
Ok I was trying to integrate omniauth with the custom authorization code from the tutorial. After some research I decided to remove all the tutorial authorization code and go with Devise instead.
Timur's answer is excellent, but assumes that the OP has already configured some things for OmniAuth. Unfortunately some readers will not have done the config and will get errors.
The minimum config for omniauth is to set up facebook credientials, make them available to the app and create a file called config/initializers/omniauth.rb with the following:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :developer unless Rails.env.production?
provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
end
And of course, it would be wise to write some tests for this feature.
Neither this nor Timur's answer deals with the situation where a user has created a local login account, then tries to log in from Facebook. It will fail due to a duplicate email or create a new account if the Facebook email is different. the User.from_omniauth(auth) function should be updated to handle this case. omniauth has a page about how to handle this, but will require significant rework from Miachal's hartl's tutorial: https://github.com/omniauth/omniauth/wiki/Managing-Multiple-Providers
Related
I am trying to use session mechanism to store information of an user that is logged like this: session[:user_id]=#user_id , and that its ok.
But when a new user login in the app, the variable session[:user_id] is updated to the new user id, making the first one perform requests with an invalid id.
I used different browsers, private browsers, a browser in a Virtual Machine and another one in the host, and still got the problem.
I appreciate some suggestions. Is it normal the session being shared between multiple users? There is another way to store some specific data, and prevent the share between users? I thought that session was unique, why that variable is changing? The same happens for cookies variable.
EDIT:
application_controller
def sign_in
if(password != "" && #user_id!= "" && jenkinsip != "")
#client = JenkinsApi::Client.new(:server_url => jenkinsip, :username=> #user_id, :password=> password)
if(#client.get_jenkins_version != nil)
session[:user_id]=#user_id
end
end
end
in html
Every time session[:user_id]=#user_id is called, the session[:user_id] is being set to whatever the #user_id variable is set as.
Try using||= instead of =
set the session withsession[:user_id]||= #user_id to only set session[:user_id] to #user_id when session[:user_id] is undefined.
Follow the excellent answer of koxtra and
also have a look on devise gem for the user authentication.
Devise will do everything for you like users signin, signup, creating sessions and many more functions. You have to only install the Devise
in your rails application.
My devise users are "database_authenticatable" and "token_authenticatable". I've tried deleting the "authentication_token" field in the database for that user from the console, but they still seem to be able to use their existing auth token. Deleting the user entirely works, but I don't want to go that far.
Edit: for clarity. I want to use the rails console to sign out a user. i.e. run rails console and then some command.
Devise provides helper methods to do these things:
user = User.find(params[:id])
sign_in user
sign_out user
Hope this helps.
If you are using Devise you could use the below in your rails console. This works perfect for me as in my app if you are using only 1 session per user account. I am storing my sessions in redisDB.
user = User.first
user.update_attributes(unique_session_id: "")
All I had to do was clear my users unique_session_id for that user and rails kicks the user out of the session.
But for multiple sessions for the same User account this does not work.
If you want to clear all user sessions you can do the below from terminal
rake db:sessions:clear
To sign_in by Devise check this way in console:
$ rails console
include Warden::Test::Helpers
def sign_in(resource_or_scope, resource = nil)
resource ||= resource_or_scope
scope = Devise::Mapping.find_scope!(resource_or_scope)
login_as(resource, scope: scope)
end
def sign_out(resource_or_scope)
scope = Devise::Mapping.find_scope!(resource_or_scope)
logout(scope)
end
#user = User.find(1)
sign_in #user
Then open http://127.0.0.1:3000/users/sign_in to test, in my case it will bypass this page and go to home page! Same to sign_out!
You may be able to use the helpers that others have mentioned after including the necessary module:
include Devise::Controllers::SignInOut
source: Module: Devise::Controllers::SignInOut
There's also another SO question where someone shares a method that doesn't involve using Devise helpers here.
I'm not a fan of the sign_out #user pattern because, at least for the devise version I'm using, that signs out the current user, regardless of the argument I pass it. If you're storing sessions in your database then you can do this:
#user.update_attributes(current_sign_in_token: "")
TBH I don't think that's the best way to do it, but it's the best way I've seen in my own research.
I believe you can simply update the password_salt and it will invalidate the user session on their next request.
user = User.first
user.update_column(:password_salt, 'reset')
Reference:
http://www.jonathanleighton.com/articles/2013/revocable-sessions-with-devise/
For old devise versions
Seems attribute tokens save sessions:
user.tokens = nil
user.save
You can create a logout link in views->layouts->application.html.erb as:-
<= link_to 'Log Out', destroy_user_session_path, method: :delete %>
It worked for me - hope it does for others as well.
So I have this app that I'm making where users have profiles after they signup and input their information.
At the moment I'm trying to add a feature that allows for new unregistered users to go to a profile to see what the app is like before they need to sign up (I'm planning on putting a "try it for free" button on the home_controller#index action. For authentication, I'm using the Devise gem.
Currently, I've watched the Railscast (393) on this, but I can't figure out (after several hours of trying) how to implement guest users and log them in using Devise.
I've also read about 4 different solutions on SO, and have decided to stick to this one (how to create a guest user in Rails 3 + Devise):
class ApplicationController < ActionController::Base
def current_user
super || guest_user
end
private
def guest_user
User.find(session[:guest_user_id].nil? ? session[:guest_user_id] = create_guest_user.id : session[:guest_user_id])
end
def create_guest_user
u = User.create(:name => "guest", :email => "guest_#{Time.now.to_i}#{rand(99)}#example.com")
u.save(:validate => false)
u
end
...
end
I have this in my application_controller.rb and don't understand how I would use these functions in the home_controller#index to create a guest user and log into the profile when the "Try it" button is clicked.
I've tried manually creating a user, saving it without authentication and then using Devise's sign_in method on link like so: Try it! and also tried
Try it!
I tried this, but the profile throws some validation messages saying I need to log in to view it. I've also tried removing before_filter authenticate! on the profiles_controller but I can't seem to get this to work at all.
Would anyone know how to create a user on the button click, and auto sign them into a guest profile? Thanks a lot for any help, I'm completely lost here.
I think you have a misunderstanding on what a guest user is. You say that you want guest users to auto sign in, and that is wrong. Guest users can't sign in, because... Well, because they are guests. You want to create them, but not sign them in.
This is why you want these helper methods in your ApplicationController, because when you try to get the current_user, if that doesn't exist (nil), you will have a fallback (that is why you use the || operator), that will assign a guest_user as a current_user.
So, forget about using sign_in links for guest users and you should be fine.
We have a customer that wants to use their current Wordpress site at the "source" for their user table.
(If it makes a difference, the rails app will be the primary app interface for a web front end as well as an iOS and Android front ends.)
So, the user will login through the Website and the idea is that an API call would be made to Wordpress with the email/pwd. It would return an authentication successful. I would then issue a token or something like this to the Mobile platforms to allow them continued access.
Any thoughts on how to make the authentication piece work between rails -> wordpress?
In case anyone else wants to accomplish the same thing. Here is how I solved the problem. First, my wordpress instance and rails instances are sitting on the same box, which makes this solution viable.
1) I am using devise for authentication on the rails side. I have created an override for the "authenticate!" method, which checks wordpress.
require 'devise/strategies/authenticatable'
module Devise
module Strategies
class DeviseOverride < Authenticatable
def valid?
true
end
def authenticate!
if params[:user]
user = User.find_by_email(params[:user][:email])
# user = User.first
if user # && user.encrypted_password == params[:user][:password]
#check password with Wordpress to verify it is a good user
result = WordpressApi.verify_user(params[:user][:email], params[:user][:password])
if result
success!(user)
else
fail!("Couldn't verify your login. Please try again.")
end
else
fail!("Could not log in")
end
else
fail!("")
end
end
end
end
end
Warden::Strategies.add(:local_override, Devise::Strategies::DeviseOverride)
2) This calls a simple method where I just call over to the wordpress instance to verify the user exists. (I was trying to find a way check the DB table directly, but the WP password hashing isn't something I wanted to tackle)
3) On the wordpress side (along with some other stuff):
$user = get_user_by('email', $email);
// print $user->data->user_email;
if ($user && wp_check_password( $pwd, $user->data->user_pass, $user->ID) )
return_json_success('Valid User', 'user', $user);
else{
return_json_error('Passwords do not match', 200);
// print "Password: {$pwd}, User: {$user} UserPass: {$user->data->user_pass} UserID: {$user->ID}";
// print 'Passwords do not match';
}
I am using Devise with my Rails 3 application. The current behavior for resetting a password, is to click on the "Forgot your password?" link. The link here is:
(url)/password/new.user
Which will call the following method in the Devise passwords_controller.rb:
def new
build_resource({})
end
This method will do:
generates the password reset token and adds it to the database,
sends an email to the person with a link that includes the token:
(url)/password/edit?reset_password_token=xxxxxxxxxxxxxxx
Is there any way to convince Devise to perform step 1 ONLY and not step 2? Are there any security issues I should be aware of if this is possible, and I did take this approach in an effort to simplify a portion of the web site.
I would recommend overriding send_devise_notification on your User (?) model and return true when the notification value is :reset_password_instructions. Something like this:
# app/models/user.rb
def send_devise_notification(notification)
return true if notification == :reset_password_instructions
end
Check their example on how to override/customize behavior for sending emails
https://github.com/plataformatec/devise/blob/master/lib/devise/models/authenticatable.rb#L127
You can disable it at instance level:
# disable all notifications
user.define_singleton_method(:send_devise_notification) { |*_| true }
# disable the one you want
user.define_singleton_method(:send_devise_notification) do |*args|
return true if args[0] == :reset_password_instructions
super
end
The title of the question is general, but the question itself is more specific. This is the answer to the general question as of 2021.
To prevent a password changed email notification from being sent when changing a user password, call skip_password_change_notification! on the user before saving the user.
user = User.find(123)
user.skip_password_change_notification!
user.password = 'DoNotUse$123'
user.save