Company wide Single Sign On(SSO) using Devise custom authentication strategy

It is a common requirement to have a single sign on for multiple software services provided by a company, something like google account with multiple google services like gmail,docs,google+ . In order to do this it would be best to have a RESTful application responsible for authentication and have all other services authenticate against this application.

To authenticate users of our rails application against an external API we add a custom authentication strategy to devise. Devise is a flexible authentication solution for Rails based on Warden. Warden uses the concept of cascading strategies to determine if a request should be authenticated. Warden will try strategies one after another until either, – One succeeds – No Strategies are found relevant – A strategy Fails.

These are the steps to add custom authentication strategy.

1. Create your own strategy implementation inheriting from Devise::Strategies::Base

railsapp/lib/custom_auth.rb

moduleCustomAuth

  moduleDevise
    moduleStrategies
      classFromSession < ::Devise::Strategies::Base
        defvalid?
          # this strategy is only valid if there is a url_token
          # in the params hash.
          # e.g. http://myapp?url_token=mysecrettoken
          params[:url_token]
        end
        defauthenticate!         
          # lookup session data with external api
          session_data = get_session_data_from_api(params[:url_token])
          
          # check if token was valid and authorise if so
          ifsession_data['error']
            # session lookup failed so fail authentication with message from api 
            fail!(session_data['error'])
          else
            # we got some valid user data
            success!(User.find(session_data['user_id']))
          end
        end
      end
    end
  end
end
2. Add this strategy to warden
railsapp/config/initializers/devise.rb
config.warden do|manager|

  manager.strategies.add(:custom_auth, CustomAuth::Devise::Strategies::FromSession)
  manager.default_strategies(:scope=> :user).unshift :custom_auth
end
Devise flow diagram
Image
  1. The HTTP request enters the rack stack.
  2. Warden gets the request and forwards it in the rack stack, adding an environment variable “warden” that points to an authentication proxy.
  3. The request gets dispatched to the rails controller, which may call authenticate_user! from a filter. This is an alias for request.env[‘warden’].authenticate!(:scope => :user).
  4. The warden proxy picks an authentication strategy. Any strategy for which valid? returns true is tried.
  5. When authentication succeeds, a user object is returned to the controller. When it fails, the symbol :warden is thrown down the stack, and caught by the warden rack application. The latter will return a response, which is a redirect to the login page by default. This can be overridden by calling warden.custom_response!.
Testing custom authentication strategy
To test the custom authentication strategy use Fakeweb and get a list of responses from the external API an add different test cases.
References

Cross Domain Requests

In order to generate a rich web experience, it is not uncommon to make requests to servers in other domains. Unfortunately, JavaScript’s same origin policy prohibits you from making a request to a server in another domain. There are many ways to overcome this restriction like, using Jquery’s .ajax method, HTML5 CORS(Cross-Origin Resource Sharing) and JSONP.

JSONP is the simplest way to make a cross domain request. JSONP or “JSON with padding” is a technique that can be used to load JavaScript from a server in a different domain. JSONP takes advantage of the fact that JavaScript’s same origin policy doesn’t apply to the HTML <script> element.

In order to make a call dynamically insert a new <script> tag into your page. The source of that script tag attribute is set to a resource in another domain. This URL includes a callback parameter corresponding to a local JavaScript function. The server in the other domain responds with JavaScript that calls this function, passing JSON as an argument. The client then evaluates this JavaScript, giving it access to the server’s data.

Here is the javascript code that makes the JSONP request.

<html>

<head>

// function to make cross domain call

function loadScript(url)
{
// adding the script tag to the head
var head = document.getElementsByTagName(‘head’)[0];
var script = document.createElement(‘script’);
script.type = ‘text/javascript’;
script.src = url;

// fire the loading
head.appendChild(script);
}

function callbackfn(json){

//handle the json

}

</head>

<body>

<button id=”load” onclick=’loadScript(“url&callback=callbackfn”)’>Deals</button>

</body>

</html>

The following link is an app that is used to get deals around you using jsonp to make the call to an external API.

Jquery’s .ajax() method can be used to make cross domain requests but the app would be much lighter if we use JSONP since we dont have to include the jquery library.

JSONP is only useful if we have to make GET request to the external domain. We can’t use POST as a request method

HTML5 GeoLocation

Many websites can benefit from knowing exactly where the user accessing their site is from, enabling a more personalized local experience. As part of the HTML5 efforts, the W3C has developed a set of APIs to effectively allow the client-side device (i.e. your iPhone 3G+, Android 2.0+ phones, or even your conventinal desktop browsers) to retrieve geographic positioning information with JavaScript.

