authentication snippets

How to install the exception_logger Rails plugin and protect the logs with basic authentication

Tagged ruby, exception_logger, install, routes, rails, basic, authentication  Languages ruby

This snippet explains how to install and use the Rails exception_logger plugin. I'll also show you how to protect your logs by extending the plugin with basic authentication.

script/plugin source http://svn.techno-weenie.net/projects/plugins
script/plugin install exception_logger

I'm using Rails Edge on this project, so I had to install classic pagination also:

script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination

Next create and execute the migration file:

./script/generate exception_migration
rake db:migrate

Before starting the server we need to setup the routes:

map.exceptions '/logged_exceptions/:action/:id', :controller => 'logged_exceptions', :action => 'index', :id => nil

You also need to include the ExceptionLoggable in your ApplicationController:

class ApplicationController < ActionController::Base
  include ExceptionLoggable
...

Start your server and access the exception log at /logged_exceptions.

Exceptions can contain email addresses, passwords, credit card numbers, so you'll want to protect /logged_exceptions from the public. This can be done by adding the following code to the end of environment.rb:

config.after_initialize do
  require 'application' unless Object.const_defined?(:ApplicationController)
  LoggedExceptionsController.class_eval do
    before_filter :authenticate

    protected

    def authenticate
      authenticate_or_request_with_http_basic do |username, password|
        username == "foo" && password == "bar"
      end
    end
  end
end

With this code we add a before filter that shows a login dialog to anyone trying to access /logged_exception/. Note that this requires Rails 2.0 basic authentication to work, so make sure you have the proper version installed.

How to install and use the restful_authentication Rails plugin

Tagged rails, ruby, authentication, restful_authentication  Languages ruby

This is an adaptation of the restful_authentication screencast by Ryan Bates, which has an issue with Rails 2.0.3 that throws the following error:

NameError (uninitialized constant SessionsController):
    /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:266:in load_missing_constant'
    /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:453:in const_missing'
    /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:465:in const_missing'
    /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/inflector.rb:257:in constantize'

Installing the restful_authentication plugin

script/plugin source http://svn.techno-weenie.net/projects/plugins/
script/plugin install restful_authentication

Generating the model and controller

script/generate authenticated user sessions

Now run the migration:

rake db:migrate

Configure routing

Open config/routes.rb and add the following routes:

map.resources :users
map.resource  :session

map.signup '/signup', :controller => 'users', :action => 'new'
map.login  '/login', :controller => 'sessions', :action => 'new'
map.logout '/logout', :controller => 'sessions', :action => 'destroy'

Include restful_authentication in ApplicationController

First remove these lines from the users and sessions controllers:

# Be sure to include AuthenticationSystem in Application Controller instead
  include AuthenticatedSystem

Now include restful_authentication in the application controller:

class ApplicationController < ActionController::Base
  include AuthenticatedSystem

Integrate restful_authentication with your views

First let's create a controller and view by executing the generate script:

script/generate controller home index

Modify index.html.erb as follows:

<h1>Welcome</h1>

<% if logged_in? %>
  <p><strong>You are logged in as <%=h current_user.login %></strong></p>
  <p><%= link_to 'Logout', logout_path %></p>
<% else %>
  <p><strong>You are currently not logged in.</strong></p>
  <p>
    <%= link_to 'Login', login_path %> or
    <%= link_to 'Sign Up', signup_path %>
  </p>
<% end %>

Start Rails and access your application. If needed, add the following to config/routes.rb to make the home controller the default:

map.root :controller => "home"

Login, sign up and logout should work.

How to add OpenID support to your Rails application with the open_id_authentication plugin

Tagged openid, authentication, rails, ruby, plugin, restful_authentication  Languages ruby

These instructions have been tested with Rails 2.0.2 and ruby-openid 2.0.4. The snippet is an adaptation of the instructions in Ryan Bates' screencast on how to integrate OpenID with Rails.

Installing and configuring the restful_authentication plugin

Follow these instructions: How to install and use the restful_authentication Rails plugin.

