How To Request SWT Token From ACS And How To Validate It At The REST WCF Service Hosted In Windows Azure
This post answers the following question:
“How do I use the Access Control Service (ACS) and a Simple Web Token (SWT) to secure a REST-based WCF service that is hosted in Windows Azure? How do I build a client application that obtains and uses an SWT token from ACS to connect to this web service based on Username/Password pair?”
Summary of steps:
- Step 1 – Create REST WCF Service
- Step 2 – Configure ACS To Issue SWT Token
- Step 3 – Implement Code That Validates The SWT Token At The REST WCF Service
- Step 4 – Implement Client That Requests SWT Token From ACS And Sends Request To REST WCF Service
- Step 5 – Deploy To Windows Azure
Step 1 – Create REST WCF Service
In this step you will create REST WCF service that can be hosted in IIS. The REST WCF service will have an SVC file that will be hosted by IIS, the WCF library that has REST interface and actual REST service that implements the interface.
To create REST WCF service to be deployed in IIS
Follow the steps outlined in RESTful WCF Architecture – Hosting RESTful WCF Services in IIS.
Step 2 – Configure ACS To Issue SWT Token
In this step you will need to configure your REST web service as relying party and also configure service identity. All these configurations are going to be accomplished using ACS management portal. Note, these configurations can be also accomplished programmatically using ACS management service. For more information on using ACS management service consider reviewing Automation content.
To configure REST web service as a relying party
Follow the steps in Step 1 – Configure a Relying Party Using the ACS Management Portal from How To: Authenticate with a Username and Password to a WCF Service Protected by ACS. The key difference is that this time the token format should be configured for SWT for your REST web service.
To configure service identity for the REST web service
Follow the steps in Step 1 - Add a Service Identity with a Password from How To: Add Service Identities with an X.509 Certificate, Password, or Symmetric Key
Step 3 – Implement Code That Validates The SWT Token At The REST WCF Service
You need to validate incoming SWT token yourself. When the token hits your REST web service the token must be validated for several aspects, mainly the format, signature, and the expiration. In the SOAP/SAML world all that performed by WIF. WIF does not have built in SWT token handler. These are the token handlers that WIF currently supports [from Windows Identity Foundation (WIF) Configuration – Part V (<securityTokenHandlers>)]":
- Saml11SecurityTokenHandler
- Saml2SecurityTokenHandler
- KerberosSecurityTokenHandler
- WindowsUserNameSecurityTokenHandler
- RsaSecurityTokenHandler
- X509SecurityTokenHandler
- EncryptedSecurityTokenHandler
To validate SWT token at the REST WCF service
Use the code provided with the Code Sample: ASP.NET Web Service. Specifically for the SWT token validation you need the following parts:
- This part checks general formatting of the token and its presence. In the Default.aspx.cs’ Page_Load method:
// Copyright 2010 Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED,
// INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR
// CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
// MERCHANTABLITY OR NON-INFRINGEMENT.
// See the Apache 2 License for the specific language governing
// permissions and limitations under the License.
// get the authorization header
string headerValue = Request.Headers.Get("Authorization");
// check that a value is there
if (string.IsNullOrEmpty(headerValue))
{
this.ReturnUnauthorized();
return;
}
// check that it starts with 'WRAP'
if (!headerValue.StartsWith("WRAP "))
{
this.ReturnUnauthorized();
return;
}
string[] nameValuePair = headerValue.Substring("WRAP ".Length).Split(new char[] { '=' }, 2);
if (nameValuePair.Length != 2 ||
nameValuePair[0] != "access_token" ||
!nameValuePair[1].StartsWith("\"") ||
!nameValuePair[1].EndsWith("\""))
{
this.ReturnUnauthorized();
return;
}
// trim off the leading and trailing double-quotes
string token = nameValuePair[1].Substring(1, nameValuePair[1].Length - 2);
// create a token validator
TokenValidator validator = new TokenValidator(
this.acsHostName,
this.serviceNamespace,
this.trustedAudience,
this.trustedTokenPolicyKey);
// validate the token
if (!validator.Validate(token))
{
this.ReturnUnauthorized();
return;
}
- This part validates cryptographic validness and other security specific aspects. TokenValidator.cs class:
// Copyright 2010 Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED,
// INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR
// CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
// MERCHANTABLITY OR NON-INFRINGEMENT.
// See the Apache 2 License for the specific language governing
// permissions and limitations under the License.
namespace Microsoft.AccessControl2.SDK.ASPNetSimpleWebsite
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
public class TokenValidator
{
private string issuerLabel = "Issuer";
private string expiresLabel = "ExpiresOn";
private string audienceLabel = "Audience";
private string hmacSHA256Label = "HMACSHA256";
private string acsHostName;
private string trustedSigningKey;
private string trustedTokenIssuer;
private string trustedAudienceValue;
public TokenValidator(string acsHostName, string serviceNamespace, string trustedAudienceValue, string trustedSigningKey)
{
this.trustedSigningKey = trustedSigningKey;
this.trustedTokenIssuer = String.Format("https://{0}.{1}/",
serviceNamespace.ToLowerInvariant(),
acsHostName.ToLowerInvariant());
this.trustedAudienceValue = trustedAudienceValue;
}
public bool Validate(string token)
{
if (!this.IsHMACValid(token, Convert.FromBase64String(this.trustedSigningKey)))
{
return false;
}
if (this.IsExpired(token))
{
return false;
}
if (!this.IsIssuerTrusted(token))
{
return false;
}
if (!this.IsAudienceTrusted(token))
{
return false;
}
return true;
}
public Dictionary<string, string> GetNameValues(string token)
{
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException();
}
return
token
.Split('&')
.Aggregate(
new Dictionary<string, string>(),
(dict, rawNameValue) =>
{
if (rawNameValue == string.Empty)
{
return dict;
}
string[] nameValue = rawNameValue.Split('=');
if (nameValue.Length != 2)
{
throw new ArgumentException("Invalid formEncodedstring - contains a name/value pair missing an = character");
}
if (dict.ContainsKey(nameValue[0]) == true)
{
throw new ArgumentException("Repeated name/value pair in form");
}
dict.Add(HttpUtility.UrlDecode(nameValue[0]), HttpUtility.UrlDecode(nameValue[1]));
return dict;
});
}
private static ulong GenerateTimeStamp()
{
// Default implementation of epoch time
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToUInt64(ts.TotalSeconds);
}
private bool IsAudienceTrusted(string token)
{
Dictionary<string, string> tokenValues = this.GetNameValues(token);
string audienceValue;
tokenValues.TryGetValue(this.audienceLabel, out audienceValue);
if (!string.IsNullOrEmpty(audienceValue))
{
if (audienceValue.Equals(this.trustedAudienceValue, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private bool IsIssuerTrusted(string token)
{
Dictionary<string, string> tokenValues = this.GetNameValues(token);
string issuerName;
tokenValues.TryGetValue(this.issuerLabel, out issuerName);
if (!string.IsNullOrEmpty(issuerName))
{
if (issuerName.Equals(this.trustedTokenIssuer))
{
return true;
}
}
return false;
}
private bool IsHMACValid(string swt, byte[] sha256HMACKey)
{
string[] swtWithSignature = swt.Split(new string[] { "&" + this.hmacSHA256Label + "=" }, StringSplitOptions.None);
if ((swtWithSignature == null) || (swtWithSignature.Length != 2))
{
return false;
}
HMACSHA256 hmac = new HMACSHA256(sha256HMACKey);
byte[] locallyGeneratedSignatureInBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(swtWithSignature[0]));
string locallyGeneratedSignature = HttpUtility.UrlEncode(Convert.ToBase64String(locallyGeneratedSignatureInBytes));
return locallyGeneratedSignature == swtWithSignature[1];
}
private bool IsExpired(string swt)
{
try
{
Dictionary<string, string> nameValues = this.GetNameValues(swt);
string expiresOnValue = nameValues[this.expiresLabel];
ulong expiresOn = Convert.ToUInt64(expiresOnValue);
ulong currentTime = Convert.ToUInt64(GenerateTimeStamp());
if (currentTime > expiresOn)
{
return true;
}
return false;
}
catch (KeyNotFoundException)
{
throw new ArgumentException();
}
}
}
}
To separate the security logic from the business logic consider using either WCF pipeline or implementing HttpModule. That way you will intercept incoming requests and validate the SWT token without polluting the code with this security plumbing.
Step 4 – Implement Client That Requests SWT Token From ACS And Sends Request To REST WCF Service
You need to write yourself the code that requests SWT token from ACS. In SOAP/SAML world you would use proper binding and also FedUtil wizard to do the work of requesting a token. These do not exist at the moment for REST WCF services.
To request a SWT token from ACS
Use GetTokenFromACS method from the Program.cs in the Client project from Code Sample: ASP.NET Web Service.
// Copyright 2010 Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED,
// INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR
// CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
// MERCHANTABLITY OR NON-INFRINGEMENT.
// See the Apache 2 License for the specific language governing
// permissions and limitations under the License.
private static string GetTokenFromACS(string scope)
{
string wrapPassword = ConfigurationManager.AppSettings.Get("WrapPassword");
string wrapUsername = ConfigurationManager.AppSettings.Get("WrapUsername");
// request a token from ACS
WebClient client = new WebClient();
client.BaseAddress = string.Format("https://{0}.{1}", SamplesConfiguration.ServiceNamespace, SamplesConfiguration.AcsHostUrl);
NameValueCollection values = new NameValueCollection();
values.Add("wrap_name", wrapUsername);
values.Add("wrap_password", wrapPassword);
values.Add("wrap_scope", scope);
byte[] responseBytes = client.UploadValues("WRAPv0.9/", "POST", values);
string response = Encoding.UTF8.GetString(responseBytes);
Console.WriteLine("\nreceived token from ACS: {0}\n", response);
return HttpUtility.UrlDecode(
response
.Split('&')
.Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase))
.Split('=')[1]);
}
To send GET request the the REST WCF service
Assuming your REST WCF service implement UriTemplate /users for WebGet then the following code can be used to issue GET request:
string token = GetTokenFromACS(realm);
WebClient client = new WebClient();
string headerValue = string.Format("WRAP access_token=\"{0}\"", token);
client.Headers.Add("Authorization", headerValue);
Stream stream = client.OpenRead(@"https://yourNameSpace.cloudapp.net/RESTfulWCFUsersServiceEndPoint.svc/users");
StreamReader reader = new StreamReader(stream);
String response = reader.ReadToEnd();
Step 5 – Deploy To Windows Azure
If you:
- Implemented REST WCF service using WebServiceHostFactory (if you followed the creation of the REST WCF service as described in RESTful WCF Architecture – Hosting RESTful WCF Services in IIS then you used WebServiceHostFactory).
- Implemented HttpModule to intercept incoming requests to validate SWT tokens
then you need to take few extra steps to make sure all assemblies get uploaded to Windows Azure since they are not directly referenced thus not automatically added to the deploy package. These are the steps:
- Expand bin folder of the the REST WCF service solution.
- Right click on the library DLL that implements the REST WCF service and select Include In Project option.
- Right click on the same library and select Properties.
- In the Properties window, select “Copy if newer” for the “Copy to Output Directory”
- Do the same steps for the HttpModule
Now you can publish and deploy your REST WCF service to Windows Azure either through Visual Studio or via Windows Azure portal.
Related Materials
- Windows Azure AppFabric Access Control Service (ACS): WCF SAML/SOAP Client Certificate Scenario
- Windows Azure AppFabric Access Control Service (ACS): WCF SAML/SOAP Username/Password Scenario
- Windows Azure AppFabric Access Control Service (ACS): WCF SAML/SOAP ADFS Scenario
- Windows Azure AppFabric Access Control Service (ACS): WCF SWT/REST OAuth Scenario
- Windows Azure AppFabric Access Control Service (ACS): REST Web Services And OAuth 2.0 Delegation