To get the current position, use the getCurrentPosition() API call. When called, it must immediately return and then asynchronously acquire a new Position object. If successful, this method must invoke its associated successCallback argument with a Position object as an argument. If the attempt fails, and the method was invoked with a non-null errorCallback argument, this method must invoke the errorCallback with a PositionError object as an argument.

In order to protect the privacy of the user, when the browser is loaded the user is initially prompted to disclose this current location of deny it.

This is the prompt on Firefox.

Here is the code snippet that is used to obtain the current position.

<!DOCTYPE html> // indicate that it is a HTML5 doc
<html>
<head>

<!– initialize for google maps –>
<meta name=”viewport” content=”initial-scale=1.0, user-scalable=no” />

</head>

<body onload=”initialize()”>

</body>

</html>

function initialize() {
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(handle_geolocation_query, handle_errors);
}

function handle_errors(error)
{
switch(error.code)
{
case error.PERMISSION_DENIED: alert(“user did not share geolocation data”);
break;

case error.POSITION_UNAVAILABLE: alert(“could not detect current position”);
break;

case error.TIMEOUT: alert(“retrieving position timedout”);
break;

default: alert(“unknown error”);
break;
}
}

function handle_geolocation_query(position){
clat = position.coords.latitude;
clng = position.coords.longitude;

console.log(‘values to map current location’ + ‘lat: ‘ + clat + ‘ lng: ‘ + clng);

}
}

Many kinds of applications and games can enriched by adding automatic location information. The location API can be paired up with google maps, bing maps or proprietary geolocation data to provide compelling solutions.

In this post I will show how to pair location API with google maps.

To display the position on the map we need to include the google maps library in the head tag.

<script type=”text/javascript” src=”http://maps.google.com/maps/api/js?sensor=false”></script&gt;

sensor=false implies that the devise has no inbuilt location detection capabilities, like GPS.

The location API response contains, the coords and timestamp fields. The timestamp field simply denotes the time at which the instance of geolocation data was created. The coords attribute contains a set of geographic coordinates together with their associated accuracy, as well as a set of other optional attributes such as altitude and speed.

Before initializing the map we need to create a LatLng object with the latitude and longitude.

Initializing the Map

var myLatlng = new google.maps.LatLng(38,-97);
var myOptions = {
zoom: 3,
center: myLatlng,
mapTypeId: google.maps.MapTypeId.ROADMAP
}
mymap = new google.maps.Map(document.getElementById(“map”), myOptions);

map” is the id of the div in the body of the html.

Once we have initialized the map, we can add markers to the map and display information for each marker. Store the initialized map and latitude and longitude in global variables called mymap, clat, clng so that they can be used through out the code.

// function for adding a marker to the page.
function addMarker(location, message) {
marker = new google.maps.Marker({
position: location,
map: mymap,
animation: google.maps.Animation.DROP
});

addInfoWindow(marker, message)
}

// function to add info window for markers
function addInfoWindow(marker, message) {
var cinfoWindow = new google.maps.InfoWindow();
var chtml = message;

google.maps.event.addListener(marker, ‘click’, function() {
cinfoWindow.setContent(chtml);
cinfoWindow.open(mymap, marker);
});
}

// function to drop marker at currentlocation
function CurrentLocation() {
Marker1=new google.maps.LatLng(clat, clng); addMarker(Marker1, “Current Location”);
}

The complete code for this app is available at the following github repository.

Deployment with Capistrano

To understand the deployment process of a rails application, I planned on deploying the paperclip_sample_app  from the previous post to an Amazon EC2 instance using capistrano. In this post I will list the various steps involved in the deployment process. Deploying any application is pretty time consuming so be prepared to give your self enough time.

The first step is to create an instance and setup it up. For a personal ruby project the amazon micro instance is an ideal candidate (http://aws.amazon.com/free/). Log in to you aws management console (https://console.aws.amazon.com/).

Select the EC2 tab and hit the Launch Instance button.

Select the Community AMIs tab and search for “ubuntu 10.04”. I have had package dependency issues with the “ubuntu 10.10”.  Select the ami-054e9a6c instance.

Double check that it is the micro instance and hit continue.

Hit Continue

Hit Continue

Hit Continue and select the create new key pair button and download the keys.

Create a security group from the EC2 tab with the following permissions.

Port 22 – ssh

Port 80 – HTTP

Port 3306 – mysql

Select the security group and hit continue.

Launch the instance.

Connecting to the instance – It is very important to follow the steps in the link given below in order to avoid permission issues during application deployment.

https://help.ubuntu.com/community/EC2StartersGuide

$ cd ~/.ec2

ssh -i key1.pem ubuntu@ec2-50-19-48-132.compute-1.amazonaws.com

To deploy the rails application, the EC2 instance has to be setup with Ruby, Apache server, Phusion Passenger. These are the following steps:

$ sudo apt-get update

$ sudo apt-get dist-upgrade

$ sudo apt-get install openssh-server

$ sudo apt-get install mysql-server

$ sudo apt-get update

$ sudo apt-get dist-upgrade

$ sudo apt-get install build-essential

Install rvm and dependencies

sudo apt-get install zlib1g-dev libssl-dev libreadline5-dev libxml2-dev libsqlite3-dev

$ bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)

