Ruby web sites and Windows Azure AppFabric Access Control

Ruby web sites and Windows Azure AppFabric Access Control

  • Comments 5

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 http://www.microsoft.com/windowsazure/features/accesscontrol/ for more details.

Configuring ACS

To configure ACS, you’ll need a Windows Azure subscription. Sign in at http://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 http://www.facebook.com/developers. See http://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=‘http://your.realm.com/’
TOKEN_TYPE=‘http://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'=>'http://schemas.xmlsoap.org/ws/2005/02/trust',
'wsu'=>'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
'wsp'=>'http://schemas.xmlsoap.org/ws/2004/09/policy',
'wsse'=>'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'addr'=>'http://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.

Leave a Comment
  • Please add 3 and 4 and type the answer here:
  • Post
  • Thank you very much for the post! I just got this working correctly on a project that I'm working on in Rails. Before reading this (and being a Rails newb), I wasn't sure how to implement ACS in Rails. This gave me everything I needed!

  • Glad it was useful Tuck.

  • Hi,

    Do you have example maybe for single sign out in Ruby?

    Thanks,

    Ratko

  • Hi Ratko,

    Single sign-out with ACS is pretty easy. It's just doing a redirect to a specially crafted URL, which then does a GET back to a page on your web site. The URL format is https://<your namespace>.accesscontrol.windows.net/v2/wsfederation?wa=wsignout1.0&wtrealm=<your realm>&wreply=<return page>. The only trick is that the realm and return page values have to be encoded. For example, if my namespace is "mynamespace", my realm is "http://localhost", and the page I want users to land on once they have been signed out is "http://localhost/youareloggedout", then this would be mynamespace.accesscontrol.windows.net/.../wsfederation

    That's it. What this does is log the user out of the services associated with ACS. If they don't log out, you see the behavior where they click the login link on your site, it redirects to ACS, then immediately back to your site without asking them to provide credentials for the identity provider. After hitting the sign-out URL above, the next time they login ACS should prompt them to select the identity provider and provide username/password for it.

    Hope this helps.

  • Also, I'm told that Windows Azure Active Directory is now the way to go vs. ACS. I haven't worked with WAAD, so I don't currently have any recommendations or pointers for that. I'll see if I can work up a sample of using that and post to the blog.

Page 1 of 1 (5 items)