Creating a custom Devise Strategy - ruby-on-rails

Been fighting with this for a while now, not sure why it isn't working.
The gist is looking to use Devise with LDAP. I don't need to do anything except for authenticate so I have no need to use anything except for a custom strategy.
I created one based on https://github.com/plataformatec/devise/wiki/How-To:-Authenticate-via-LDAP and as far as I can tell everything should work except whenever I try to run the server (or rake route) I get a NameError
lib/devise/models.rb:88:in `const_get': uninitialized constant Devise::Models::LdapAuthenticatable (NameError)
I've traced the error down to my app/models/user.rb
class User < ActiveRecord::Base
devise :ldap_authenticatable, :rememberable, :trackable, :timeoutable
end
If I remove :ldap_authenticatable then the crash goes away but I have no routes to user#session and a login prompt cannot be accessed.
My supporting files:
lib/ldap_authenticatable.rb
require 'net/ldap'
require 'devise/strategies/authenticatable'
module Devise
module Strategies
class LdapAuthenticatable < Authenticatable
def authenticate!
if params[:user]
ldap = Net::LDAP.new
ldap.host = 'redacted'
ldap.port = 389
ldap.auth login, password
if ldap.bind
user = User.where(login: login).first_or_create do |user|
success!(user)
else
fail(:invalid_login)
end
end
end
def login
params[:user][:login]
end
def password
params[:user][:password]
end
end
end
end
Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable)
And finally, inside config/initializers/devise.rb
Devise.setup do |config|
# ==> LDAP Configuration
require 'ldap_authenticatable'
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :ldap_authenticatable
end
end
I've exhausted my searches, maybe someone can see something I am missing.
Cheers

Is your lib/ldap_authenticatable.rb is in the autoload path or explicitly required? Since Rails 3 code in lib folder isn't autoloaded by default any more. Here is one way on how to solve it
IMHO Devise is a great gem. However in order to write your own strategy you have to be familiar not only with Devise but with Warden source code as well, and a lot of boilerplate code needs to be written in various places, so I start to investigate how to make customizations easier for Devise and come up with this gem devise_custom_authenticatable. You can check it and probably it'll solve your problem in a different way. This gem is used in production code base for rather busy application, so it battle proven :)

File paths should match namespaces. You need to add 2 levels of directories.
mkdir lib/devise
mkdir lib/devise/strategies
mv lib/ldap_authenticatable.rb lib/devise/strategies/ldap_authenticatable.rb
Since you are namespaced to
module Devise
module Strategies
class LdapAuthenticatable < Authenticatable
...

few steps to take care while creating custom strategy:
you will have to take care of the strategies folder as #csi mentioned along with it create a models folder and inside models create ldap_authenticatable.rb . so the structure will look like this.
lib/devise/strategies/ldap_authenticatable.rb
lib/devise/models/ldap_authenticatable.rb
Add these lines to lib/devise/models/ldap_authenticatable.rb
require Rails.root.join('lib/devise/strategies/ldap_authenticatable')
module Devise
module Models
module LdapAuthenticatable
extend ActiveSupport::Concern
end
end
end
In config/initializers/devise.rb add these lines to the top.
Devise.add_module(:ldap_authenticatable, {
strategy: true,
controller: :sessions,
model: 'devise/models/ldap_authenticatable',
route: :session
})
This should take care of the custom auth.

Related

The reason why it's possible to use helper method like current_user, authenticate_user!, and so on without including any module

The title is my question.
devise provide us many useful methods like current_user, authenticate_user!, and so on. I want to know why is it possible to use them without including any module like below.
class ApplicationController < ActionController::Base
before_action :authenticate_user!
end
Those method's definition is here
Somebody please help me!
The answer is devise included its helper methods into ActionController on behalf of you when Rails on_load
# devise/rails.rb
module Devise
class Engine < ::Rails::Engine
# ...
initializer "devise.url_helpers" do
Devise.include_helpers(Devise::Controllers)
end
# ...
end
# devise.rb
# ...
def self.include_helpers(scope)
ActiveSupport.on_load(:action_controller) do
include scope::Helpers if defined?(scope::Helpers)
include scope::UrlHelpers
end
ActiveSupport.on_load(:action_view) do
include scope::UrlHelpers
end
end
# ...
I saw many 3rd gems using on_load to include their methods (or themselves) into Rails core, maybe it's a typical way to do that (allows Rails to lazily load a lot of components and thus making the app boot faster). If you install some gems and you could use their methods on your model/controller/view then those gems did the same thing devise did above.
About those methods current_user, authenticate_user! ... they are dynamic methods devise will generate when it did include scope::Helpers into Rails (see code above and the link).
So one very good thing about rails is the fact that you get a lot of things for free out of the box. One of these things at the top level is autoloading.
So in the case of Devise. When you install Devise and run the generate command, you get a devise.rb file inside of your config/initializers/ folder. This file is always autoloaded by rails on server startup and reload. That's how all these methods are able to be use these devise methods without importing anything.
Read more at rails docs

Devise: remove module for extended model

I got big legacy app with model User. I added new type of user:
class ExtendedUser < User
devise :database_authenticatable, ...
end
New type of user is extending existing one, so all code written for User should work also for ExtendedUser, without extending it is impossible.
Question is how to remove devise module from ExtendedUser which comes from User?
User is confirmable and ExtendedUser should not be.
I'm suspecting that removing module is impossible and easiest way is to put .skip_confirmation! before all places in code where this user will be created and where email can change.
You could just use inheritance to dynamically evaluate which modules are included:
class User < User
devise *devise_modules
# ...
def self.devise_modules
[:database_authenticatable, ...]
end
end
class ExtendedUser < User
# ...
def self.devise_modules
super.excluding(:confirmable)
end
end

Preview devise mailer not found

I try to have a preview of devise mailer but when it doesn't work.
I have followed this rails guide and it doesn't seems difficult and many things to do.
Devise already have a mailer class here
So I should only have to create a preview file so here is what I have done so far.
I generated my devise views in the repository app/views/users/mailer/
I have my devise.rbwhich look like that
Devise.setup do |config|
# ==> Mailer Configuration
# Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class
# with default "from" parameter.
config.mailer_sender = 'please-change-me-at-config-initializers-devise#example.com'
# Configure the class responsible to send e-mails.
config.mailer = 'Devise::Mailer'
# Configure the parent class responsible to send e-mails.
#config.parent_mailer = 'ActionMailer::Base'
end
I have my test/mailers/previews/devise_mailer_preview.rb
class DeviseMailerPreview< ActionMailer::Preview
def confirmation_instructions
Devise::Mailer.confirmation_instructions(User.first, "faketoken")
end
def reset_password_instructions
Devise::Mailer.reset_password_instructions(User.first, "faketoken")
end
end
I didn't change the path config.action_mailer.preview_pathin my application.rb because test/mailers/previews is the default path
So now when I try to access to http://localhost:3000/rails/mailers I have a white empty page
and if I try http://localhost:3000/rails/mailers/users/mailer/confirmation_instructions I have this error Mailer preview 'users/mailer/confirmation_instructions' not found
I tried many different link but still have the same error, I also tried to followed this stackoverflow answer but no success.
It's seems so easy but I can't succeed ...
Ok I found out why.
Because I'm using the gem rspec-rails the repository test is replaced tp spec
I just needed to move my test/mailers/previews/devise/mailer_preview.rbto spec/mailers/previews/devise/mailer_preview.rb and still not need to change the configuration of config/application.rb
Hope it will help some people
also note that in rails 6 your devise mailer preview should be wrote with a module
module Devise
class MailerPreview< ActionMailer::Preview
def confirmation_instructions
Devise::Mailer.confirmation_instructions(User.first, "faketoken")
end
def reset_password_instructions
Devise::Mailer.reset_password_instructions(User.first, "faketoken")
end
end
end
Moving the mailers folder worked for me!

How to extend middleware to custom classes outside the MVC scope (Rails)?

I am connecting to Shopify's API with the gem omniauth-shopify-oauth2.
In config/initializers/shopify.rb I am setting my OmniAuth::Builder like this:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :shopify, "my_shopify_api_key", "my_shopify_secret",
scope: 'read_orders',
setup: lambda { |env| params = Rack::Utils.parse_query(env['QUERY_STRING'])
env['omniauth.strategy'].options[:client_options][:site] = "http://#{params['shop']}" }
end
Then from any controller file I can access Shopify's API like this:
omniauth = request.env['omniauth.auth']
if omniauth && omniauth[:provider] && omniauth[:provider] == "shopify"
token = omniauth['credentials'].token
session = ShopifyAPI::Session.new("#{username}.myshopify.com", token)
ShopifyAPI::Base.activate_session(session)
# Do stuff with the session
ShopifyAPI::Base.clear_session
end
If, on the other hand, I move the controller functionality to e.g. app/my_custom_folder/my_custom_file.rb I get an error in the very first line of code:
NameError (undefined local variable or method `request' for #<MyCustomClass:0x007fe07ec11f68>):
I assume that the error is raised because my custom classes and files can't access the middleware functionality. So how can I extend the functionality?
Rails auto-requires everything in certain directories, but my_custom_folder isn't one of those.
You could add something like this to config/application.rb
config.autoload_paths += %W(
#{config.root}/my_custom_folder
)
Or, you could drop my_custom_file.rb in a blessed location like app/controllers/concerns
ALSO:
It seems to me that the code you moved to my_custom_file.rb probably also expects to be inside a controller, which has access to request (through ActionController::Base).
Without knowing what my_custom_file.rb looks like, I would propose you set it up as an include like so:
# in app/controllers/concerns/my_custom_file.rb
module MyCustomFile
extend ActiveSupport::Concern
included do
# if necessary to run stuff on include:
# before_action :do_some_oauth_shopify_stuff
end
# This will be treated as part of all controllers now:
def do_some_oauth_shopify_stuff
# your code
end
end
# near top of app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include MyCustomFile
# rest of application_controller.rb
end
And maybe pick a better name for the module/file.