$ echo “[[ -s $HOME/.rvm/scripts/rvm ]] && source $HOME/.rvm/scripts/rvm” >> ~/.bash_profile

$ source ~/.bash_profile

$ sudo apt-get install build-essential openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0 libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev ncurses-dev automake libtool bison subversion

$rvm install 1.9.2

This will take around 15 to 20 minutes.

$rvm use 1.9.2

Installing passenger and its dependencies

$ sudo apt-get install apache2-prefork-dev libapr1-dev libaprutil1-dev

$ gem install passenger

$ passenger-install-apache2-module

$ sudo apt-get install libcurl4-openssl-dev

$ sudo apt-get install apache2-mpm-prefork

$ passenger-install-apache2-module

Edit /etc/apache2/apache2.conf

$ sudo vi /etc/apache2/apache2.conf

Navigate to the bottom of the file and hit i.

Hit enter and paste the following lines.

LoadModule passenger_module /home/ubuntu/.rvm/gems/ruby-1.9.2-p290/gems/passenger-3.0.11/ext/apache2/mod_passenger.so
PassengerRoot /home/ubuntu/.rvm/gems/ruby-1.9.2-p290/gems/passenger-3.0.11
PassengerRuby /home/ubuntu/.rvm/wrappers/ruby-1.9.2-p290/ruby

Hit ESC and type :wq!

This saves the changes.

This is a useful tutorial on vi editor. http://www.washington.edu/computing/unix/vi.html

Restart apache server

$ sudo /etc/init.d/apache2 restart

To verify, enter the public DNS of your instance in a browser.

Sync github to the server

http://help.github.com/linux-set-up-git/

$ chmod 700 .ssh/

Deploying rails app using capistrano

$ cd /var/www

$ sudo mkdir apps

$ cd apps

$ sudo mkdir paperclip_sample_app

Set correct permissions for ubuntu by making ubuntu the owner of the following directories.

$ chown ubuntu /var/www/apps

$ chown ubuntu /var/www/apps/paperclip_sample_app

copy the public key(~/.ssh/id_rsa.pub) on your machine to the ~/.ssh/authorized_keys folder

cat ~/.ssh/id_rsa.pub | ssh -i key1.pem ubuntu@ec2-50-19-48-132.compute-1.amazonaws.com ‘cat >> .ssh/authorized_keys’

This is very important, otherwise capistrano asks for password during  deployment.

$ capify .

Here is the config/deploy.rb file

commit to github

$ cap deploy:setup

$ cap deploy:check

If we get this error

$ cd /var/www/apps/paperclip_sample_app/current

$ rvm use 1.9.2

$ rvm gemset use global

$ gem install rake

create the db(should have the same name as that in the config/db file)

Before configuring apache server we need to associate the amazon instance with an IP address and buy a domain name.

Configuring apache to render the newly deployed app.

https://help.ubuntu.com/community/ApacheMySQLPHP

$ sudo vi /etc/apache2/sites-available/paperclip_sample_app

Add the following content to /etc/apache2/sites-available/paperclip_sample_app

<VirtualHost *:80>
ServerName http://www.vi-shal.com
DocumentRoot /var/www/apps/paperclip_sample_app/current/public
<Directory /var/www/apps/paperclip_sample_app/current/public>
AllowOverride all
Options -MultiViews
</Directory>
</VirtualHost>

$ sudo a2dissite default

$ sudo a2ensite paperclip_sample_app

$ sudo /etc/init.d/apache2 reload

Open a browser and enter the domain name or IP address of you instance. You should now be able to see the newly deployed site.

Uploading images to Amazon S3

To upload images to the cloud we use paperclip gem. This post will demonstrate how to use scaffolding to create a new Rails app from scratch that uses the Paperclip plugin to upload and display an image file. Initially the images are uploaded to the public folders of the app and later the app is configured to upload images to amazon’s S3 storage service.

This app displays the list of users with their profile pictures, username and email_id.

Paperclip uses imagemagik, so in order to get paperclip to work we need to install imagemagik. This can be done using

brew install imagemagick
gem install rmagick

$ rails new paperclip_sample_app

Setup the database.yml file and create a new MySQL database to use with the sample app. Contents of config/databases.yml

