ben vandgrift · on living the dream

Secure Communication for Rails Applications (3)

User Authentication

This is the third segment in a series dealing with securing the environment of a rails environment. Previously: an introduction, and setting up a certificate authority.

Today, we'll be fleshing out the application that lives in our environment, and authenticating a user via their certificate.

Application: Retsyn

We're dealing with certs, after all.

Our goal: create an authentication system that identifies a user via a certificate. The user submits a certificate as part of the request, we verify it and use its public key to identify the user. We use the public key, since that's what gets transmitted back on verification to the app by webrick, apache, and nginx.

You can find the source for the app in progress on github.

Retsyn User

We created our certificates and keys in the last post; now we create a bare rails app, and add a User model:

rails generate model user name:string public_key:string

Since we're using a public key as an identifier, increase the :limit of the public_key before you run the migration. Skipping this step will mean the public keys assigned to the users may be truncated (silently), and won't resolve when we're searching for them later. Also, don't forget the index:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :name, :null => false
      t.string :public_key, :limit => 2048, :null => false

      t.timestamps
    end
    add_index :users, :public_key
  end

  def self.down
    drop_table :users
  end
end

We have a User model, now we need a simple user. Take your user.p12 file from above and extract the public key:

openssl pkcs12 -in path/to/user.p12 -out user.pubkey -clcerts -nokeys

The public key block is the only thing we care about at the moment. From the rails console, create a user with the appropriate public key (be sure and include the trailing newline!), and a name that suits your taste. I used "User". This will provide us with a user to break in our authentication system against.

Aside: SSL Via Webrick?

Yes, it can be done. You'll need to add the following to the top of your script/rails file:

require 'rubygems'
require 'rails/commands/server'
require 'rack'
require 'webrick'
require 'webrick/https'

module Rails
  class Server < ::Rack::Server
    def default_options
      super.merge({
          :Port => 3443,
          :environment => (ENV['RAILS_ENV'] || "development").dup,
          :daemonize => false,
          :debugger => false,
          :pid => File.expand_path("tmp/pids/server.pid"),
          :config => File.expand_path("config.ru"),
          :SSLEnable => true,
          :SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER,
          :SSLPrivateKey => OpenSSL::PKey::RSA.new(
                 File.open("path/to/your/server/key").read),
          :SSLCertificate => OpenSSL::X509::Certificate.new(
                 File.open("path/to/your/server/cert").read),
          :SSLCACertificateFile => 'path/to/your/ca/cert',
          :SSLCertName => [["CN", WEBrick::Utils::getservername]]
      })
    end
  end
end

What's going on here?

We need to instruct Webrick to require SSL connections:

:SSLEnable => true

Recall from the discussion of the SSL handshaking process that an SSL server may request a certificate from a client attempting to establish an SSL connection. We've instructed the Webrick server above to do so with the folowing lines. The first line requires a verification, the second gives Webrick the location of the CA Certificate we created to validate the client certificate against. If the client certificate is present and signed by our CA, it will be passed along by Webrick as a part of the request. If not, Webrick will not populate the request with the client cert.

:SSLVerifyClient => OpenSSL::SSL::VERIFY_PEER
:SSLCACertificateFile => 'path/to/your/ca/cert',

Finally, we provide a certificate and key (created earlier) for Webrick to send to connecting clients, so they can validate our credentials:

:SSLPrivateKey => OpenSSL::PKey::RSA.new(
       File.open("path/to/your/server/key").read),
:SSLCertificate => OpenSSL::X509::Certificate.new(
       File.open("path/to/your/server/cert").read),

The process for configuring apache or nginx is similar, and we'll address that later.

Authenticating Users

While Webrick will reject invalid certificates, we still want to ensure than any connecting user is present in our application. We will check this by adding a before filter to application_controller.rb:

before_filter :authenticate

def authenticate
  head :status => 403 and return unless current_user
end

helper_method :current_user
def current_user
  client_cert = request.env['SSL_CLIENT_CERT']
  return nil if client_cert.nil? || client_cert.blank?
  @current_user ||= User.find_by_public_key(client_cert)
end

Our web server will add the public key to the request's environment as 'SSL_CLIENT_CERT', so that's what we check against. If either the cert or the user is not present, it will quickly eject them from the system.

Connecting to Retsyn

Start the web server and connect with curl:

curl -kv https://localhost:3443

The verbose output (the -v flag at work) that walks you through what's happening.

The handshake:

...
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Server key exchange (12):
* SSLv3, TLS handshake, Request CERT (13):
* SSLv3, TLS handshake, Server finished (14):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Client key exchange (16):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSL connection using DHE-RSA-AES256-SHA
...

The request:

GET / HTTP/1.1
User-Agent: [stuff]
Host: localhost:3443
Accept: */*

And the response:

HTTP/1.1 403 Forbidden 
Content-Type: text/html; charset=utf-8
X-Ua-Compatible: IE=Edge
Cache-Control: no-cache
X-Runtime: 0.018457
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-02-18) OpenSSL/1.0.0d
...

Curl is sending nothing when a certificate is being requested, so our authenticate filter is kicking back a 403 response, as it should. If you pass in the user's cert and key, however, things look a little more rosy:

curl -k --cert certs/user.pem:test --key certs/user.key https://localhost:3443

Response:

HTTP/1.1 200 OK 
...

A browser makes a connection the same way curl does. If a certificate isn't present, the server will return a 403 Forbidden response. To properly authenticate, you'll need to install the user.p12 file somewhere your browser can get to it. On Mac OS X 10.6.7, this is done through Keychain Access for webkit browsers, or via
Preferences > Advanced > Encryption > View Certificates using Firefox.

Adding Users

We should build a simple management console, as entering users via the console is going to get old pretty quickly. We should also add a validation on the User model ensuring that the submitted public keys are well-formed:

require 'openssl'

class User < ActiveRecord::Base
  validate :public_key_resolves
  
  def public_key_resolves
    OpenSSL::X509::Certificate.new(self.public_key)
  rescue OpenSSL::X509::CertificateError => e
    errors.add(:public_key, "Certificate error: #{e.message}")
  end
end

Creating a user with a malformed public_key will now generate a reasonable error.

Next

Users and authentication are done! In the next post, we'll add secure file storage to our application, using both drive- and file-based encryption.

written: Jun 17 2011