Ruby web sites and Windows Azure AppFabric Access Control

Recently I was trying to figure out how to allow users to login to a Ruby (Sinatra) web site using an identity service such as Facebook, Google, Yahoo, etc. I ended up using Windows Azure AppFabric Access Control Service (ACS) since it has built-in support for:

  • Facebook
  • Google
  • Yahoo
  • Windows Live ID
  • WS-Federation Identity Providers

The nice thing is that you can use one or all of those, and your web application just needs to understand how to talk to ACS and not the individual services. I worked up an example of how to do this in Ruby, and will explain it after some background on ACS and how to configure it for this example.

What is ACS?

ACS is a claims based, token issuing provider. This means that when a user authenticates through ACS to Facebook or Google, that it returns a token to your web application. This token contains various ‘claims’ as to the identity of the authenticating user.

The tokens returned by ACS are either simple web tokens (SWT) or security assertion markup language (SAML)1.0 or 2.0. The token contains the claims, which are statements that the issuing provider makes about the user being authenticated.

The claims returned may, at a minimum, just contain a unique identifer for the user, the identity provider name, and the dates that the token is valid. Additional claims such as the user name or e-mail address may be provided – it’s up to the identity provider as to what is available. You can see the claims returned for each provider, and select which specific ones you want, using the ACS administration web site.

ACS costs $1.99 a month per 100,000 transactions, however there’s currently a promotion going on until January 1, 2012 during which you won’t be charged for using the service. See https://www.microsoft.com/windowsazure/features/accesscontrol/ for more details.

Configuring ACS

To configure ACS, you’ll need a Windows Azure subscription. Sign in at https://windows.azure.com, then navigate to the “Service Bus, Access Control & Caching” section. Perform the following tasks to configure ACS:

  1. Using the ribbon at the top of the browser, select “New” from the Service Namespace section.
  2. In the “Create a new Service Namespace” dialog, uncheck Service Bus and Cache, enter a Namespace and Region, and then click the Create Namespace button.
  3. Select the newly created namespace, and then click the Access Control Service icon on the ribbon. This will take you to the management site for your namespace.
  4. Follow the “Getting Started” steps on the page.

1: Select Identity Providers

In the Identity Providers section, add the identity providers you want to use. Most are fairly straight forward, but Facebook is a little involved as you’ll need an Application ID, Application secret, and Application permissions. You can get those through https://www.facebook.com/developers. See https://blogs.objectsharp.com/cs/blogs/steve/archive/2011/04/21/windows-azure-access-control-services-federation-with-facebook.aspx for a detailed walkthrough of using ACS with Facebook.

2: Relying Party Applications

This is your web application. Enter a name, Realm (the URL for your site,) the Return URL (where ACS sends tokens to,) error URL’s, etc. For token format you can select SAML or SWT. I selected SWT and that’s what the code I use below uses. You’ll need to select the identity providers you’ve configured. Also be sure to check “Create new rule group”. For token signing settings, click Generate to create a new key and save the value off somewhere.

3: Rule groups

If you checked off “Create new rule group” you’ll have a “Default Rule Group” waiting for you here. If not, click Add to add one. Either way, edit the group and click Generate to add some rules to it. Rules are basically how you control what claims are returned to your appication in the token. Using generate is a quick and easy way to populate the list. Once you have the rules configured, click Save.

4: Application Integration

In the Application Integration section, select Login pages, then select the application name. You’ll be presented with two options; a URL to an ACS-hosted login page for your application and a button to download an example login page to include in your application. For this example, copy the link to the ACS-hosted login page.

ACS with Ruby

The code below will parse the token returned by the ACS-hosted login page and return a hash of claims, or an array containing any errors encountered during validation of the token. It doesn't fail immediately on validation, as it may be useful to examine the validation failures to figure out any problems that were encountered. Also note that the constants need to be populated with values specific to your application and the values entered in the ACS management site.

 require 'nokogiri'
require 'time'
require 'base64'
require 'cgi'
require 'openssl'
 REALM=‘https://your.realm.com/’