production:
adapter: mysql2
encoding: utf8
database: paperclip_sample_app_development
reconnect: false
pool: 5
username: root
password: password
socket: /var/run/mysqld/mysqld.sock

development:
adapter: mysql2
encoding: utf8
database: paperclip_sample_app_development
reconnect: false
socket: /var/run/mysqld/mysqld.sock
pool: 5
username: root

$ cd paperclip-sample-app
$ rake db:create

Add the paperclip gem to the Gemfile

source ‘http://rubygems.org&#8217;

gem ‘rails’, ‘3.0.10’
gem ‘mysql’
gem ‘paperclip’

gem ‘ruby-debug’, :groups => [:development, :test]

Lets generate the user model.

$ rails g scaffold user name:string email:string

Now we need to generate the database columns necessary for Paperclip on the user model object

$ rails g paperclip user avatar
$ rake db:migrate

The paperclip generator creates the following columns in the users table, “avatar_file_name,” “avatar_content_type,” “avatar_file_size” and “avatar_updated_at”

Next we need to add a line to the user model to let it know that it has a file attachment called avatar.

app/models/user.rb

class User < ActiveRecord::Base
has_attached_file :avatar
end

 These are the views used in the app.

app/views/users/new.html.erb

<h1>New user</h1>

<% form_for(@user, :html => { :multipart => true }) do |f| %>
<%= f.error_messages %>

<p>
<%= f.label :name %><br />
<%= f.text_field :name %>
</p>
<p>
<%= f.label :email %><br />
<%= f.text_field :email %>
</p>
<p>
<%= f.label :avatar %><br />
<%= f.file_field :avatar %>
</p>
<p>
<%= f.submit ‘Create’ %>
</p>
<% end %>

<%= link_to ‘Back’, users_path %>

app/views/users/show.html.erb

<p>
<b>Name:</b>
<%=h @user.name %>
</p>

<p>
<b>Email:</b>
<%=h @user.email %>
</p>

<p>
<b>Avatar:</b>
<%= image_tag @user.avatar.url %>
</p>

<%= link_to ‘Edit’, edit_user_path(@user) %> |
<%= link_to ‘Back’, users_path %>

app/views/users/show.html.erb

<p>
<b>Name:</b>
<%=h @user.name %>
</p>

<p>
<b>Email:</b>
<%=h @user.email %>
</p>

<p>
<b>Avatar:</b>
<%= image_tag @user.avatar.url %>
</p>

<%= link_to ‘Edit’, edit_user_path(@user) %> |
<%= link_to ‘Back’, users_path %>

Paperclip gives us different options to configure the images sizes. If no options are given, paperclip uploads the actual image size. In order to upload avatars we need to make the following changes to the user model.

app/models/user.rb

class User < ActiveRecord::Base
has_attached_file :avatar,
:styles => {
:thumb => “75×75>”,
:small => “150×150>”
}
end

This app stores the images in the public/system folders.
To upload images to the cloud we need to create an S3 account. The S3 account can be created at http://aws.amazon.com/. You can use your amazon account credentials to log in.  Note your ‘access key id’ and ‘secret access key’. These can be found at https://aws-portal.amazon.com/gp/aws/securityCredentials#access_credentials.
Include the aws-s3 gem,  in the gem file.
Gemfile

gem ‘aws-s3’, :require => ‘aws/s3’
Open up the Rails console and establish a connection to our account. We then create a new ‘bucket’ where our files will be stored.
# Terminal
> bundle
> rails c
console > include AWS::S3
console > Base.establish_connection!(:access_key_id => ‘yoursecretid’, :secret_access_key => ‘yoursecretkey’)
console > Bucket.create(‘paperclip_sample_app’)
console > Service.buckets
=> [#<AWS::S3::Bucket:0x00000 @attributes={“name”=>”paperclip_sample_app“, “creation_date”=>2012-02-12 00:00:00 UTC}, @object_cache=[]>]
console > Base.disconnect
console > exit
Next we need to config the user model so that paperclip uploads to S3 rather than the public/system folders. Before that we should create a file called in the config folder to store the S3 credentials.
# config/amazon_s3.yml

access_key_id: ‘yoursecretid’
secret_access_key: ‘yoursecretkey’
bucket: ‘paperclip_sample_app
The following changes must be made to the user model.
class User < ActiveRecord::Base
has_attached_file :avatar,
:styles => {
:thumb => “75×75>”,
:small => “150×150>”
},
:storage => :s3,
:s3_credentials => “#{Rails.root}/config/amazon_s3.yml”,
:path => “/:style/:filename”
validates_attachment_size :avatar, :less_than => 1.megabyte
end

Now the images will be uploaded into the cloud. Using the aws console we can look at the images that we uploaded.