My User.rb:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,:confirmable,:token_authenticatable,
:recoverable, :rememberable, :trackable, :validatable, :authentication_keys => [:name]
My routes:
devise_for :users, :controllers => { :sessions => "sessions", :confirmations => "confirmations", :passwords => "passwords", :registrations => "registrations" }
My ConfirmationsController is a standard controller but with different redirect.
I have link on my email like:
/users/confirmation?confirmation_token=167bad44a15e02b0bd570b51e1bf927b88368d8855d92b9833a24017a2bad4be
In database user has
confirmation_token:167bad44a15e02b0bd570b51e1bf927b88368d8855d92b9833a24017a2bad4be
But when i click on that link i only see page with:
Resend confirmation instructions
Confirmation token is invalid
What i dont do - what else i have to set.
CONFIRMATIONCONTROLLER:
def resource_params
params.require(:user).permit(:confirmation_token)
end
private :resource_params
def show
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
if resource.errors.empty?
set_flash_message(:notice, :confirmed) if is_navigational_format?
sign_in(resource_name, resource)
session['new_user'] = true
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
else
respond_with_navigational(resource.errors, :status => :unprocessable_entity){ render :new }
end
end
protected
# The path used after resending confirmation instructions.
def after_resending_confirmation_instructions_path_for(resource_name)
new_registration_path(resource_name)
end
I say "standard controller" because when i remove it and do not use custom controller problem is that same.
Which version of devise are you using? If you're on 3.1.0 or higher, this behavior is expected:
CHANGELOG.md
The tokens that are stored in the database are not supposed to match the tokens that you send in the confirmation e-mails. See devise/lib/devise/models/confirmable.rb, which now contains the following:
def confirm_by_token(confirmation_token)
original_token = confirmation_token
confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
As you can see, the token that you pass in via query string params is consumed by the Devise.token_generator, and the result of that operation is what's compared with the token in the database to discover the user record.
It looks like it's temporarily possible (in 3.1 but not 3.2) to turn this off by setting
config.allow_insecure_token_lookup = true
in your devise initializer. But the default behavior has been changed to make devise more secure. See this blog post for a complete rundown of the security improvements in devise 3.1, including this change.
You can use the solution below (setting config.allow_insecure_token_lookup = true) but, according to the Devise changelog, this will be only available temporarily.
The problem likely arose because you ran the devise generator to dump all of their views into yours back before they made these changes. Then you updated your Devise gem and all the back end stuff changed but your views didn't. Now all your devise-related views and mailers are out of date and won't work with the new token styles.
You can see the new emails at: https://github.com/plataformatec/devise/tree/master/app/views/devise/mailer. The major change is changing this line:
<p><%= link_to 'Confirm my account', confirmation_url(#resource, :confirmation_token => #resource.confirmation_token) %></p>
to
<p><%= link_to 'Confirm my account', confirmation_url(#resource, confirmation_token: #token) %></p>
The main difference is #resource.confirmation_token becomes just #token.
I change #resource.confirmation_token to #token then it works.
Related
I have a Rails 5 site which consists of 2 parts:
Admin area
API-only client area
I'm using Devise for both parts and https://github.com/lynndylanhurley/devise_token_auth gem for the API frontend.
The problem is about using the omniauth authentication. When I omniauth authenticate into the admin area - everything is ok - I get back some successful HTML-response.
But the problem is that I'm getting the same HTML-response in the API-area - but I need some JSON-response - not HTML one.
Here is my code:
config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: { sessions: 'users/sessions', :omniauth_callbacks => 'users/omniauth_callbacks' }
namespace :api do
mount_devise_token_auth_for 'User', at: 'auth', controllers: { sessions: 'api/users/sessions', :omniauth_callbacks => 'api/users/omniauth_callbacks' }
end
end
app/models/user.rb
class User < ApplicationRecord
# Include default devise modules.
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:omniauthable,
:omniauth_providers => [:facebook, :vkontakte]
include DeviseTokenAuth::Concerns::User
devise :omniauthable
def self.from_omniauth_vkontakte(auth)
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
user.email = auth.extra.raw_info.first_name.to_s + "." + auth.extra.raw_info.last_name.to_s + '#vk.com'
user.password = Devise.friendly_token[0,20]
end
end
end
app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def vkontakte
#user = User.from_omniauth_vkontakte(request.env["omniauth.auth"])
if #user.persisted?
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Vkontakte") if is_navigational_format?
else
session["devise.vkontakte_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
config/initializers/devise.rb
Devise.setup do |config|
config.omniauth :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"], provider_ignores_state: true
config.omniauth :vkontakte, ENV["VKONTAKTE_APP_ID"], ENV["VKONTAKTE_APP_SECRET"]
end
Gemfile
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-vkontakte'
Gemfile.lock
devise (4.3.0)
devise_token_auth (0.1.42)
Here's my log:
Started GET "/api/auth/vkontakte" for 127.0.0.1 at 2017-06-20 17:34:23
+0300
Started GET "/omniauth/vkontakte?namespace_name=api&resource_class=User" for
127.0.0.1 at 2017-06-20 17:34:23 +0300
I, [2017-06-20T17:34:23.237270 #15747] INFO -- omniauth: (vkontakte) Request phase initiated.
Started GET "/omniauth/vkontakte/callback?code=0b8446c5fe6873bb12&state=52254649eb899e3b743779a1a4afc0304f249a6dd90b4415" for 127.0.0.1 at 2017-06-20 17:34:23 +0300
I, [2017-06-20T17:34:23.672200 #15747] INFO -- omniauth: (vkontakte) Callback phase initiated. Processing by Users::OmniauthCallbacksController#vkontakte as */* Parameters: {"code"=>"0b8446c5fe6873bb12", "state"=>"52254649eb899e3b743779a1a4afc0304f249a6dd90b4415"}
I guess that the problem is about a so-called "callback" url. I don't understand where it is set. It is obvious from the log that at the end of the auth process the GET "/omniauth/vkontakte/callback..." query is called. And probably it is called always - no matter if I initiated the oath sequence from admin or api client area.
I use Chrome Postman to make the API query http://localhost:3000/api/auth/vkontakte - and I get the HTML-response back ("successful login etc.") - but I need surely some JSON-response.
Is there a way to dynamically change the callback path depending on some precondition?
Is the callback query somewhat different depending on from where the oath procedure was initiated?
EDIT1:
This is not a single problem here unfortunately. Looks like the oauth is simply not implemented in the https://github.com/lynndylanhurley/devise_token_auth gem. So, even if I succeed to switch the oauth login procedure to the JSON way - how do I login the user the devise_token_auth-way - generating 3 tokens etc...? The app/controllers/users/omniauth_callbacks_controller.rb needs to be totally reimlemented.
You can render json from your OmniauthCallbacksController based on some extra parameter provided when your request a connection from the API for example.
These extra parameters will be availables in this hash request.env["omniauth.params"].
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def vkontakte
#user = User.from_omniauth_vkontakte(request.env["omniauth.auth"])
if #user.persisted?
sign_in #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Vkontakte") if is_navigational_format?
if request.env["omniauth.params"]["apiRequest"]
render status: 200, json: { message: "Login success" }
else
redirect_to after_sign_in_path_for(#user)
end
else
session["devise.vkontakte_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
You can this extra parameters by calling the auth helper with additional parameters, they will be passed to your OmniauthController : user_vkontakte_omniauth_authorize_path(api_request: true) (Or whatever your route helper is)
I ended up implementing my own oauth callback procedure - instead of using one from the devise_token_auth gem.
The devise_token_auth gem does contain the oauth authentication - but it appears to be not working properly.
Here are my code changes:
config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: { sessions: 'users/sessions', :omniauth_callbacks => 'users/omniauth_callbacks' }
namespace :api do
mount_devise_token_auth_for 'User', at: 'auth', controllers: { sessions: 'api/users/sessions'}
end
end
app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
include DeviseTokenAuth::Concerns::SetUserByToken
def vkontakte
#user = User.from_omniauth_vkontakte(request.env["omniauth.auth"])
namespace_name = request.env["omniauth.params"]["namespace_name"]
if #user.persisted?
if namespace_name && namespace_name == "api"
#client_id = SecureRandom.urlsafe_base64(nil, false)
#token = SecureRandom.urlsafe_base64(nil, false)
#user.tokens[#client_id] = {
token: BCrypt::Password.create(#token),
expiry: (Time.now + DeviseTokenAuth.token_lifespan).to_i
}
#user.save
#resource = #user # trade-off for "update_auth_header" defined in "DeviseTokenAuth::Concerns::SetUserByToken"
sign_in(:user, #user, store: false, bypass: false)
render json: #user
else
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "Vkontakte") if is_navigational_format?
end
else
session["devise.vkontakte_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
The inclusion of include DeviseTokenAuth::Concerns::SetUserByToken provides 5 auth headers in response:
access-token →BeX35KJfYVheKifFdwMPag
client →96a_7jXewCThas3mpe-NhA
expiry →1499340863
token-type →Bearer
uid →376449571
But the response still lacks these headers (available at a common sign-in):
Access-Control-Allow-Credentials →true
Access-Control-Allow-Methods →GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Origin →chrome-extension://aicmkgpgakddgnaphhhpliifpcfhicfo
Access-Control-Max-Age →1728000
I don't know whether they are important and if yes - how to provide them.
PS The same identical approach works with Facebook too.
I have a problem with my devise gem. When I log out with a user, devise deletes my user out of my mysql database. I recognized this error first on yesterday, before it was working. I don't remember what register and login is also still possible.
User Controller
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #user }
end
end
def destroy
#user = User.find(params[:id2])
#user.destroy
respond_to do |format|
format.html { redirect_to root_url}
end
end
end
Logout-Link
<%= link_to " Logout", destroy_user_session_path(:id2 => current_user.id), :class => "btn btn-danger icon-share-alt ",:style=>"font-family: FontAwesome;font-weight: bold;color:black", :method => :delete %>
User Model
class User < ActiveRecord::Base
rolify
belongs_to :ressourcen
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :vorname, :nachname, :email, :password, :password_confirmation, :roleid,:id
validates :email, :uniqueness => true
That seems really weird. If devise and routes are setup in a default way, I can't see how this should happen. What does:
rake routes
show you for destroy_user_session_path? If you haven't overridden devise's SessionsController, and haven't changed devise's routes, the only entry for destroy_user_session should look something like this:
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
It definitely shouldn't be going to your UsersController at all. It should be going to devise's SessionsController.
In any case, this logout link works for me:
<%= link_to "Sign out", destroy_user_session_path, :method => :delete %>
destroy_user_session_path shouldn't need the user id. It's only destroying the user in the session, so there is no need for an id.
UPDATE:
When you log out, what page are you returned to? If you haven't defined the SessionsController yourself and haven't overridden after_sign_out_path(), it should try to return to root_path. If you don't have root defined in your routes, it will return to "/". Can you confirm from your log file (e.g. development.log) exactly what happens when you click on the 'logout' link? Which actions are called and when/what SQL queries are called? Just when you click on the link, not when you later browse to look at users. Also, what does the devise_for line in your routes.rb look like? It's worth digging in to exactly what is happening when you only click on the logout link.
As far as I know the only thing inside devise which could delete a user from the database is RegistrationsController.destroy() which would be called if a DELETE request was sent to /users (with no id). It would find the currently-logged-in user from the session (which is why it doesn't need an id) and destroy them. In the standard devise views, it looks like this can only be called from devise/registrations/edit.html.erb and presumably you aren't doing this.
Did you customize/override the devise sessions controller?
If not your logout link should probably look like this:
<%= link_to " Logout", destroy_user_session_path, :class => "btn btn-danger icon-share-alt ",:style=>"font-family: FontAwesome;font-weight: bold;color:black", :method => :delete %>
Devise doesn't know what the :id2 param is.
Try replacing this:
destroy_user_session_path
with this:
destroy_session_path(:user_id => current_user.id)
And than in SessionsController destroy your session. This way you are destroying your user every time someone click on logout link.
I have a custom mailer (UserMailer.rb) and a few methods to override the default Devise methods for the welcome email and forgot password emails. The mailer uses a custom template to style the emails--and it works great.
In config/initializers, I have a file with
module Devise::Models::Confirmable
# Override Devise's own method. This one is called only on user creation, not on subsequent address modifications.
def send_on_create_confirmation_instructions
UserMailer.welcome_email(self).deliver
end
...
end
(Again, UserMailer is setup and works great for the welcome email and reset password email.)
But what's not working is the option to "Resend confirmation instructions." It sends with the default Devise styling and I want it to use the styling of my mailer layout. I know I can manually add the layout to the default Devise layout, but I'd like to keep DRY in effect and not have to do that.
I've tried overriding the send_confirmation_instructions method found here, but I'm getting a wrong number of arguments (1 for 0) error in create(gem) devise-2.2.3/app/controllers/devise/confirmations_controller.rb at
7 # POST /resource/confirmation
8 def create
9 self.resource = resource_class.send_confirmation_instructions(resource_params)
In my initializer file, I'm able to get to this error by adding a new override for Devise, but I'm probably not doing this correctly:
module Devise::Models::Confirmable::ClassMethods
def send_confirmation_instructions
UserMailer.send_confirmation_instructions(self).deliver
end
end
Any ideas?
You don't have to go through that initializer to do that. I've done this by overriding the confirmations controller. My routes for devise look like:
devise_for :user, :path => '', :path_names => { :sign_in => 'login', :sign_out => 'logout', :sign_up => 'signup'},
:controllers => {
:sessions => "sessions",
:registrations => "registrations",
:confirmations => "confirmations"
}
Then, create the confirmations_controller and extend the Devise::ConfirmationsController to override:
class ConfirmationsController < Devise::ConfirmationsController
In that controller, I have a create method to override the default:
def create
#user = User.where(:email => params[:user][:email]).first
if #user && #user.confirmed_at.nil?
UserMailer.confirmation_instructions(#user).deliver
flash[:notice] = "Set a notice if you want"
redirect_to root_url
else
# ... error messaging or actions here
end
end
Obviously, in UserMailer you can specify the html/text templates that will be used to display the confirmation message. confirmation_token should be a part of the #user model, you can use that to create the URL with the correct token:
<%= link_to 'Confirm your account', confirmation_url(#user, :confirmation_token => #user.confirmation_token) %>
How can I customize error messages to override devise passwords controller?
class PasswordsController < Devise::PasswordsController
def create
self.resource = resource_class.send_reset_password_instructions(params[resource_name])
if resource.errors.empty?
set_flash_message(:notice, :send_instructions) if is_navigational_format?
respond_with resource, :location => home_path
else
binding.pry
flash[:devise_password_error] = (resource.errors.map do |key, value|
value.capitalize
end).flatten.join('|')
redirect_to home_path and return
end
end
def edit
self.resource = resource_class.new
resource.reset_password_token = params[:reset_password_token]
end
end
resource.errors is available in this method but it contains default messages such as Email not found and Email can't be blank. i need to customize this messages. I've tried to remove :validatable from my user model and add custom validators but this works only for my custom registrations controller derived from Devise::RegistrationsController and not for custom passwords controller.
Is there any solution?
The answer is to modify config/locales/devise.en.yml but you must add the settings, they are not there by default.
en:
activerecord:
errors:
models:
user:
attributes:
password:
confirmation: "does not match"
too_short: "is too short (minimum is %{count} characters)"
Credit for this goes to Vimsha who answered virtually the same question for me.
Devise messages are located in config/locales/devise.en.yml
I'm not sure which message you're trying to override, but that's where you want to do that.
It's not ideal, but based on this related ticket I've got it working with the following (which I know is a bit of a hack, but it works):
module DeviseHelper
def devise_error_messages!
resource.errors.full_messages.map { |msg| msg == 'Email not found' ? 'The email address you entered could not be found. Please try again with other information.' : msg }.join('<br/>')
end
end
Put this in a module called devise_helper.rb in your /app/helpers directory
Add this to your routes.rb
devise_for :users, controllers: { passwords: 'passwords' }
or
devise_for :users, :controllers => { :passwords => 'passwords' }
In a recent project, facebook Users can login using their Facebook UID to upload picture submissions based on file uploads or uploads from their personal albums etc.
Everything works quite nice on my local system in the development environment. Login via Facebook, Logout, Upload - all great.
In production though I'm facing a unknown and hard to debug problem. It seems that every once in a while (actually reproducable when uploading a new Submission to the system) the session is lost, the picture is NOT uploaded and the facebook user is logged out (!).
I'm using devise and omniauth. Omniauth is integrated into Devise.
Following is all the code that touches Devise/Omniauth or the User.
app/models/user.rb
class User < ActiveRecord::Base
devise :omniauthable, :rememberable, :omniauth_providers => [:facebook]
def self.create_with_omniauth(auth)
u = User.find_by_uid(auth["uid"])
return u unless u.nil?
create! do |user|
user.provider = auth["provider"]
user.uid = auth["uid"]
user.name = auth["user_info"]["name"]
user.email = auth['user_info']['email']
end
end
def after_signin_path
'/competition'
end
end
Database contains all needed fields for :rememberable, I hope.
app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def facebook
# You need to implement the method below in your model
#user = User.create_with_omniauth(env["omniauth.auth"])
if #user.persisted?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
#user.update_attributes!(:current_auth_token => env["omniauth.auth"]['credentials']['token'], :last_language => I18n.locale.to_s, :updated_at => Time.now, :remember_created_at => Time.now)
sign_in_and_redirect(:user, #user)
else
redirect_to '/competition'
end
end
protected
def after_omniauth_failure_path_for resource
'/competition'
end
end
config/initializers/devise.rb
OmniAuth.config.full_host = "http://#{APP_CONFIG[:domain]}"
Devise.setup do |config|
config.mailer_sender = "devise#myapp.host.com"
require 'devise/orm/active_record'
config.stretches = 10
config.encryptor = :bcrypt
config.timeout_in = 3.days
config.pepper = "2a4b8b2ed9e12e553a7a542176f2ace1af62c062f3ba203a590b8b6307f33042b394922807a840004a3dcdf1c4e97ae085fe2c29654ddaeab7c60f431a8078abb"
config.omniauth :facebook, APP_CONFIG[:facebook_app_id], APP_CONFIG[:facebook_app_secret], {
:scope => "email,user_photos,user_photos,publish_stream,offline_access",
:client_options => {
:ssl => {
:ca_file => "/etc/pki/tls/certs/ca-bundle.crt"
}
}
}
end
There are no auth-related methods in application_controller.rb.
routes.rb:
The interesting part below:
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
match '/logout_fb' => 'start#logoutfb'
authenticate :user do
get '/users/connect/:network', :to => redirect("/users/auth/%{network}")
end
Somehow I cannot get to understand the authenticate block, which according to another post should be helpful.. ideas on this too?
So many theories:
One is that the facebook function in the omniauth_callbacks_controller runs aside of the users' session, and hence sign_in_and_redirect won't work. So I had the idea of redirecting to another page like '/auth?uid=xxx' but this sounds both wrong, insecure and not stable.
Any help or hints are appreciated!
A bit of a long shot but try turning off protect_from_forgery - I had some issues with sessions disappearing and it turned out to be the issue discussed here https://github.com/intridea/omniauth/issues/203
In my config/initializers/omniauth.rb, I had to add the following:
OmniAuth.config.full_host = "http://yourdomain.com" # Or have an environment specific URL.
You are using devise but you are not using it's own helpers. For instance, you've defined your own current_user method. To be honest, I can't see any obvious mistakes you've made, so it's just a desperate tip.
what kind of a session store do you use locally and what in production?
When you say "facebook user is logged out", this user is still logged in to facebook, but lost his session at yourapp.com ?
Are you sure that user.id is never nil or that you anywhere else than in .destroy set session[:user_id]= some_nil_variable ?