Freigeben über


How To Request SWT Token From ACS And How To Validate It At The REST WCF Service Hosted In Windows Azure

Programming Windows Azure - Programming the Microsoft Cloud

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?”

REST WCF SWT ACS UID/PWD

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.csPage_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:

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.