Rails Devise attr_accessible problem

Im trying to add devise authorization to my rails 3 app.
Its all going well except Im also trying to follow this tutorial to dynamically set attr_accessible for role_ids only for admin users (I dont want regular users changing their role, but an admin should be able to do so)... the problem is, the railscast tutorial approach assumes I have access to change the controller behavior when in fact devise is handling all that under the hood.
Please Help
You can subclass the Devise controllers, you just have to generate the views and move them to the correct place. Check out "Configuring views" and "Configuring controllers" in the Devise readme.
I ended up adding role_ids to attr_accessible, then subclassing the RegistrationsController and adding a before_filter to remove that param for non-admins.
class Users::RegistrationsController < Devise::RegistrationsController
before_filter :remove_admin_params, :only => [:create, :update]
protected
# disable setting the role_ids value unless an admin is doing the edit.
def remove_admin_params
params[:user].delete(:role_ids) unless current_user.try(:admin?)
end
end
Just make sure to add the registration views to /app/views/users/registrations/.
The best way I found to handle this is from RailsCast 237. It is more verbose than Arrel's answer, but it does not force you to add role (or other fields) to attr_accessible.
Add the following method in an initializer:
class ActiveRecord::Base
attr_accessible
attr_accessor :accessible
private
def mass_assignment_authorizer(role = :default)
if accessible == :all
self.class.protected_attributes # hack
else
# super returns a whitelist object
super + (accessible || [])
end
end
end
Then in your controller, you can do:
user.accessible = :role if can? :set_role, resource
This call, unfortunately, has to be made after the user (or whatever) object has been instantiated. That means that you would have to subclass the controller, and call this after the resource instantiation in update and create.
This is for Rails 3.2. In earlier versions I believe the method mass_assignment_authorizer does not take a parameter. The attr_accessible with no values sets a fail-safe application wide denial for mass assignment. This can also be done in the application.rb file with
config.active_record.whitelist_attributes = true

Resources