Devise, how to use with MongoDB's embedded documents - ruby-on-rails

In my Rails app I have to models: Account & User. I'm using MongoID and Devise with Omniauth.
I'm having some issues with devise and warden about logging in users correctly with this configuration:
class Account
#code..
include Mongoid::Document
embeds_many :users
#code..
end
class User
#code..
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable
devise :omniauthable, omniauth_providers: [:twitter] #just an example..
include Mongoid::Document
embedded_in :account
#code..
end
And a pretty standard OmniauthCallbacksController:
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def twitter
callback_of('Twitter')
end
protected
def callback_of(provider)
#user = User.find_user_from(request.env["omniauth.auth"])
#user = User.new_with(request.env["omniauth.auth"]) if #user.blank?
if #user.persisted?
sign_in_and_redirect #user, event: :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, kind: provider) if is_navigational_format?
else
redirect_to new_user_registration_path, alert: "There was a problem with your #{provider} account. Please try again."
end
end
def after_sign_in_path_for(resource)
if resource.registered
account_path(resource.account)
else
new_account_path
end
end
end
That code works great, if the user is not registered an account document is created and an embedded user document is stored inside it. Then it's called sign_in_and_redirect #user, event: :authentication and the user is correctly signed in. Then there is a redirection to new_account_path where things become weird..
After redirection user is not logged in anymore, and therefore all devise helpers, like current_user are useless. I guess this is caused because users are embedded on accounts so Warden can't find the user correctly.
I found this Devise wiki's post, but seems to be outdated.
I'm not a devise/warden expert, so I'd need some help with this. Any help will be very appreciated.
Thanks!

Related

Can't setup simple OAuth2 between Google and toy website using Ruby on Rails, Devise, Omniauth