Installing the ruby-openid gem

gem install ruby-openid

Installing the open_id_authentication Rails plugin

script/plugin source http://svn.techno-weenie.net/projects/plugins/
script/plugin install open_id_authentication

Create the migration files

rake open_id_authentication:db:create

Add the following to the self.up method in 002_add_open_id_authentication_tables.rb:

add_column :users, :identity_url, :string

Configuring the routes

map.open_id_complete 'session', :controller => "sessions", :action => "create", :requirements => { :method => :get }

Protect the identity_url field

Next protect the identity_url field, by adding the following to user.rb, account.rb or your custom user model:

attr_accessible :login, :email, :password, :password_confirmation, :identity_url

Add the following to the self.down method in 002_add_open_id_authentication_tables.rb:

remove_column :users, :identity_url

Integrating Open-id with the login page

Add the following to sessions/new.html.erb:

<label for="openid_url">OpenID URL</label><br />
<%= text_field_tag "openid_url" %>

Make sure you're showing flash messages, otherwise you won't see the error messages:

<html>
  <head></head>
  <body>
    <%= [:notice, :error].collect {|type| content_tag('div', flash[type], :id => type) if flash[type] } %>

    <%= yield %>
  </body>
</html>

Modifying the sessions controller

Copy & paste the following code in app/controllers/sessions_controller.rb:

class SessionsController < ApplicationController
  # Hack to fix: No action responded to show
  def show
    create
  end

  def create
    if using_open_id?
      open_id_authentication(params[:openid_url])
    else
      password_authentication(params[:login], params[:password])
    end
  end

  def destroy
    self.current_user.forget_me if logged_in?
    cookies.delete :auth_token
    reset_session
    flash[:notice] = "You have been logged out."
    redirect_back_or_default('/')
  end

  protected

  def open_id_authentication(openid_url)
    authenticate_with_open_id(openid_url, :required => [:nickname, :email]) do |result, identity_url, registration|
      if result.successful?
        @user = User.find_or_initialize_by_identity_url(identity_url)
        if @user.new_record?
          @user.login = registration['nickname']
          @user.email = registration['email']
          @user.save(false)
        end
        self.current_user = @user
        successful_login
      else
        failed_login result.message
      end
    end
  end

  def password_authentication(login, password)
    self.current_user = User.authenticate(login, password)
    if logged_in?
      successful_login
    else
      failed_login
    end
  end

  def failed_login(message = "Authentication failed.")
    flash.now[:error] = message
    render :action => 'new'
  end

  def successful_login
    if params[:remember_me] == "1"
      self.current_user.remember_me
      cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
    end
    redirect_back_or_default('/')
    flash[:notice] = "Logged in successfully"
  end
end

OpenID authentication from behind a proxy

First, set the HTTP_PROXY environment variable to the proxy URL:

export HTTP_PROXY=http://proxy.aktagon.com:8080/

Then add the following to environment.rb:

OpenID::fetcher_use_env_http_proxy

How to implement multiple OmniAuth strategies for Devise

Tagged authentication, facebook, devise, omniauth  Languages ruby

This snippet is useful if you're using Devise and OmniAuth for authentication and need to support authentication with, for example, two separate Facebook applications.

First, put this in, for example, lib/omniauth_i18n.rb:

# The "name" method defines the URL, so the URL becomes /users/auth/facebook_x
module OmniAuth::Strategies
   class FacebookX < Facebook
    def name 
       :facebook_x
    end 
  end
  class FacebookY < Facebook
    def name 
       :facebook_y
    end 
  end
end

Configure each strategy separately in config/initializers/devise.rb:

config.omniauth :facebook_x, 'yyy', 'yyy'
config.omniauth :facebook_y, 'zzz', 'zzz'

This was tested with devise 1.1, oa-auth 0.1.6 and oauth 0.4.4, and will probably break sometime in the future. OmniAuth supports dynamic strategies

Brute-Force Authentication Protection with ModSecurity

Tagged modsecurity, brute-force, authentication  Languages apacheconf

