Authenticate custom strategy without creating Devise user - ruby-on-rails

My setup: Rails 3.0.9, Ruby 1.9.2, Devise 1.3.4, Warden 1.0.4
I'm trying to figure out if it possible to authenticate a custom strategy and not have to create a devise user in the process upon successful authentication. In my config.warden block, the authentication works fine but if I don't create a Devise user, I won't be authenticated. My ideal scenario requires me to either successfully authenticate against a 3rd party provider and sign into my app (using Devise without a corresponding Devise user record) or if I fail to authenticate, then try Devise standard login path.
Here is the devise.rb code snippet I got working but I have to create a devise user for the authentication to work, this is something I wish to avoid
config.warden do |manager|
manager.strategies.add(:custom_strategy) do
def valid?
params[:user] && params[:user][:email] && params[:user][:password]
end
def authenticate!
...perform authentication against 3rd party provider...
if successful_authentication
u = User.find_or_initialize_by_email(params[:user][:email])
if u.new_record?
u.app = 'blah'
u.save
end
success!(u)
end
end
end
manager.default_strategies(:scope => :user).unshift :custom_strategy
end

I realized the question is old but I saw it a couple of time when I was searching for a solution to similar thing so I decided to post the answer in case anyone in the future stumbles upon similar issue. Hope this will help!
I recently had to do similar thing -> had users in my database that were authenticated with some devise/warden strategies but had created another app that has to have access to some of the endpoints to my application. Basically I wanted to do a HMAC authentication.
But I didn't want to involve any user objects in that process and here is what I had to do (provided that you already have you custom strategy that authenticates incoming request without using user object)
create a fake user model that is used so that devise wont blow op. You dont have to create any database table for that
mine looked similar to below:
class Worker # no need to create a table for him
extend ActiveModel::Callbacks
extend Devise::Models
include ActiveModel::Validations
include Concerns::ObjectlessAuthenticatable
define_model_callbacks :validation
attr_accessor :id
def persisted
false
end
def initialize(id)
#id = id
end
def self.serialize_from_session(id)
self.new(id: id)
end
def self.serialize_into_session(record)
[record.id]
end
def self.http_authenticatable
false
end
end
then in devise initializer (/initializers/devise.rb) I've added separate authentication strategy like below:
...
config.warden do |manager|
manager.scope_defaults :user, :strategies => [
...strategies i was using for users
]
manager.scope_defaults :worker, :strategies => [:worker_authentication], store: false, action: 'unautenticated_worker'
manager.failure_app = CustomFailingApp
end
...
then in routes.rb I had to create a mapping for devise to use like so
devise_for :worker # you can pass some custom options here
then wherever I needed to authenticate the worker, not the user I just had to call (in the controller) authenticate_worker!

I would expect that this is against the design of devise where all actions are done using restful routes for a resource. That said, the comments in Warden's success! method say:
# Parameters:
# user - The user object to login. This object can be anything you have setup to serialize in and out of the session
So could you not change the object u to some other object that represents the user, like a plain old Hash?

Related

Run warden strategy in devise when user is already authenticated

I'm using devise to authenticate my users via username / password or a couple of OAuth providers. Now I have to support a custom authentication Strategy that uses a token inside a header.
module WardenStrategies
class ApiKey < Warden::Strategies::Base
def valid?
api_token.present?
end
def authenticate!
user = User.find_by(api_token: api_token)
if user
success!(user)
else
fail!("Invalid email or password")
end
end
private
def api_token
env["HTTP_AUTHORIZATION"].to_s.remove("Bearer ")
end
end
end
I've added the strategy to devise and warden
# config/initializers/devise.rb
Devise.setup do |config|
# ...
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :api_key
end
# ...
end
# config/initializers/warden.rb
Warden::Strategies.add(:api_key, WardenStrategies::ApiKey)
The problem now is: I have a playground on the same site that sends requests to my API via JavaScript. In the playground I can provide a custom API key for a different user. But when the user is already logged in via a normal session, warden doesn't run the strategy any more, because it already has a User assigned.
So, how can I force warden / devise to try a strategy, even when the user is already authenticated?
Before calling any strategies warden checks if there's already a stored user in session (from previous requests, where some strategy with store?=true (the default) has succeeded)
You can try faking a 'non-set' user (without full log out) by something like:
# manager is Warden::Manager
manager.prepend_on_request do |proxy|
proxy.set_user(nil, scope: :user, store: false) if proxy.env["HTTP_AUTHORIZATION"].present?
end
PS. your strategy probably should also have def store?; false; end, as api keys are usually required with each request, and also should not result in persisted session

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.

Rails 4.2.6 - Devise 4.2.2 - Default Auth