TOKEN_TYPE=‘https://schemas.xmlsoap.org/ws/2009/11/swt-token-profile-1.0’ #SWT
ISSUER=‘https://yournamespace.accesscontrol.windows.net/'
TOKEN_KEY='the key you generated in relying applications above’
 class ResponseHandler
  attr_reader :validation_errors, :claims

  def initialize(wresult)
    @validation_errors = []
    @claims={}
    @wresult=Nokogiri::XML(wresult)

    parse_response()
  end
  
  def is_valid?
    @validation_errors.empty?
  end
  
  private
  #parse through the document, performing validation & pulling out claims
  def parse_response
    parse_address()
    parse_expires()
    parse_token_type()
    parse_token()
  end
  #does the address field have the expected address?
  def parse_address
    address = get_element('//t:RequestSecurityTokenResponse/wsp:AppliesTo/addr:EndpointReference/addr:Address')
    @validation_errors << "Address field is empty." and return if address.nil?
    @validation_errors << "Address field is incorrect." unless address == REALM
  end
  
  #is the expire value valid?
  def parse_expires
    expires = get_element('//t:RequestSecurityTokenResponse/t:Lifetime/wsu:Expires')
    @validation_errors << "Expiration field is empty." and return if expires.nil?
    @validation_errors << "Invalid format for expiration field." and return unless /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$/.match(expires)
    @validation_errors << "Expiration date occurs in the past." unless Time.now.utc.iso8601 < Time.iso8601(expires).iso8601
  end
  
  #is the token type what we expected?
  def parse_token_type
    token_type = get_element('//t:RequestSecurityTokenResponse/t:TokenType')
    @validation_errors << "TokenType field is empty." and return if token_type.nil?
    @validation_errors << "Invalid token type." unless token_type == TOKEN_TYPE
  end
  
  #parse the binary token
  def parse_token
    binary_token = get_element('//t:RequestSecurityTokenResponse/t:RequestedSecurityToken/wsse:BinarySecurityToken')
    @validation_errors << "No binary token exists." and return if binary_token.nil?
    
    decoded_token = Base64.decode64(binary_token)
    name_values={}
    decoded_token.split('&').each do |entry|
      pair=entry.split('=') 
      name_values[CGI.unescape(pair[0]).chomp] = CGI.unescape(pair[1]).chomp
    end

    @validation_errors << "Response token is expired." if Time.now.to_i > name_values["ExpiresOn"].to_i
    @validation_errors << "Invalid token issuer." unless name_values["Issuer"]=="#{ISSUER}"
    @validation_errors << "Invalid audience." unless name_values["Audience"] =="#{REALM}"
 
    # is HMAC valid?
    token_hmac = decoded_token.split("&HMACSHA256=")
    swt=token_hmac[0]
    @validation_errors << "HMAC does not match computed value." unless name_values['HMACSHA256'] == Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha256'),Base64.decode64(TOKEN_KEY),swt)).chomp
    
    # remove non-claims from collection and make claims available

    @claims = name_values.reject {|key, value| !key.include? '/claims/'}

  end
  
  #given an path, return the content of the first matching element
  def get_element(xpath_statement)
    begin
      @wresult.xpath(xpath_statement,
              't'=>'https://schemas.xmlsoap.org/ws/2005/02/trust',
              'wsu'=>'https://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
              'wsp'=>'https://schemas.xmlsoap.org/ws/2004/09/policy',
              'wsse'=>'https://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
              'addr'=>'https://www.w3.org/2005/08/addressing')[0].content
    rescue
      nil
    end
  end
end

So what’s going on here? The main pieces are:

  • get_element, which is used to pick out various pieces of the XML document.
  • parse_addres, expires, token_type, which pull out and validate the individual elements
  • parse_token, which decodes the binary token, validates it, and returns the claims collection.

After processing the token, you can test for validity by using is_valid? and then parse through either the claims hash or validation_errors array. I'll leave it up to you to figure out what you want to do with the claims; in my case I just wanted to know the identity provider the user selected and thier unique identifier with that provider so that I could store it along with my sites user specific information for the user.

Summary

As mentioned in the introduction, ACS let’s you use a variety of identity providers without requiring your application to know the details of how to talk to each one. As far as your application goes, it just needs to understand how to use the token returned by ACS. Note that there may be some claim information provided that you can use to gather additional information directly from the identity provider. For example, FaceBook returns an AccessToken claim field that you an use to obtain other information about the user directly from FaceBook.

As always, let me know if you have questions on this or suggestions on how to improve the code.