ActiveAdmin: How to setup HTTP basic authentication? - ruby-on-rails

I want to set basic authentication for ActiveAdmin, which internal devise solution doesn't apply to my case. For that I would like to be able to add middleware to the ActiveAdmin Engine before this is bundled into my app. What I managed to do was:
ActiveAdmin::Engine.configure do |config|
config.middleware.use Rack::Auth::Basic do |username, password|
username == 'admin' && password == 'root'
end
end
But apparently this doesn't make it work, since my active admin routes are still unprotected. How can I effectively do this? And no, I don't want to protect my whole site with basic authentication.

Here's a few ideas:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
http_basic_authenticate_with :name => "frodo", :password => "thering", :if => :admin_controller?
def admin_controller?
self.class < ActiveAdmin::BaseController
end
Or, the monkeypatching version
# config/initializers/active_admin.rb
# somewhere outside the setup block
class ActiveAdmin::BaseController
http_basic_authenticate_with :name => "frodo", :password => "thering"
end
If you only want to protect specific resources, you can use the controller block:
# app/admin/users.rb
ActiveAdmin.register Users do
controller do
http_basic_authenticate_with :name => "frodo", :password => "thering"
end
# ...
end
I was hoping that I would be able to extend the controller in this way in config/initializers/active_admin.rb in the setup block, but this didn't work for me:
# app/admin/users.rb
ActiveAdmin.setup do |config|
config.controller do
http_basic_authenticate_with :name => "frodo", :password => "thering"
end
# ...
end
You might try it though, as it could be an ActiveAdmin version thing (I could have sworn that I saw that documented somewhere...)
Good luck, I hope this helps.
UPDATE: A couple more options:
I hadn't realized before that :before_filter in activeadmin config takes a block.
# config/initializers/active_admin.rb
ActiveAdmin.setup do |config|
# ...
config.before_filter do
authenticate_or_request_with_http_basic("Whatever") do |name, password|
name == "frodo" && password == "thering"
end
end
end
And... just one more idea. It sounds like you are not keen on adding anything to application_controller, but this version is not conditional like the first above:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def authenticate_admin
authenticate_or_request_with_http_basic("Whatever") do |name, password|
name == "frodo" && password == "thering"
end
end
end
# config/initializers/active_admin.rb
ActiveAdmin.setup do |config|
# ...
config.authentication_method = :authenticate_admin
end

If you just want to protect the admin area of ActiveAdmin, then you should try this:
# app/admin/dashboard.rb
controller do
http_basic_authenticate_with :name => "mega-admin", :password => "supersecret"
end
that works like a charm ;-)
have fun

just another solution for you would be:
# app/controllers/application_controller.rb
protected
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == "admin" && password == "superpassword"
end
end
# config/initializers/active_admin.rb
config.before_filter :authenticate
the big plus for this solution ist, that you can call
before_filter :authenticate
in every area you want to protet.

Related

Ruby on Rails: can I use http basic auth in constraints?