IP-Based Blocking

The following ModSecurity script protects from brute-force authentication attacks by blocking IPs. It does this by checking the response code sent by the login page (/sessions). HTTP status 200 means a failed authentication attempt. After 3 attempts the IP is blocked.

<LocationMatch /sessions>
         # Uncomment to troubleshoot
        #SecDebugLogLevel 9
        #SecDebugLog /tmp/troubleshooting.log

        # Enforce an existing IP address block
        SecRule IP:bf_block "@eq 1" \
                "phase:2,deny,\
                msg:'IP address blocked because of suspected brute-force attack'"

        # Check that this is a POST
        SecRule REQUEST_METHOD "@streq POST" "phase:5,chain,t:none,nolog,pass"
                # AND Check for authentication failure and increment counters
                # NOTE this is for a Rails application, you probably need to customize this
                SecRule RESPONSE_STATUS "^200" \
                        "setvar:IP.bf_counter=+1"

        # Check for too many failures from a single IP address. Block for 10 minutes.
        SecRule IP:bf_counter "@ge 3" \
                "phase:5,pass,t:none, \
                setvar:IP.bf_block,\
                setvar:!IP.bf_counter,\
                expirevar:IP.bf_block=600"
</LocationMatch>

Username-based Blocking

A serious hacker will have billions of IPs, yes billions in the near future, so it's better to block by username. To block usernames, use this script:

<LocationMatch /sessions>
        # Retrieve the username
        SecAction phase:2,nolog,pass,initcol:USER=%{ARGS.username}

        # Enforce an existing username block
        SecRule USER:bf_block "@eq 1" \
                "phase:2,deny,\
                msg:'Username \"%{ARGS.username}\" blocked because of suspected brute-force attack'"

        # Check that this is a POST
        SecRule REQUEST_METHOD "@streq POST" "phase:5,chain,t:none,nolog,pass"
                # AND Check for authentication failure and increment counters
                # NOTE this is for a Rails application, you probably need to customize this
                SecRule RESPONSE_STATUS "^200" \
                        "setvar:IP.bf_counter=+1"

        # Check for too many failures for a single username
        SecRule USER:bf_counter "@ge 3" \
                "phase:5,t:none,pass,\
                setvar:USER.bf_block,\
                setvar:!USER.bf_counter,\
                expirevar:USER.bf_block=600"
</LocationMatch>

Password-based Blocking

Hackers might want to try a reverse brute-force attack on passwords, so you could also block multiple failed login attemps that use the same password. Just modify the script to read the password parameter:

# Retrieve the password parameter
        SecAction phase:2,nolog,pass,initcol:USER=%{ARGS.password}

Note, you might want to use the RESOURCE collection instead of USER, if you're blocking both usernames and passwords.

On Learning ModSecurity

* Buy the ModSecurity Handbook * Use Lua when possible. ModSecurity rules are severely limited by the Apache configuration language. * Use chain for controlling flow. chain = AND operator. OR operator is |. * There are 5 collections: GLOBAL, IP, RESOURCE, SESSION, USER. Future versions might give you unlimited collections... * Use LocationMatch and SecDebugLog when troubleshooting:

<LocationMatch /sessions>
        SecDebugLogLevel 9
        SecDebugLog /tmp/troubleshooting.log

How to Test Authentication With Devise+Capybara+Minitest

Tagged devise, capybara, authentication, warden, transaction, gotcha, minitest  Languages ruby

Testing authentication functionality with Capybara and Devise? See the following checklist:

* Use shared connections or disable transactional fixtures. * Set Capybara.default_host to match config.session_store.domain or you'll get "401 Unauthorized" * Name of test should end with "integration", e.g. describe "Dashboard Business integration" do * Add the following to your integration tests:

include Warden::Test::Helpers
  Warden.test_mode!

  after do
    Warden.test_reset!
  end

Full example of integration test with Devise, Capybara, and minitest:

