I made Devise authentication to log out via GET, but couldn't make it log out using this Angular.js code:
$scope.logout = ->
$http.get('/users/sign_out').success ->
#If it does not redirect from 'editor' to 'login' then you haven't actually logged out
$location.path('editor')
Devise's logout behaviour seems to be random - sometimes it logs out, sometimes not.
And if I enter /users/sign_out into browser's address bar, it logs out always.
Ok, I switched the Devise authentication's log out to POST request to get rid of caching problems and used following Angular.js code:
$scope.logout = ->
$http.post('/users/sign_out').success ->
$location.path('editor')
The first time it logged out fine, as always, but then I couldn't make it to log out.
I decided to make my own method to see what happens:
match '/logout' => 'api#logout', :via => :post
class ApiController < ApplicationController
before_filter :authenticate_user!
def logout
sign_out
if current_user
puts 'Has not signed out!'
else
puts 'Has signed out!'
end
head :ok
end
end
and detected that after sign_out the current_user is always nil, but then the Angular application by some miracle manages to access other methods of ApiController, and current_user isn't nil there!
I do not understand that. Ok, let us suppose that there may follow some other HTTP request, right after (or at the same time as) logout request, passing the authentication cookie and Devise re-logins, but shouldn't the session ID passed in cookie be expired immediately after call of sign_out method?!
sorry I never responded earlier, hope this helps
My Sesisons Controller
$scope.signOutUser = function () {
$http.delete('/api/users/sign_out', {
auth_token: Session.currentUser // just a cookie storing my token from devise token authentication.
}).success( function(result) {
$cookieStore.remove('_pf_session');
$cookieStore.remove('_pf_name');
$cookieStore.remove('_pf_email');
location.reload(true); // I need to refresh the page to update cookies
}).error( function(result) {
console.log(result);
});
}
My Devise Sessions Controller I overrode
class SessionsController < Devise::SessionsController
before_filter :authenticate_user!, only: :destroy
def destroy
token = params[:auth_token]
#user = User.find_by_authentication_token(token)
#user.reset_authentication_token!
sign_out(#user)
render status: :ok, json: {message: "You have successfully logged out"}
end
end
As you can see, I'm not using Rails cookies and thus my answer may not pertain. If I did I would probably add a line like session[:user] = nil in my destroy action.
Related
I've been battling this for about 24 hours now, and nothing I'm finding in my searches is leading to a solution.
My issue is my session data is not persisting and I can not log in to my app. Everything worked in Dev mode, but has not yet worked in Production. I'm using a Rails 6 Api hosted on Heroku and a React front end. I can successfully make the api call, find the user, and log them in using (I use "puts" to help me log the session at that instance. The session hash has a session_id and user_id at this point):
def login!
session[:user_id] = #user.id
puts "login_session: #{session.to_hash}"
end
After this the app redirects to the user page or an admin page depending on the users authorization.
When the redirect happens that the user or admin page calls the api to see if the user is authorized using:
def logged_in?
puts "logged_in_session: #{session.to_hash}"
!!session[:user_id]
end
The session is empty. Here is my sessions controller:
class SessionsController < ApplicationController
def create
#user = User.find_by(email: session_params[:email])
puts #user.inspect
if #user && #user.authenticate(session_params[:password])
login!
render json: {
logged_in: true,
user: UserSerializer.new(#user)
}
else
render json: {
status: 401,
errors: ['no such user', 'verify credentials and try again or signup']
}
end
end
def is_logged_in?
if logged_in? && current_user
render json: {
logged_in: true,
user: UserSerializer.new(current_user)
}
else
render json: {
logged_in: false,
message: 'no such user or you need to login'
}
end
end
def is_authorized_user?
user = User.find(params[:user_id][:id])
if user == current_user
render json: {
authorized: true
}
else
render json:{
authorized: false
}
end
end
def destroy
logout!
render json: {
status: 200,
logged_out: true
}
end
def omniauth
#user = User.from_omniauth(auth)
#user.save
login!
render json: UserSerializer.new(#user)
end
private
def session_params
params.require(:user).permit(:username, :email, :password)
end
def auth
request.env['omniauth.auth']
end
Would any be able to point me the right direction??
Thank you
I would verify the following:
When first authenticated, does the response from the endpoint include the cookie data?
Check the cookie store in your browser (there's a few extensions you can use to make this easier) and verify that the domain names match and the content in the cookie is what you'd expect.
You can cross reference the cookie ID with the ID in your session store (depending on where you've chosen to store this).
Can you verify the cookie contents (user_id) and session contents in the session store.
Make sure that the cookie data is being sent on the next request after authenticating (check the request headers in the network tab of your dev tools in the browser).
This is all assuming that you're using a browser to talk to this JSON endpoint. APIs usually don't use cookies as it's a browser thing. Alternative authentication mechanisms might be a short lived token (JWT for example) that is generated when authenticating that can be used for subsequent requests.
Quick update: I am able to get the "Set-Cookie: _session_id=..." in the response but it is blocked to due to "SameSite=lax" attribute.
I believe I need to change to SameSite = none, but I'm not sure were to do that.
Any advice?
A bit late but if you're using Rails 6 API, session has been disabled. You need to add the middleware manually. Here is the documentation using-session-middlewares
# This also configures session_options for use below
config.session_store :cookie_store, key: '_interslice_session'
# Required for all session management (regardless of session_store)
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options
I am having trouble with my authentication actions from react-native to my ruby on rails backend api. I thought my signup/ sign in actions were working fine until I made my logout action and I began receiving a rails controller error. My logout function, requests a delete of user's session and can't find it, but my login/signup functions have no problem creating a session.
My Sessions#Controller:
class SessionsController < ApplicationController
def create
#user = User.find_by_credentials(
params[:user][:username],
params[:user][:password]
)
if #user
login(#user)
render "users/show.json.jbuilder"
else
render json: ["Invalid username/password combination"], status: 401
end
end
def destroy
#user = current_user # I believe the problem is here, as it can't find current_user see
# application controller below
if #user
logout
render json: {}
else
render json: ["Nobody signed in"], status: 404
end
end
end
Application#controller , where I find the current user based on the session_token
class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
helper_method :current_user, :logged_in?, :logout
# private
def current_user
return nil unless session[:session_token]
#current_user ||= User.find_by(session_token: session[:session_token])
end
def logged_in?
!!current_user
end
def login(user)
user.reset_session_token!
session[:session_token] = user.session_token
#current_user = user
end
def logout
current_user.reset_session_token!
session[:session_token] = nil
#current_user = nil
end
end
My logout action:
export const testLogout = () => dispatch => {
dispatch(loadSpinner());
axios({
method: 'delete',
url: 'http://10.0.2.2:3000/session',
})
.then((response) => {
console.log("response: ", response);
if(reponse.status === 200) {
{dispatch(logoutCurrentUser(response))}
} else {
console.log("status", response.status);
};
});
};
My routes:
Rails.application.routes.draw do
resources :users, only: [:create, :show]
resource :session, only: [:create, :destroy, :show]
end
The errors (1):
Started DELETE "/session" for 127.0.0.1 at 2020-05-27 10:23:23 -0700
Processing by SessionsController#destroy as HTML
Completed 404 Not Found in 0ms (Views: 0.2ms | ActiveRecord: 0.0ms).
(2):
Possible Unhandled Promise Rejection (id: 0):
Error: Request failed with status code 404
Error 2 from the testLogout() function never even makes it to the .then(response) in the function.
Now this 404 error is my own from the #destroy method in my sessions_controller and I am a bit unsure how to get a better error here but from what I am reading online is that react-native handles browser cookies differently and I am concerned I am missing something conceptually but I am very unsure of what it is. I know I could use AsyncStorage for persisting sessions but how does that factor in with the back-end rails authentication? Any help figuring out how to log out a user is greatly appreciated thank you!
I highly recommend using some form of a JWT for authentication handling. Using sessions with an external client is certainly possible, but it's a headache you probably don't want to deal with. Read about CORS. In short, you don't want to deal with CORS if you don't have to.
This seems like a fairly decent guide if you're using devise: https://medium.com/#eth3rnit3/crud-react-native-ror-backend-with-devise-auth-token-4407cac3aa0b
Currently I have an Ajax call via a button click that updates a shopping cart, but the controller that it posts to has the authenticate_user! before_filter applied, which is what I want, as a user must be logged in.
Because it's an ajax call I normally just get a 401 error and no redirect, so to solve this I have:
$(document).on "ajaxError", (event, request, settings) ->
if request.status == 401
window.location.href = '/users/login'
However this is causing me issues with getting the flash[:notice] to appear as it's lost by the time i get to the login page.
From doing some reading on various posts on here I understand that I can use flash.keep which would persist my message, but I think to do that I am going to have to change my approach on handling the redirect. If I can do this in the controller I could also use if request.xhr?, couldn't I?
My question is how would I add this functionality whilst keeping all existing functionality of the authenticated_user! helper method. From looking at the devise docs the helper is dynamically built, isn't it?
I know I could put a helper into my application controller:
def authenticate_user!
super
end
So expanding on that I have tried
def authenticate_user!
if request.xhr?
respond_to do |format|
format.html { redirect_to new_user_session_path }
end
else
super
end
But when clicking the update button, which is this
$(window).load ->
$('a[data-target]').click (e) ->
e.preventDefault()
$this = $(this)
if $this.data('target') == 'Add to'
url = $this.data('addurl')
new_target = "Remove from"
else
url = $this.data('removeurl')
new_target = "Add to"
$.ajax url: url, type: 'put', success: (data) ->
$('.badge-number').html(data)
$this.find('span').html(new_target)
$this.data('target', new_target)
I get this error in the console
Routing Error
No route matches [PUT] "/users/login"
Not sure where to go from here.
This sounds quite high-friction from the user point of view. If I click "add to cart", I expect the thing to be added to the cart. I certainly don't expect to be redirected to a login/signup page. At this point, I would just navigate away.
A better approach is, if user is not logged in, create a guest user and write its id to the cookies. Guest user has cart, can add stuff, etc.
Then, at checkout page, you offer the choice "login" / "sign up". In either case, you get a real user. At which point you migrate guest cart and delete the guest user.
Example:
def current_or_guest_user
return guest_user unless current_user
# current user exists. And we found a guest user
if session[:guest_user_id]
transfer_guest_user_stuff # cart, posts and what-have-you
guest_user.destroy
session[:guest_user_id] = nil
end
current_user
end
def guest_user
unless #cached_guest_user
#cached_guest_user = User.find_or_create_by(id: session[:guest_user_id], is_guest: true)
session[:guest_user_id] = #cached_guest_user.id
end
#cached_guest_user
end
As the first piece of advice, You could use Trubolinks gem which provide all AJAX functionality "in-box" and is default in Rails 4+.
As the second piece of advice, You could generate Users Controler ( rails generate devise:controllers Users) and\or Devise modules such as sessions. passwords and other controlers with Customize Devise Tools.
Additionally, You could customize before_filter in certain Controller's code, for example:
class TicketsController < ApplicationController
before_filter :store_location
before_filter :authenticate_user!, :only => [:new, :delete]
In this sample, :authenticate_user should be only for :new and :delete methods. Other methods not require users authentication ...
I hope it help You!
I need only show a message when send the reset password instructions, I don't need redirect to new session, I overwritten the controllerPassword but when I put a redirect_to there is a error with this render.
The path used after sending reset password instructions
def after_sending_reset_password_instructions_path_for(resource_name)
flash[:notice] = "We have sent an email with the instructions to reset your password"
redirect_to new_user_password_path and return
end
this is the error:
Render and/or redirect were called multiple times in this action......
How can I fix it?
If you remove redirect_to new_user_password_path and return from your code entirely it will stop redirecting. However, if you do this it won't show your flash notice until the user manually refreshes the page. From here there are two fixes:
redirect to the current page to force a refresh and show the notice.
bind your flash notice to an AJAX request so that it's sent asynchronously. There are a lot of ways to do that, but this answer covers it pretty well.
The controller action which is calling after_sending_reset_password_instructions_path_for has a render or a redirect_to.
Try removing redirect_to in after_sending_reset_password_instructions_path_for, and let the calling action handle it.
I have to overwrite other class called: SessionsController < Devise::SessionsController
I don't know if this is the best solution but worked for me.
This was I did:
class SessionsController < Devise::SessionsController
# GET /resource/sign_in
def new
url = (request.env["HTTP_REFERER"]).to_s
path = url.split("?").first
if path == "http://#{Rails.application.secrets.domain_name}/users/password/new"
redirect_to new_user_password_path and return
end
self.resource = resource_class.new(sign_in_params)
clean_up_passwords(resource)
yield resource if block_given?
respond_with(resource, serialize_options(resource))
end
end
I am using Devise and Omniauth for facebook authentication. I am trying to redirect the User to the previous page after the User logs in either with Devise(regular) or Omniauth(fb).
"request.referer" works when the user uses the dropdown login on my navbar, but when the user tries to log in through the "http://localhost:3000/users/sign_in" url, "request.referer" gives me back an infinite loop error.
Here is the sequence. The problem is in (d).
(a) Unlogged-in User clicks Vote up for Mission.
(b) User is redirected to the "users/sign_in" url, because of the "before_filter :authenticate_user!" in the Missions Controller.
(c) User signs in either by typing in username/pword, or clicking Facebook icon.
(d) User should be redirected back to previous page, with the function VoteUp completed, but instead gives routing error
=> when I go back and refresh the page, the user is signed in, so I know it's just a problem with routing
MissionsController.rb
class MissionsController < ApplicationController
before_filter :authenticate_user!, :except => [:show, :index]
def vote_for_mission
#mission = Mission.find(params[:id])
if #mission.voted_by?(current_user)
redirect_to request.referer, alert: 'You already voted on this mission.'
else
#mission.increment!(:karma)
#mission.active = true
#mission.real_author.increment!(:userpoints) unless #mission.real_author.blank?
current_user.vote_for(#mission)
redirect_to request.referer, notice: 'Your vote was successfully recorded.'
end
end
ApplicationsController.rb
class ApplicationController < ActionController::Base
protect_from_forgery
def after_sign_in_path_for(resource)
sign_in_url = "http://localhost:3000/users/sign_in" || "http://onvard.com/users/sign_in" ||
"http://www.onvard.com/users/sign_in" #url_for(:action => 'new', :controller => 'sessions', :only_path => false, :protocol => 'http')
if (request.referer == sign_in_url)
env['omniauth.origin'] || request.env['omniauth.origin'] || stored_location_for(resource) || getting_started_path || root_path
else
request.referer
end
end
The User is redirected to the "getting_started_path," which I put so I knew the previous options weren't working. The 'else' case, which is the dropdown login form I created with jquery, redirects the user to the previous page perfectly fine, but when I try to put the request.referer in the 'request.referer==sign_in_url' case, it gives me an infinite loop.
How would I redirect the User to the previous page even when the user logs in through the 'users/sign_in' url??
You can't simply redirect back, because the sign_in_url is meant to only accept users that are not logged in, and after logging in your app is sending a logged in user back to that page.
If you need to redirect the user to a specific location after login you should store the original place the user tried to go before the redirect to the login page, then after the login, check if the user should go to a different place after logging in, and redirect him there.
I'm addressing this reply to future generations who use devise and have the same problem of OP: you should be able to solve it by using #stored_location_for, that returns the path that the user wanted to reach before being redirected to the sign in page.