I am looking to set a secondary password by which I can authenticate a user for a login as from admin. The reason for this work around is the front end is a single page application.
Each user has been given a unique login_as string. now I need to configure Devise to compare the login_as if the password fails.
Any help is appreciated. I am of course open to an alternative solution if there is a better way.
Thanks.
This post from Duncan Robertson was very helpful in solving my issue. I essentially created an override strategy and called it in the devise.rb file. I had some concern regarding tampering with a large user base but it has proved successful. By adding a column to users named ":signin_as" and then setting it to a default unique string with a rake I then had what I needed to fallback on if the initial sign in failed.
the override strategy (config/initializers/auth_override.rb)
module Devise
module Strategies
class AuthOverride < Authenticatable
def custom_auth(user, signin_as)
if user[:signin_as] == signin_as
return true
else
return false
end
end
def authenticate!
user = User.find_by_email(email)
if user
if user.valid_password?(params[:password])
success!(user)
elsif custom_auth(user, params[:password])
success!(user)
else
fail
end
else
fail
end
end
end
end
end
including the strategy in devise (config/initializers/devise.rb)
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :auth_override
end

Rails devise disable password recovery for certain user types

In my Rails project I have different types of users one of which has the user_status :admin, which has full rights to edit content unlike the rest of the users. For obvious reasons I want to add additional security for these types of users, in particular, completely disable password recovery.
What is the correct way of overriding standard Devise password recovery (:recoverable Devise module) methods so that when a user tries to get a reset password link for a user which is an admin user (user_status == "admin") the system gives back the "standard email not found" message?
This is somewhat like the unanswered question: Restrict Devise password recovery to only certain users
Thank you in advance.
The method I chose and that worked for me was overriding the send_reset_password_instructions method of the User model by adding the following to models/user.rb:
def send_reset_password_instructions
return false if self.user_status == 'admin'
super
end
This makes Devise not do anything in case the email belongs to an admin account.
For any future viewers, here's another way to do it. Vitaly's example did work for me, but I was still getting the "Your password email has been sent." notice (I wanted a separate alert to flash), so I went another route.
Extending the Devise::PasswordsController was the easiest solution for me:
class Devise::Extends::PasswordsController < Devise::PasswordsController
def create
if some_condition?
redirect_to :root
flash[:alert] = 'You cannot reset your password, buddy.'
else
super
end
end
Then, in routes.rb:
devise_for :users, controllers: { passwords: 'devise/extends/passwords' }
That will direct your app to the extended controller, then hit the devise controller ("super") if your condition is not met.
Not tested, but I think you can overwrite the reset_password! in the User model as follows:
def reset_password!(new_password, new_password_confirmation)
return false if user_status == 'admin'
super
end
This prevents the password from being reset if the user is an admin.
I don't know if this is the best method to override, there are more devise recoverable methods that are candidate to be overwritten in your User model, ie send_reset_password_instructions. Check the manual for all the interesting methods.
Snippet above from Keller Martin works pretty well!
Some minor issues I faced are the following:
If you got uninitialized constant Devise::Extends (NameError) (probably it's just due to old ruby version?) then you can just use nested modules definition.
If you need to allow some action to run for non authenticated user then you can skip the filter.
Below is updated snippet.
module Devise
module Extends
class PasswordsController < Devise::PasswordsController
skip_before_filter :authenticate_user!, :only => [ :edit ]
def edit
redirect_to "https://www.google.com/"
end
end
end
end

Filtering users who are able to sign in with Devise

I have a Rails app using Devise for authentication. Users belong to Dealers and I want to prevent users who belong to disabled dealers from being able to sign in.
Is there a straightforward way to extend Devise's authentication finder so that it will not include users from deleted dealers? Perhaps using a named scope on User?
Cheers
Tristan
Turns out all I needed to do was override my user model's find_for_authentication method:
class User < ActiveRecord::Base
...
# Intercept Devise to check if DealershipUser's Dealership is active
def self.find_for_authentication(conditions)
user = super
return nil if user.is_a?(DealershipUser) && user.dealership.deleted?
user
end
...
end
Find the user in the normal way by calling super.
I'm using STI so I check that the user is a DealershipUser and then check if the dealership is deleted (acts_as_paranoid).
Return the user.
This is a very specific solution for my scenario but you could override find_for_authentication however you like provided you return the user afterwards.
Searching Stackoverflow.com gave me this question/answer: Custom authentication strategy for devise
Basically, you have to implement a custom authentication strategy at Warden's level (that underlies Devise). For my project, I did the following:
In config/initializers/devise.rb:
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :user_has_login_access
end
end
Warden::Strategies.add(:user_has_login_access) do
def valid?
# pass the commit parameter as 'login' or something like that, so that this strategy only activates when the user is trying to login
params[:commit] == 'login'
end
def authenticate!
u = User.find_by_email(params[:user][:email])
if u.can_login? # retrieves boolean value stored in the User model, set wherever
success! u
else
fail! "Account does not have login privilages."
end
end
end
You can read more about custom Warden strategies here: https://github.com/hassox/warden/wiki/Strategies
Hope that helps!

Resources