I am trying to learn Ruby on Rails by building a small web application. My first step was to start with OAuth login so users could login using Facebook, Google etc.
But when I go to the /users/sign_up devise page on my localhost and click on Sign in with GoogleOauth2, it "does nothing" and the console tells me:
D, [2021-10-05T04:55:04.716439 #10144] DEBUG -- omniauth: (google_oauth2) Request phase initiated.
W, [2021-10-05T04:55:04.730086 #10144] WARN -- omniauth: Attack prevented by OmniAuth::AuthenticityTokenProtection
E, [2021-10-05T04:55:04.730681 #10144] ERROR -- omniauth: (google_oauth2) Authentication failure! authenticity_error: OmniAuth::AuthenticityError, Forbidden
I've setup devise and omniauth-google-oauth2, and registered an app in the Google developer console, adding the appropriate callback uris as http://127.0.0.1:3000/users/auth/google_oauth2/callback and http://localhost:3000/users/auth/google_oauth2/callback, and wrote the key and secret to .env using the dotenv gem to read them, running the server with dotenv rails server.
I'd like to understand what is wrong here, why, how would I go about debugging this, and how to fix it so that logging in through Google takes me to my homepage, the "Yay! You're on rails!" screen.
My files are setup as such:
routes.rb:
Rails.application.routes.draw do
devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth' }
end
app/models/users.rb:
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:timeoutable,
:omniauthable, omniauth_providers: [:google_oauth2]
def self.create_from_google_data(provider_data)
where(provider: provider_data.provider, uid: provider_data.uid).first_or_create do | user |
user.email = provider_data.info.email
user.password = Devise.friendly_token[0, 20]
user.skip_confirmation!
end
end
end
app/config/initializers/devise.rb:
...
config.omniauth :google_oauth2, ENV['GOOGLE_APP_ID'], ENV['GOOGLE_APP_SECRET'], scope: 'userinfo.email,userinfo.profile'
...
app/controllers/users/omniauth_controller.rb
class Users::OmniauthController < ApplicationController
def google_oauth2
#user = User.create_from_google_data(request.env['omniauth.auth'])
if #user.persisted?
sign_in_and_redirect #user
set_flash_message(:notice, :success, kind: 'Google') if is_navigational_format?
else
flash[:error] = 'There was a problem signing you in through Google. Please register or try signing in later.'
redirect_to new_user_registration_url
end
end
def failure
flash[:error] = 'There was a problem signing you in. Please register or try signing in later.'
redirect_to new_user_registration_url
end
end
app/config/initializers/session_store.rb:
Rails.application.config.session_store :active_record_store, key: '_devise-omniauth_session'
Please let me know if any further clarification is needed to debug this issue.
For anyone stumbling upon the same issue, I solved it by adding the following gem to the project, after trying tons of fixes I found online:
gem "omniauth-rails_csrf_protection"
Why? No clear idea. Answers with more explanation would be very welcome.

Error Ruby on Rails: Users::OmniauthCallbacksController#facebook is missing a template for this request format and variant

I'm trying to add an external login with facebook, but whenever I realize the home page correctly directs me to the facebook page, but then I get the following error, and could not understand what it can be.
"Users::OmniauthCallbacksController#facebook is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot."
"That's what you'll get from an XHR or API request. Give it a shot."
raise ActionController::UnknownFormat, message
else
logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
super
this is my controller
class Users::OmniauthCallbacksController < ApplicationController
def facebook
#User = User.from_omniauth(request.env["omniauth.auth"])
if #User.persisted?
#User.remember_me = true
sign_in_and_redirect #User, event: :authentication
end
end
end
this is the user model.
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,:omniauthable, :omniauth_providers => [:facebook]
def self.from_omniauth(auth)
where(provider: auth[:provider], uid: auth[:uid]).first_or_create do |user|
if auth[:info]
user.email = auth[:info][:email]
user.name = auth[:info][:name]
end
user.password = Devise.friendly_token[0,20]
end
end
has_many :articles , :dependent => :destroy
end
and i put this line in config/initializers/divise.rb
config.omniauth :facebook, '504432376574467', 'b2bb80641fcc2ca4d28e48c5ce*******'
My guess would be that User.from_omniauth fails to create the user (possibly due to user.password and user.password_confirmation not matching), which causes the Users::OmniauthCallbacksController#facebook to reach the end of the method without going inside the if clause.
To check, you could for example add an else clause to your Facebook callback and raise an error in there.

Setting up different User models and registration paths for Devise on Ruby on Rails

I am very new to ruby and I have been really struggling with this for months. I searched extensively and tried what the answers said but still no luck. (I tried Multiple user models with Ruby On Rails and devise to have separate registration routes but one common login route but didnt work)
I currently have a user.rb model and it is connected to devise and works fine.
1- On the sign-up page, I would like to have 3 buttons that would lead to separate registration forms (one each for business, manager and the already existing user). Do I set this up in routes.rb?
2- The forms will have different attributes that will populate their respective databases.
3- After completion of the form they will be directed to their respective routes. User to the current default route while business to the business dashboard and manager to the manager dashboard. Is this again in routes.rb or devise?
I would greatly appreciate any guidance!
I've read through the documentations for devise, cancan and rolify but I can't seem to bring it all together to work for me.
I am very new to ruby and I have been really struggling with this for months. I searched extensively and tried what the answers said but still no luck. (I tried Multiple user models with Ruby On Rails and devise to have separate registration routes but one common login route but didnt work)
I currently have a user.rb model and it is connected to devise and works fine.
1- On the sign-up page, I would like to have 3 buttons that would lead to separate registration forms (one each for business, manager and the already existing user). Do I set this up in routes.rb?
2- The forms will have different attributes that will populate their respective databases.
3- After completion of the form they will be directed to their respective routes. User to the current default route while business to the business dashboard and manager to the manager dashboard. Is this again in routes.rb or devise?
I would greatly appreciate any guidance!
I've read through the documentations for devise, cancan and rolify but I can't seem to bring it all together to work for me.
#user.rb
class User < ActiveRecord::Base
has_many :contibutions
rolify
# Include default devise modules. Others available are:
# :lockable, :timeoutable
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable, :omniauthable
validates_format_of :email, :without => TEMP_EMAIL_REGEX, on: :update
def admin?
has_role?(:admin)
end
def self.find_for_oauth(auth, signed_in_resource = nil)
# Get the identity and user if they exist
identity = Identity.find_for_oauth(auth)
user = identity.user
if user.nil?
# Get the existing user from email if the OAuth provider gives us an email
user = User.where(:email => auth.info.email).first if auth.info.email
# Create the user if it is a new registration
if user.nil?
user = User.new(
name: auth.extra.raw_info.name,
#username: auth.info.nickname || auth.uid,
email: auth.info.email.blank? ? TEMP_EMAIL : auth.info.email,
password: Devise.friendly_token[0,20]
)
user.skip_confirmation!
user.save!
end
# Associate the identity with the user if not already
if identity.user != user
identity.user = user
identity.save!
end
end
user
end
end
I'd go with one User model and a two stage signup. First they would click on their desired button, each one passing a unique 'role' param in the URL and going to the devise signup page. Here they would enter only their email/password and we would pass the param from the URL to a simple 'role' hidden field in the form.
Then as step 2, after technically registering, they are directed to a separate edit account type page (each user having a different account, outlined below) to fill in the rest of their details.
The models:
models/user.rb
class User < ActiveRecord::Base
has_one :account
has_one :business_account
has_one :manager_account
end
models/account.rb
class Account
belongs_to :user
models/business_account.rb
class BusinessAccount
belongs_to :user
models/manager_account.rb
class ManagerAccount
belongs_to :user
Then, using devise, I'd override the registrations_controller to add a role based on a hidden field in the first step simple registration form (which would just be email/password/role).
In that file, I'd also override the after_signup_path method, to redirect to an edit_account type page for the relevant account we create for them during signup.
First the routes:
devise_for :users, :controllers => {:registrations => "registrations"}
resources :users do
resource :account
resource :business_account
resource :manager_account
end
Then the controller (see comments within code):
controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
def create
build_resource(sign_up_params)
if resource.save
# you will name the following param. make sure it's in devise strong_params
# also the == will depend on how you pass the role - string, integer etc
if sign_up_params[:role] == "1"
user.add_role :standard
resource.build_account(user_id: resource.id) # code to create user account
elsif sign_up_params[:role] == "2"
user.add_role :manager
resource.build_manager_account(user_id: resource.id) # code to create user account
elsif sign_up_params[:role] == "2"
user.add_role :business
resource.build_business_account(user_id: resource.id) # code to create user account
end
if resource.active_for_authentication?
set_flash_message :notice, :signed_up if is_navigational_format?
sign_up(resource_name, resource)
respond_with resource, :location => after_sign_up_path_for(resource)
else
set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
expire_session_data_after_sign_in!
respond_with resource, :location => after_inactive_sign_up_path_for(resource)
end
else
clean_up_passwords resource
respond_with resource
end
end
protected
# override the after signup path to your desired route, e.g
def after_sign_up_path_for(resource)
if sign_up_params[:role] == "1"
edit_user_account_path(resource.id)
elsif sign_up_params[:role] == "2"
edit_user_manager_account_path(resource.id)
elsif sign_up_params[:role] == "2"
edit_user_business_account_path(resource.id)
end
end
end
The above would redirect them to a separate accounts controller/view depending on the account type. This solution would save you a lot of headaches down the line.

Rails 4 devise , How to sign up new users if signed in?

I want to block signup for non-admins so that only a superuser/administrator can add new users. How can I achieve that?
I tried following the method mentioned here: Devise before filter that prevents access to “new_user_registration_path” unless user is signed-in but had no results.
I have installed devise, cancan and rolify. In addition, I also don't want anyone to go to the /users/sign_up page and sign in. Only admins must have the ability to sign up new users.
Due to the devise installation there is no users controller. Please guide me through making one if needed.
routes.rb
FifthApp::Application.routes.draw do
devise_for :users
resources :qanotes
end
user.rb
class User < ActiveRecord::Base
#resourcify :resources
rolify
has_many :qanotes
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
I keep on getting redirected to the root, i.e. localhost:3000, when I try to go to sign up page (only after log in).
Remove :regesterable keyword from default devise modules from your user model. Than you should add your own form for creating new user, and thus you can add new users.
Hope it will help. Thanks
You could customize registration controller (e.g registrations_controller.rb), and you should add before_filter authentication and before_filter only administrator to registrations_controller.rb looks like :
class RegistrationsController < Devise::RegistrationsController
before_filter :authenticate_user!
before_filter :is_administratior?, only: [:new, :create]
def new
super
end
def create
super
end
private
def is_administratior?
if user_signed_in? # if user signed
if current_user.administrator? # if adminstrator return true
true
else
redirect_to some_path
end
else
redirect_to login_path
end
end
See my answer here about Devise/cancan redirect admin

Devise / ActionMailer sending duplicate emails for registration confirmation

My rails application uses devise to handle registration, authentication, etc. I'm using the confirmable module. The bug is this– when a user registers with email, Devise is sending two confirmation emails with different confirmation links. One link works, the other directs the user to an error page.
Devise spits out a message associated with the error: "Confirmation token is invalid" and takes the user to the Resend Confirmation Email page.
I'm hosting with heroku and using sendgrid to send the emails. update: The bug also occurs on localhost.
I have no idea where the root of this bug is, and this might be more code than what you need to see:
models/user.rb
...
devise :database_authenticatable, :registerable, :omniauthable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable, :authentication_keys => [:login]
...
## callbacks
after_create :account_created
# called after the account is first created
def account_created
# check if this activiy has already been created
if !self.activities.where(:kind => "created_account").blank?
puts "WARNING: user ##{self.id} already has a created account activity!"
return
end
# update points
self.points += 50
self.save
# create activity
act = self.activities.new
act.kind = "created_account"
act.created_at = self.created_at
act.save
end
...
def confirmation_required?
super && (self.standard_account? || self.email_changed)
end
...
controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
def update
unless #user.last_sign_in_at.nil?
puts "--------------double checking whether password confirmation is required--"
## if the user has not signed in yet, we don't want to do this.
#user = User.find(current_user.id)
# uncomment if you want to require password for email change
email_changed = #user.email != params[:user][:email]
password_changed = !params[:user][:password].empty?
# uncomment if you want to require password for email change
# successfully_updated = if email_changed or password_changed
successfully_updated = if password_changed
params[:user].delete(:current_password) if params[:user][:current_password].blank?
#user.update_with_password(params[:user])
else
params[:user].delete(:current_password)
#user.update_without_password(params[:user])
end
if successfully_updated
# Sign in the user bypassing validation in case his password changed
sign_in #user, :bypass => true
if email_changed
flash[:blue] = "Your account has been updated! Check your email to confirm your new address. Until then, your email will remain unchanged."
else
flash[:blue] = "Account info has been updated!"
end
redirect_to edit_user_registration_path
else
render "edit"
end
end
end
end
controllers/omniauth_callbacks_controller
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_filter :verify_authenticity_token
def facebook
user = User.from_omniauth(request.env["omniauth.auth"])
if user.persisted?
flash.notice = "Signed in!"
# if the oauth_token is expired or nil, update it...
if (DateTime.now > (user.oauth_expires_at || 99.years.ago) )
user.update_oauth_token(request.env["omniauth.auth"])
end
sign_in_and_redirect user
else
session["devise.user_attributes"] = user.attributes
redirect_to new_user_registration_url
end
end
end
config/routes.rb
...
devise_for :users, controllers: {omniauth_callbacks: "omniauth_callbacks",
:registrations => "registrations"}
...
I'm happy to provide more information if needed. I'm also open to customizing/overriding the devise mailer behavior, but I don't know how to go about that.
Much thanks!
Solved!
I was able to override Devise::Mailer and force a stack trace to find out exactly what was causing duplicate emails. Devise::Mailer#confirmation_instructions was being called twice, and I found out that the problem was with my :after_create callback, shown below:
in models/user.rb...
after_create :account_created
# called after the account is first created
def account_created
...
# update points
self.points += 50
self.save
...
end
Calling self.save somehow caused the mailer to be triggered again. I solved the problem by changing when the points are added. I got rid of the after_create call and overrode the confirm! method in devise to look like this:
def confirm!
super
account_created
end
So now the user record doesn't get modified (adding points) until after confirmation. No more duplicate emails!
I originally went with Thomas Klemm's answer but I went back to look at this when I had some spare time to try and figure out what was happening as it didn't feel right.
I tracked the 'problem' down and noticed that it only happens when :confirmable is set in your devise (User) model and reconfirmable is enabled in the devise initializer - which in hindsight makes a lot of sense because essentially in the after_create we ARE changing the User model, although we aren't changing the email address - I suspect Devise may do this because the account isn't confirmed yet, but in any case it is easy to stop the second email just by calling self.skip_reconfirmation! in the after_create method.
I created a sample rails project with a couple of tests just to ensure the correct behaviour. Below are the key excerpts. If you have far too much time on your hands, you can see the project here: https://github.com/richhollis/devise-reconfirmable-test
app/models/User.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :confirmable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
after_create :add_attribute
private
def add_attribute
self.skip_reconfirmation!
self.update_attributes({ :status => 200 }, :without_protection => true)
end
end
initializers/devise.rb
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
..
..
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
# db field (see migrations). Until confirmed new email is stored in
# unconfirmed email column, and copied to email column on successful confirmation.
config.reconfirmable = true
..
..
end
spec/models/user_spec.rb
require 'spec_helper'
describe User do
subject(:user) { User.create(:email => 'nobody#nobody.com', :password => 'abcdefghijk') }
it "should only send one email during creation" do
expect {
user
}.to change(ActionMailer::Base.deliveries, :count).by(1)
end
it "should set attribute in after_create as expected" do
user.status.should eq(200)
end
end
Running the rspec tests to ensure only one email is sent confirms the behaviour:
..
Finished in 0.87571 seconds 2 examples, 0 failures
Thanks for your great solution, Stephen! I've tried it and it works perfectly to hook into the confirm! method. However, in this case, the function is being called (as the name says) when the user clicks the confirmation link in the email he receives.
An alternative is to hook into the generate_confirmation_token method, so your method is called directly when the confirmation token is created and the email is sent.
# app/models/user.rb
def generate_confirmation_token
make_owner_an_account_member
super # includes a call to save(validate: false),
# so be sure to call whatever you like beforehand
end
def make_owner_an_account_member
self.account = owned_account if owned_account?
end
Relevant source of the confirmation module.

Resources