class IntegrationSpec < MiniTest::Spec
  include Rails.application.routes.url_helpers
  include Capybara::DSL
  include Warden::Test::Helpers
  Warden.test_mode!

  before do
    @routes = Rails.application.routes
  end

  after do
    Warden.test_reset!
  end

  def sign_in(user)
    login_as(user, scope: :user)
  end

  def sign_out
    logout(:user)
  end

  def default_url_options
    Rails.configuration.action_mailer.default_url_options
  end
end

MiniTest::Spec.register_spec_type( /integration$/, IntegrationSpec )

How to Implement Client-Cert Authentication with Apache (Smart Cards, HST-kortti, FINEID)

Tagged rails, hst-kortti, fineid, smart card, authentication  Languages bash

The goal is to automatically sign in users who have an SSL client-certificate issued by a known certificate authority, e.g. Finnish Väestörekisteri (VRK).

First, we’re going to install and configure Apache 2.2 for client-cert authentication.

Install Apache 2.2

$ brew install -v httpd22.rb 2>&1

Download VRK Certificates

In this example we’re using VRK’s root test certificate. Make sure you pick the right certificate for your purposes, e.g. the one for healthcare professionals.

VRK certificates can be downloaded here: http://fineid.fi/default.aspx?docid=2293&site=10&id=597

$ cd /usr/local/etc/apache2/2.2/
$ mkdir ssl
$ wget http://vrk.fineid.fi/certs/vrktestc.crt
$ mv vrktestc.crt ssl/
# Convert from DER to PEM format
$ openssl x509 -in ssl/vrktestc.crt -inform DER -outform PEM -out ssl/vrktestc.pem

Download VRK Revocation List

Revocation lists for VRK’s client certificates can be found here: http://fineid.fi/default.aspx?docid=2330&site=10&id=588#whereistherevocationlist

$ cd /usr/local/etc/apache2/2.2/
$ wget http://proxy.fineid.fi/crl/vrkcqcc.crl
$ mv vrkcqcc.crl ssl/
# Convert CRL file from DER to PEM format
$ openssl crl -in ssl/vrkcqcc.crl -inform der -out ssl/vrkcqcc.crl.pem

More about OpenSSL's CRL command.

Generate Self-Signed Certificate for Apache

If you have a real certificate, skip this part.

$ cd /usr/local/etc/apache2/2.2/
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ssl/server.key -out ssl/server.crt

Configure Apache

<VirtualHost xxx:443>

ProxyPreserveHost On
ProxyPass / http://0.0.0.0:8888/

SSLCertificateFile "/usr/local/etc/apache2/2.2/ssl/server.crt"
SSLCertificateKeyFile "/usr/local/etc/apache2/2.2/ssl/server.key"
SSLCACertificateFile "/usr/local/etc/apache2/2.2/ssl/vrktestc.pem" 
SSLVerifyClient optional # none, optional, require and optional_no_ca
SSLVerifyDepth 2 # Root certificate requires depth >= 2

# Let Rails know we’re using HTTPS
RequestHeader set X_FORWARDED_PROTO 'https'

#
# Show a help page if client authentication fails.
#
RewriteEngine On
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !^SUCCESS$
RewriteRule .* /ssl-client-verify-failed.html [L]

#
# Forward certificate information to application
# See http://httpd.apache.org/docs/2.2/mod/mod_ssl.html#envvars
#
# Subject's certificate
RequestHeader set SSL_CLIENT_S_DN "%{SSL_CLIENT_S_DN}s"
RequestHeader set SSL_SERVER_S_DN_OU "%{SSL_SERVER_S_DN_OU}s"
# Issuer's certificate
RequestHeader set SSL_CLIENT_I_DN "%{SSL_CLIENT_I_DN}s"
# Verification status: NONE, SUCCESS, GENEROUS or FAILED:reason
RequestHeader set SSL_CLIENT_VERIFY "%{SSL_CLIENT_VERIFY}s"


# Optional settings
#
# Require issuer of certificate to have a specific O and OU:
<Location />
    SSLRequire %{SSL_CLIENT_I_DN_O} eq "Vaestorekisterikeskus TEST" and \
           %{SSL_CLIENT_I_DN_OU} eq "Terveydenhuollon testiammattivarmenteet"
