Understanding Windows Azure AppFabric Access Control via PHP
In a post I wrote a couple of weeks ago, Consuming SQL Azure Data with the OData SDK for PHP, I didn’t address how to protect SQL Azure OData feeds with the Windows Azure AppFabric access control service because, quite frankly, I didn’t understand how to do it at the time. What I aim to do in this post is share with you some of what I’ve learned since then. I won’t go directly into how to protect OData feeds with AppFabric access control service (ACS, for short), but I will use PHP to show you how ACS works.
Disclaimer: The code in this post is intended to be educational only. It is simplified code that is intended to help you understand how ACS works. If you already have a good understanding of ACS and are looking to use it with PHP in a real application, then I suggest you check out the AppFabric SDK for PHP Developers (which I will examine more closely in a post soon). If you are interested in playing with my example code, I’ve attached it to this post in a .zip file.
Credits: A series of articles by Jason Follas were the best articles I could find for helping me understand how ACS works. I’ll borrow heavily from his work (with his permission) in this post, including his bouncer-bartender analogy (which Jason actually credits Brian H. Prince with). If you are interested in Jason’s articles, they start here: Windows Azure Platform AppFabric Access Control: Introduction.
AppFabric Access Control Service (ACS) as a Nightclub
The diagram below (adapted from Jason Follas’ blog) shows how a nightclub (with a bouncer and bartender) might go about making sure that only people of legal drinking age are served drinks (I’ll draw the analogy to ACS shortly):
- Before the nightclub is open for business, the bouncer tells the bartender that he will be handing out blue wristbands tonight to customers who are of legal drinking age.
- Once the club is open, a customer presents a state-issued ID with a birth date that shows he is of legal drinking age.
- The bouncer examines the ID and, when he’s satisfied that it is genuine, gives the customer a blue wristband.
- Now the customer can go to the bartender and ask for a drink.
- The bartender examines the wristband to make sure it is blue, then serves the customer a drink.
I would add one step that occurs before the steps above and is not shown in the diagram:
0. The state issues IDs to people and tells bouncers how to determine if an ID is genuine.
To draw the analogy to ACS, consider these ideas:
- You, as a developer, are the State. You issue ID’s to customers and you tell the bouncer how to determine if an ID is genuine. You essentially do this (I’m oversimplifying for now) by giving giving both the customer and bouncer a common key. If the customer asks for a wristband without the correct key, the bouncer won’t give him one.
- The bouncer is the AppFabric access control service. The bouncer has two jobs:
- He issues tokens (i.e. wristbands) to customers who present valid IDs. Among other information, each token includes a Hash-based Message Authentication Code (HMAC). (The HMAC is generated using the SHA-256 algorithm.)
- Before the bar opens, he gives the bartender the same signing key he will use to create the HMAC. (i.e. He tells the bartender what color wristband he’ll be handing out.)
- The bartender is a service that delivers a protected resource (drinks). The bartender examines the token (i.e. wristband) that is presented by a customer. He uses information in the token and the signing key that the bouncer gave him to try to reproduce the HMAC that is part of the token. If the HMACs are not identical, he won’t serve the customer a drink.
- The customer is any client trying to access the protected resource (drinks). For a customer to get a drink, he has to have a State-issued ID and the bouncer has to honor that ID and issue him a token (i.e. give him a wristband). Then, the customer has to take that token to the bartender, who will verify its validity (i.e. make sure it’s the agreed-upon color) by trying to reproduce a HMAC (using the signing key obtained from the bouncer before the bar opened) that is part of the token itself. If any of these checks fail along the way, the customer will not be served a drink.
To drive this analogy home, I’ll build a very simple system composed of a client (barpatron.php) that will try to access a service (bartender.php) that requires a valid ACS token.
Hiring a Bouncer (i.e. Setting up ACS)
In the simplest way of thinking about things, when you hire a bouncer (or set up ACS) you are simply equipping him with the information he needs to determine if a customer should be issed a wristband. Essentially, this means providing information to a customer that the bouncer will recognize as a valid ID. And, we also need to equip the bartender with the tools for validating the color of a wristband issued by the bouncer. I think this is worth keeping in mind as you work through the following steps.
To use Windows Azure AppFabric, you need a Windows Azure subscription, which you can create here: https://www.microsoft.com/windowsazure/offers/. (You’ll need a Windows Live ID to sign up.) I purchased the Windows Azure Platform Introductory Special, which allows me to get started for free as long as keep my usage limited. (This is a limited offer. For complete pricing details, see https://www.microsoft.com/windowsazure/pricing/.) After you purchase your subscription, you will have to activate it before you can begin using it (activation instructions will be provided in an e-mail after signing up).
After creating and activating your subscription, go to the AppFabric Developer Portal: https://appfabric.azure.com/Default.aspx (where you can create an access control service). To create a service…
1. Click a project name.
2. Click Add Service Namespace.
3. Choose a service namespace (make sure it is valid) and the region in which you want the service deployed, then click Create (you can leave the ServiceBus connections at 0 because they don’t apply to access control):
4. On the resulting page, make note of your Current Management Key and the STS endpoint (which will always be of the form https://SERVICE_NAMESPACE.accesscontrol.windows.net/WRAPv0.9/). (Note that ServiceBus-related information is not shown in the picture below.)
5. Once you have created a service, you need to create token policies within the service namespace (we’ll only create one token policy). I’ll use the command line tool (ACM) that is available in the Windows Azure AppFabric SDK. After you download the tools you’ll need to modify the configuration file (which is in the Windows Azure platform AppFabric SDK\V1.0\Tools directory) by adding your service namespace and your management key:
6. A token policy defines the time-to-live for a token after it is issued (86400 seconds = 24 hours in our case) and the key that will be used to sign tokens (this is the key that will be shared with the bartender). You can create a token policy with the following command:
acm create tokenpolicy –name:BouncerPolicy –timeout:86400 –autogeneratekey
To see the token policy id, name, timeout, and key, use this command: acm getall tokenpolicy.
7. Next, we need to create a scope that is associated with the token policy. The scope defines where a token will eventually be used (the value of the “appliesto” parameter. You can create a scope with the following command (with out the line breaks):
acm create scope –name:Bartender
–appliesto:https://localhost/bartender.php
-tokenpolicyid:<from previous step>To see the generated information, use this command: acm getall scope.
8. Next we have to create an issuer that will be trusted by ACS (i.e. We need to play the role of the State here, and let the bouncer know how to recognize a State-issued ID). To create an issuer, use the following command:
acm create issuer -name:Washington -issuername:Washington –autogeneratekey
Again, to see the generated information use this command: acm getall issuer.
9. Finally, we need to create a rule that defines which claims should be present in a token issued by the service. In the rule we’ll create, we’ll assume that ACS is expecting a DOB claim (with some value) and that the bartender is expecting Birthdate claim. ACS will then simply pass the value of the DOB claim on with the Birthdate name. Here is the command for creating the rule (omit the line breaks):
acm create rule -name:Birthdate
-scopeid:<scope id from step 7>
-inclaimissuerid:<issuer id from step 8>
-inclaimtype:DOB
-outclaimtype:Birthdate
-passthrough
To see the generated information, use this command: acm getall rule –scopeid:<scope id from step 7>.
Recall that we set out to give the bouncer the information he needs to issue (or not issue) wristbands. That information is the serervice namespace, the issuer name, the issuer key, the scope (i.e. the place where a token will be used), and a DOB claim. Note that, in a way, you play the State since you decide what customers to give this information to (i.e. you are the State issuing drivers licenses). We have also set up a way for the bartender to determine if a token that has been presented to him is valid: he will use the token policy key (shared between him and the bouncer) to try to replicate an HMAC generated by the bouncer.
Setting Up the Bartender (i.e. Verifying Tokens)
Now we need a way for the bartender to validate the agreed upon color of the wristbands (i.e. validate tokens that are presented to him). This may vary from service to service, but at the heart of it is an HMAC that is included in the token itself. The bartender will use a key (the token policy key) shared between the bouncer and himself to try to replicate the HMAC. The details are in the code below...
A typical token issued by ACS will look something like this:
wrap_access_token=Birthdate%3d1-1-70%26Issuer%3dhttps%253a%252f%252fbouncernamespace.accesscontrol.windows.net%252f%26Audience%3dhttp%253a%252f%252flocalhost%252fbartender.php%26ExpiresOn%3d1283809703%26HMACSHA256%3d19pmWGr9pgH9RGdqYhrPcO6qse8YLGPYZGMoQOvz%252biY%253d&wrap_access_token_expires_in=86400
If you can look past the URL encoding, you will see much of the information we provided when setting up ACS: the service namespace is part of the Issuer URL, the “applies to” value is the Audience, and the value of ExpiresOn is determined by the length of the timeout we set for tokens. The value of HMACSHA256 is the HMAC created by signing this part of the token, Birthdate=1-1-70&Issuer=https%3a%2f%2fbouncernamespace.accesscontrol.windows.net%2f&Audience=http%3a%2f%2flocalhost%2fbartender.php&ExpiresOn=1283788760, with a signing key (the token policy key) that is shared with the bartender. For the bartender to make sure this is a valid token (which, in my example, is expected in an Authorization header), he has to first perform a couple simple checks: 1) Make sure an Authorization header is present, and 2) Make sure the Authorization header is properly formed (i.e. beings with wrap_access_token).
// Check for presence of Authorization header
if(!isset($_SERVER['HTTP_AUTHORIZATION']))
Unauthorized("No authorization header.");
$header = $_SERVER['HTTP_AUTHORIZATION'];// Header must start with wrap_access_token
$i = stripos($header, "wrap_access_token");
if ($i != 0 || $i === false)
Unauthorized("Authorization header doesn't start with wrap_access_token.");
Next, the bartender needs to “split” the header for further processing:
//Header must have exactly two parts, token is in second part
$headerSplit = explode('=', $header, 2);
if (count($headerSplit) != 2)
Unauthorized("Header doesn't have two parts.");$token = $headerSplit[1];
Now the real work of making sure the token is valid can be done. The heavy lifting is done by the Validate method in a class called TokenValidator (which I will look at more closely in a moment).
// Validate token
$validator = new TokenValidator($token, SIGNING_KEY, SERVICE_NAMESPACE, APPLIES_TO);
if(!$validator->Validate())
Unauthorized("Token is not valid.");
else
echo "What would you like to drink?";
Here is the Unauthorized function used in the code above:
function Unauthorized($reason)
{
echo $reason." No drink for you!<br/>";
die();
}
Let’s look more closely at the TokenValidator class…in particular the Validate method. The Validate method simply calls 4 other methods that validate the HMAC, determine whether the token is expired, determine whether the issuer is trusted, and determine whether the audience is trusted:
public function Validate()
{
if($this->isHMACValid() && !$this->isExpired() &&
$this->isTrustedIssuer() && $this->isTrustedAudience())
return true;
else
return false;
}
In turn, each of these methods is fairly simple. And, to make them easier to read, the constructor breaks the token into name-value pairs (stored in the tokenParts property):
public function TokenValidator($token, $signingKey, $serviceNamespace, $audience)
{
$this->_token = $token;
$this->_signingKey = $signingKey;
$this->_serviceNamespace = $serviceNamespace;
$this->_audience = $audience;
$params = explode('&', $token);
foreach ($params as $param)
{
$namevalue = explode('=', $param, 2);
$this->_tokenParts[$namevalue[0]] = urldecode($namevalue[1]);
}
}
The isHMACValid method splits the token on &HMACSHA256= and then uses the signing key that is shared with the bouncer to create an HMAC based on the first part of the token:
private function isHMACValid()
{
$tokenSplit = explode("&HMACSHA256=", $this->_token, 2);
if(count($tokenSplit) != 2)
{
echo "Failed to split on &HMACSHA256=.<br/>";
return false;
}
$hmac = hash_hmac('sha256', $tokenSplit[0], base64_decode($this->_signingKey), true);
$locallyGeneratedSignature = base64_encode($hmac);if($this->_tokenParts['HMACSHA256'] != $locallyGeneratedSignature)
{
echo "Signatures don't match.<br/>";
return false;
}
return true;
}
The isExpired, isTrustedIssuer, and isTrustedAudience speak for themselves:
private function isExpired()
{
$currentTime = time();
if($currentTime > $this->_tokenParts['ExpiresOn'])
{
echo "Token is expired<br/>";
return true;
}
else
{
return false;
}
}private function isTrustedIssuer()
{
if('https://'.$this->_serviceNamespace.'.accesscontrol.windows.net/' ==
$this->_tokenParts['Issuer'])
return true;
else
{
echo "Not a trusted issuer.<br/>";
return false;
}
}private function isTrustedAudience()
{
if($this->_audience == $this->_tokenParts['Audience'])
return true;
else
{
echo "Not a trusted audience.<br/>";
return false;
}
}
See, making sure a wristband is blue isn’t all that complicated. :-) (All the code above can be found in the bartender.php file in the attachment to this post.)
Getting a Wristband (i.e. Requesting a Token)
For a customer to get a token, he must have 4 key pieces of information that we made notes about when setting up ACS:
$service_namespace = "Your_service_namespace"; //From step 3 above.
$wrap_name = "Issuer_name"; //From step 8 above.
$wrap_password = "Issuer_key"; //The generated key from step 8 above.
$wrap_scope = "https://localhost/bartender.php"; //Value of “applies to” in step 7 above.
$claims=array('DOB'=>'1-1-70'); //Any DOB claim.
A request to ACS for a token must be an HTTP POST request. With the information above, we can send a POST request using cURL. The post body is a concatenation of the much of the information above, and the URL is based on the $service_namespace. Note that I’m assuming the token is the last line in the response (the response body):
//Define post body and URL for token request.
$postBody = 'wrap_name' . '=' . urlencode($wrap_name) . '&' .
'wrap_password' . '=' . urlencode($wrap_password) . '&' .
'wrap_scope' . '=' . $wrap_scope;
foreach ($claims as $key => $value)
{
$postBody = $postBody . '&' . $key . '=' . $value;
}$url = 'https://' . $service_namespace . '.' .
'accesscontrol.windows.net' . '/' . 'WRAPv0.9';// Initialize cURL session for requesting token.
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt ($ch, CURLOPT_POST, 1);
curl_setopt ($ch, CURLOPT_POSTFIELDS, $postBody);
curl_setopt($ch, CURL_HTTP_VERSION_1_1, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_USERAGENT,"Bar Patron");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);// Execute cURL session and extract token.
$ACSResponse = curl_exec($ch);
curl_close($ch);
$responseParts = explode("\n", $ACSResponse);
$token = $responseParts[count($responseParts)-1];
And now, with a token in hand, we can take it to the bartender. I’ll again use cURL to send a request (this time a GET request) with the token as the value of the Authorization header.
// Initialize cURL session for presenting token.
$ch2=curl_init();
curl_setopt($ch2, CURLOPT_URL, "https://localhost/bartender.php");
curl_setopt($ch2, CURLOPT_HTTPHEADER, array("Authorization: ".urldecode($token)));
curl_setopt($ch2, CURLOPT_HEADER, true);
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);$bartenderResponse = curl_exec($ch2);
curl_close($ch2);$responseParts = explode("\n", $bartenderResponse);
echo $responseParts[count($responseParts)-1];
The code above is in the barpatron.php file (attached to this post).
Wrapping Up
Once you have set up ACS (i.e. once you have “Hired a Bouncer”), you should be able to take the two files (bartender.php and barpatron.php) in the attached .zip file, modify them to that they use your ACS information (service namespace, signing keys, etc.), and then load the barpatron.php in your browser to see how it all works.
If you have been reading carefully, you may have noticed a flaw in this bouncer-bartender analogy: in the real world, the bouncer would actually check the DOB claim to make sure the customer is of legal drinking age. In this example, however, it would be up to the bartender to actually verify that the customer is of legal drinking age. (At this time, ACS doesn’t have a rules engine that would allow for this type of check.) Or, the Issuer would have to verify legal drinking age. i.e. You might write a service that only provides the customer with the information necessary to request a token if he has somehow proven that he is of legal drinking age.
I hope this helps in understanding how ACS works. Look for a post soon that shows how to use the AppFabric SDK for PHP Developers to make things easier!
Thanks.
-Brian