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.