</Location>

# Export SSL and certificate variables
# SSLOptions +ExportCertData +StrictRequire +StdEnvVars

# Revocation list
SSLCARevocationFile "/usr/local/etc/apache2/2.2/ssl/vrkcqcc.crl.pem"

Remember to harden your SSL configuration.

Troubleshooting & FAQ

How can I verify that the SSL-certificate is set up properly?

$ openssl s_client -connect localhost:443 -showcerts

Acceptable client certificate CA names
/C=FI/ST=Finland/O=Vaestorekisterikeskus TEST/OU=Certification Authority Services/OU=Varmennepalvelut/CN=VRK TEST Root CA

How can I verify the client certificate is valid?

Export the client certificate (e.g. via browser) to a file named, e.g., atte-mussolini.pem and verify it against the CA file:

cat atte-mussolini.pem | openssl verify -CAfile /usr/local/etc/apache2/2.2/ssl/vrktestc.pem

Why am I getting SSL handshake failures?

Invalid self-signed certificate?

Removing the smart card or disconnecting the smart card reader will close the browser

This means you need to expire the session when the browser is closed. In a Rails application you need to remove expire_after from session_store.rb.

Browser issues

Browers might perform client-cert authentication in different ways. Many browsers have bugs related to client-cert authentication.

Inserting the smart card after starting the browser might mean the SSL client certificate’s information is not sent to the server.

If you’re having issues, the best solution is usually to restart the browser.

Mac and Safari issues

Safari might present the wrong certificate to the server. You can try to tell Safari to use the right certificate by opening the Keychain Access app and specifying a URL, either full or partial depending on the Safari version, for the certificate you want to use with a specific URL. This is done by adding a "New Certificate Preference" to the certificate.

Safari might not like the "SSLVerifyClient optional", see http://www.mnxsolutions.com/apache/safari-providing-an-ssl-error-client-certificate-rejected%E2%80%9D-when-other-browsers-work.html

One solution to this is using Chrome or Firefox.

Also see:

  1. http://support.apple.com/kb/HT1679
  2. http://support.apple.com/kb/TA22353
  3. FINEID + MAC

Another solution I haven't tried might be to use the "SSLCADNRequestFile" setting in mod_ssl.

Browser sends the wrong certificate

Try using the SSLCADNRequestFile mod_ssl setting to list the certificate authorities your server accepts:

See the section on certificate_authorities in rfc4346:

certificate_authorities
         A list of the distinguished names of acceptable certificate
         authorities.  These distinguished names may specify a desired
         distinguished name for a root CA or for a subordinate CA; thus,
         this message can be used to describe both known roots and a
         desired authorization space.  If the certificate_authorities
         list is empty then the client MAY send any certificate of the
         appropriate ClientCertificateType, unless there is some
         external arrangement to the contrary.

Apache is unable to read the revocation list (CRL)

VRK’s revocation certificate is not in the format required by Apache’s mod_ssl, i.e. the file doesn’t begin with: ----~~BEGIN X509 CRL----~~

This means you need to convert the CRL to PEM format using the following command:

$ openssl crl -in vrkcqcc.crl -inform der >revoked.pem

Note that you can also use the LDAP API to get a list of revoked cards. Apache doesn’t support LDAP revocation.

Implementing Client-Cert Authentication in Web Applications

An application that needs to support client-cert authentication should implement the following system and user stories:

* Enable client-cert authentication (user)

As a user I want to enable client-cert authentication, so that I don’t need to sign in manually.

* Disable client-cert authentication (user)

* Sign in user with client certificate (system)

In Ruby on Rails you can access the client-certification information through the headers set by Apache:

request.headers['HTTP_SSL_CLIENT_S_DN'] == user's distinguished name (DN)
request.headers['HTTP_SSL_CLIENT_I_DN'] == issuer's DN
request.headers['HTTP_SSL_CLIENT_VERIFY'] == 'SUCCESS'