I use Rails 5 API mode
routes.rb:
Rails.application.routes.draw do
constraints Basic do
mount Rswag::Ui::Engine => '/api-docs'
mount Rswag::Api::Engine => '/api-docs'
mount Sidekiq::Web => '/sidekiq'
# etc
end
end
constraints/basic.rb:
class Basic
def self.matches?(request)
authenticate_or_request_with_http_basic do |email, password|
email == 'foo' && password = 'bar'
end
end
end
But I'm getting an error:
NoMethodError (undefined method `authenticate_or_request_with_http_basic' for Basic:Class)
Can I use http basic auth in constraints?
I dont think you can use authenticate_or_request_with_http_basic method out of controllers scope. You can set up before_filter with auth check in general controller. Here is an example taken from docs comments:
class AdminController < ApplicationController
before_filter :authenticate
def authenticate
authenticate_or_request_with_http_basic('Administration') do |email, password|
email == 'foo' && password == 'bar'
end
end
end
Also here is what I found Rails Authentication Routing Constraints Considered Harmful.
That being said I think there is a way:
class Basic
def self.matches?(request)
if ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
credentials = ActionController::HttpAuthentication::Basic.decode_credentials(request)
email, password = credentials.split(':')
email == 'foo' && password == 'bar'
end
end
end
Here is docs on HTTP Basic authentication with examples

ForbiddenAttributesError, only in tests, after minor version upgrade

I'm trying to upgrade from Rails 4.1.11 to 4.1.16. Several of my SessionsController specs fail after this upgrade. But when I try to log in in the development environment, everything seems to work fine.
I have the following code:
# sessions_controller.rb
def create
#identity = Identity.find_or_create(auth)
...
end
protected
def auth
request.env["omniauth.auth"]
end
# identity.rb
class Identity < ActiveRecord::Base
...
def self.find_or_create(auth)
where(auth.slice(:uid, :provider)).first_or_create
end
end
In my specs, I use a fixture in spec_helper.rb to supply the OmniAuth hash:
# spec_helper.rb
OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new({
:provider => 'facebook',
:uid => '1234567',
:info => {
:email => 'joe#bloggs.com',
...
}
}
When I try to use the OmniAuth attributes to find a user's identity, I get a ForbiddenAttributesError:
1) SessionsController GET :create not yet signed in sets the user ID in the session hash
Failure/Error: get :create
ActiveModel::ForbiddenAttributesError:
ActiveModel::ForbiddenAttributesError
# /Users/pat/.rvm/gems/ruby-2.1.2/gems/activemodel-4.1.6/lib/active_model/forbidden_attributes_protection.rb:21:in `sanitize_for_mass_assignment'
I understand the idea of strong parameters and whitelisting attributes in the params hash. But I can't use require on the OmniAuth hash (at least not directly), and I haven't had to in the past.
This is the example provided on OmniAuth's GitHub page:
class SessionsController < ApplicationController
def create
#user = User.find_or_create_from_auth_hash(auth_hash)
self.current_user = #user
redirect_to '/'
end
protected
def auth_hash
request.env['omniauth.auth']
end
end
Presumably, it works with recent versions of Rails.
How can I get my tests to pass? Here is the rest of the code if you want to take a look.
Have you tried whitelisting the parameters like this?
def auth
ActionController::Parameters.new(request.env["omniauth.auth"]).permit(...)
end
Try changing the rspec version in your gemfile to this:
gem "rspec-rails", '~> 2.14.0.rc1'

HTTP Basic Auth for some (not all) controllers

Using Rails 3.2.
I have half a dozen controllers, and want to protect some (but not all) of them with http_basic_authenticate_with.
I don't want to manually add http_basic_authenticate_with to each controller (I could add another controller in the future and forget to protect it!). It seems the answer is to put it in application_controller.rb with an :except arg which would list the controllers that should not be protected. The problem is, the :except clause wants method names rather than external controller module names, e.g.:
http_basic_authenticate_with :name => 'xxx', :password => 'yyy', :except => :foo, :bar
So then I thought "Wait, since I already have the protected controllers grouped in routes.rb, let's put it there." So I tried this in my routes:
scope "/billing" do
http_basic_authenticate_with :name ...
resources :foo, :bar ...
end
But now I get
undefined method `http_basic_authenticate_with'
What's the best way to approach this?
Do it the way Rails does it.
# rails/actionpack/lib/action_controller/metal/http_authentication.rb
def http_basic_authenticate_with(options = {})
before_action(options.except(:name, :password, :realm)) do
authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
name == options[:name] && password == options[:password]
end
end
end
All that http_basic_authenticate_with does is add a before_action. You can just as easily do the same yourself:
# application_controller.rb
before_action :http_basic_authenticate
def http_basic_authenticate
authenticate_or_request_with_http_basic do |name, password|
name == 'xxx' && password == 'yyy'
end
end
which means you can use skip_before_action in controllers where this behavior isn't desired:
# unprotected_controller.rb
skip_before_action :http_basic_authenticate

Conditional HTTP Basic Authentication

I want to implement HTTP basic authentication on my staging server, but only for those outside the local network. I have a Rails 3.1 app. In application.rb, I have the following:
class ApplicationController << ActionController::Base
http_basic_authenticate_with :realm => "Staging", :name => "user", :password => "password" if :need_authentication?
private
def need_authentication?
Rails.env == "staging" && request.remote_addr !~ /^192.168.0.\d{1,3}$/
end
end
Here's the rub: even when the need_authentication? method explicitly returns false, the app still asks me to authenticate, as if it's completely ignoring the if clause at the end.
So, is there any way to only require authentication under certain conditions?
In Rails 4, the :if condition works. For example,
class ApplicationController < ApplicationController::Base
http_basic_authenticate_with name: "user", password: "password" if Rails.env == 'staging'
end
or if you want a helper method to set the condition,
class ApplicationController < ApplicationController::Base
http_basic_authenticate_with name: "user", password: "password", if: :need_authentication?
private
def need_authentication?
Rails.env == 'staging'
end
end
This is what worked:
class ApplicationController < ActionController::Base
before_filter :authenticate_if_staging
private
def authenticate_if_staging
if Rails.env == 'staging' && request.remote_addr !~ /^192.168.0.\d{1,3}$/
authenticate_or_request_with_http_basic 'Staging' do |name, password|
name == 'username' && password == 'secret'
end
end
end
end
'Staging' is the name of the realm. This is not required, but can be used for clarification.

How do I create an admin subdomain to manage subdomains in Rails

I am using AuthLogic and the subdomain method that dhh covered in this blog post, everything is working great, and as expected. What I'm trying to figure out is how to create a subdomain like 'admin' or 'host' that will have a user authenticated from AuthLogic (this may be trivial and unnecessary to mention) that will manage the subdomains. So basically, all subdomains will act normally, except admin.site.com which will go to its own controller and layout..
dhh suggested just throwing in an exception to redirect, but I'm not sure where that goes, it didnt seem that simple to me, any ideas?
EDIT
I think that the fact I am using AuthLogic is important here, because the subdomain logic isnt forwarding users anywhere, once authenticated AuthLogic sends the user to /account - so my question may be related to how do I tell AuthLogic to a different spot if the user is a root user, logging into the admin subdomain..
Here is the code we have implemented thus far
Company Model
class Company < ActiveRecord::Base
has_many :users
has_many :brands, :dependent => :destroy
validates_presence_of :name, :phone, :subdomain
validates_format_of :subdomain, :with => /^[A-Za-z0-9-]+$/, :message => 'The subdomain can only contain alphanumeric characters and dashes.', :allow_blank => true
validates_uniqueness_of :subdomain, :case_sensitive => false
validates_exclusion_of :format, :in => %w( support blog billing help api www host admin manage ryan jeff allie), :message => "Subdomain {{value}} is not allowed."
before_validation :downcase_subdomain
protected
def downcase_subdomain
self.subdomain.downcase! if attribute_present?("subdomain")
end
end
SubdomainCompanies Module
module SubdomainCompanies
def self.included( controller )
controller.helper_method(:company_domain, :company_subdomain, :company_url, :company_account, :default_company_subdomain, :default_company_url)
end
protected
# TODO: need to handle www as well
def default_company_subdomain
''
end
def company_url( company_subdomain = default_company_subdomain, use_ssl = request.ssl? )
http_protocol(use_ssl) + company_host(company_subdomain)
end
def company_host( subdomain )
company_host = ''
company_host << subdomain + '.'
company_host << company_domain
end
def company_domain
company_domain = ''
company_domain << request.domain + request.port_string
end
def company_subdomain
request.subdomains.first || ''
end
def default_company_url( use_ssl = request.ssl? )
http_protocol(use_ssl) + company_domain
end
def current_company
Company.find_by_subdomain(company_subdomain)
end
def http_protocol( use_ssl = request.ssl? )
(use_ssl ? "https://" : "http://")
end
end
Application Controller
class ApplicationController < ActionController::Base
include SubdomainCompanies
rescue_from 'Acl9::AccessDenied', :with => :access_denied
helper :all # include all helpers, all the time
protect_from_forgery # See ActionController::RequestForgeryProtection for details
helper_method :current_user_session, :current_user, :current_company_name
filter_parameter_logging :password, :password_confirmation
before_filter :check_company_status
protected
def public_site?
company_subdomain == default_company_subdomain
end
def current_layout_name
public_site? ? 'public' : 'login'
end
def check_company_status
unless company_subdomain == default_company_subdomain
# TODO: this is where we could check to see if the account is active as well (paid, etc...)
redirect_to default_company_url if current_company.nil?
end
end
end
Look into subdomain-fu which allows you to route to different controllers and actions based on the subdomain. I have done a Railscasts Episode on the subject.
It might looks something like this.
# in routes.rb
map.manage_companies '', :controller => 'companies', :action => 'index', :conditions => { :subdomain => "admin" }
This will need to be high enough up in the routes list so nothing else is matched before it.
FOR RAILS 2.3: You can download a full example app (with a step-by-step tutorial) showing how to implement an admin subdomain, a main domain, and multiple user subdomains using the Devise gem for authentication and the subdomain-routes gem for managing subdomains. Here's the link: subdomain authentication for Rails 2.3.
FOR RAILS 3: Here's a complete example implementation of Rails 3 subdomains with authentication (along with a detailed tutorial). It's much easier to do this in Rails 3 than in Rails 2 (no plugin required).

Resources