Securing Service Bus with Azure access control

London bus stop

I’m working on a project in which I needed clients to send and retrieve messages from a Windows Azure Service Bus Topic and Subscriptions and use certificates for authentication.

 

The use of certificates is more secure because, done well, there is less risk of wrong people accessing, and being able to take away, the credentials and use them maliciously.

 

The Service Bus itself only supports authentication based Shared Access Signatures but, obviously, it also supports authenticating using SWT tokens retrieved from ACS which, itself, does support authenticating using certificates.

 

So – for a client to read a message from the subscription the general flow would be:

 

1. Client calls ACS with a certificate for authentication, asking for a token for a Service Bus subscription (the protected resource or RP).
2. ACS returns an SWT token for the request RP based on the certificate that is valid for the configured amount of time.
3. The client calls the Service Bus to retrieve a message from the subscription and includes the SWT token received from ACS in the request’s HTTP header.
4. Service Bus authenticates request and returns message off the subscription.

To get this to work I’ve needed the following steps

 

Setting up Azure Access Control

As I’m using this in the context of the Service Bus an ACS namespace has already been created for me when I created the Service Bus namespace.

 

In this namespace I needed to configure a Service Identity:

 

Create a Service Identity

I’ve given it a name (which I’ll need to use in the client, as you’ll see shortly) and configured it with an X.509 certificate under the credential settings.

 

It is worth pointing out that I’m uploading the .cer file which DOES NOT contain the private key, only the originator of the request should have the private key, which can then be used to issue authentication requests to this service identity in ACS.

 

Also worth noting is that the certificate can be any .cer file and doesn’t need to have a full trusted root. I created a self-signed certificate through Internet Information Services, exported the PFX and then exported the public key.

 

 

1

 

Create a Relying Party

With the identity set-up it is time to configure the relying party, sometimes referred to as the protected resource; this is essentially the service-bus entity I want to grant access to, in this case a topic.

 

The screenshot below shows the configuration for a topic, I’m going over the important elements below -

 

2

The name carries no specific significance and can be any value.

 

The realm is very important and has to match the values expected by the service bus; when reading messages from a service bus subscription the realm URI should be set to: http://<servicebusnamespace>.servicebus.windows.net/<topicname>/Subscriptions/<subscriptionname>/ ; to send messages to a service bus topic the realm URI should be set to: http://<servicebusnamespace>.servicebus.windows.net/<topicname>/

 

Note: I’ve got the URL set to HTTP not HTTPS

 

When actually calling service bus you’ll typically include a /messages at the end of the URL. This must not be included here.

 

Set the token format to SWT as this is what the Service Bus expects and generate the token value in order to be able to save the relying party.

 

As we only want to be able to authenticate using the certificate I unchecked the Windows Live Id Identity provider.

 

I’ve left the “Create new rule group” checked as I will need a new rule group for this token and clicked on save.

 

A new key has been generated in the portal for the SWT key and I had to delete this key by going to Certificate and Keys, found the token in the token signing section and deleted it. This is the token for the relying party. I’m using the default service namespace keys so I don’t require the relying party one.

 

3

 

Create the Rule Group

With the relying party created I moved on to set the rule group up. Remembering that I left the “Create new rule group” checked there is a rule group with the name “Default Rule Group for <relyingpartyname>” created for me. Clicking on the link for the group displayed the Edit Rule Group screen.

 

4

 

So now, I needed to create the claims that allowed access to topic.

 

The screenshot below shows the claim configuration for a topic, I’m going over the important elements below -

 

5

 

The rule I need to setup is all about identifying the requestor according to the service identity and, if valid, output the relevant service bus action claim, to do so I:

 

Set the Input claim issuer to Access Control Service.

 

Set the Input Claim Type to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier

 

The input claim type is very important and has to match the name I specified for the Service Identity. In my case this was myserviceidentity.

 

Service Bus only supports one output claim type: net.windows.servicebus.action. So, I set the Output Claim to this type.

 

For the output claim values, for Service Bus, I have three options: Listen, Send or Manage. For the topic I’m setting up, only Send or Manage are valid. I set it to Send and then save the claim rule. For a rule relating to a Service Bus Subscription I set the action to the Listen claim.

 

I could have added Manage as well, and to do this I would just click the Add link on the rule group and repeat the same steps with an output claim value of Manage.

 

Calling Service Bus

With all the pieces in place in ACS I moved to calling the service bus topic -

 

I’ve divided the next bit into two distinct parts:

1.Creating the token that can be used to call service bus

2.Sending or Listening on the topic / subscription in Service Bus

 

Creating the Token

I used the PFX file that I generated from the self-signed certificate from IIS (the same PFX I used to export the public key CER file ealier).

 

For simplicity I’m reading the certificate file from the file system. Really this should come from the certificate store on the machine. I used the following code to read the PFX certificate file from the file system into a byte array I had to supply the password to the PFX file as well otherwise it won’t load:

 

 

       private static byte[] ReadBytesFromPfxFile(string pfxFileName, string protectionPassword)
       {
            //
            // Read the bytes from the .pfx file.
            //
            byte[] signingCertificate;
            using (FileStream stream = File.OpenRead(pfxFileName))
            {
                using (BinaryReader br = new BinaryReader(stream))
                {
                    signingCertificate = br.ReadBytes((int)stream.Length);
                }
            }
 
            //
            // Double check on the read byte array by creating a X509Certificate2 object which should not throw.
            //
            X509Certificate2 cert = new X509Certificate2(signingCertificate, protectionPassword);
 
            if (!cert.HasPrivateKey)
            {
                throw new InvalidDataException(pfxFileName + "doesn't have a private key.");
            }
 
            return signingCertificate;
        }
 

Next I had to create a SAML assertion from the byte array. The SAML assertion is going to be sent to ACS to get the token that enables access to the service bus topic.

 

In the code below I’m supplying the nameIdentifier, this is the Service Identity value I set in ACS, the certificate in bytes, its password and the service bus namespace. I construct the URL for connecting to ACS from the service bus namespace supplied. The address for ACS will be the service bus namespace suffixed with –sb. So the full URL looks like: https://<servicebusnamespace>-sb.accesscontrol.windows.net

 

    /// 
        /// Creates a SAML assertion signed with the given certificate.
        /// 
        private static Saml2SecurityToken GetSamlAssertionSignedWithCertificate(string nameIdentifierClaim, byte[] certificateWithPrivateKeyRawBytes, string password, string serviceBusNamespace)
        {
            string acsUrl = string.Format(CultureInfo.InvariantCulture, "https://{0}-sb.{1}", serviceBusNamespace, AcsHostUrl);

            Saml2Assertion assertion = new Saml2Assertion(new Saml2NameIdentifier(nameIdentifierClaim));

            Saml2Conditions conditions = new Saml2Conditions();
            conditions.NotBefore = DateTime.MinValue;
            conditions.NotOnOrAfter = DateTime.MaxValue;
            conditions.AudienceRestrictions.Add(new Saml2AudienceRestriction(new Uri(acsUrl, UriKind.RelativeOrAbsolute)));
            assertion.Conditions = conditions;

            Saml2Subject subject = new Saml2Subject();
            subject.SubjectConfirmations.Add(new Saml2SubjectConfirmation(Saml2Constants.ConfirmationMethods.Bearer));
            subject.NameId = new Saml2NameIdentifier(nameIdentifierClaim);
            assertion.Subject = subject;

            X509SigningCredentials clientSigningCredentials = new X509SigningCredentials(
                    new X509Certificate2(certificateWithPrivateKeyRawBytes, password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable));

            assertion.SigningCredentials = clientSigningCredentials;

            return new Saml2SecurityToken(assertion);
        }


 

Next I converted the token to a string this way:

 

            private static string GetSaml2TokenString(Saml2SecurityToken token)
        {
            XmlWriterSettings writerSettings = new XmlWriterSettings();
            StringBuilder sb = new StringBuilder();

            writerSettings.OmitXmlDeclaration = true;

            using (XmlWriter xw = XmlWriter.Create(sb, writerSettings))
            {
                new Saml2SecurityTokenHandler().WriteToken(xw, token);

                return sb.ToString();
            }
        }


I chose to call ACS using OAuth so I needed to set up an OAuth request. The ACS URL is the same as when creating the SAML assertion, the token is the string created from the code above and the applies to is the realm is the value I set when I created the relying party in ACS. The realm gets assigned to the parameter OAuth2Constants.Scope.

 

The code below sets this request I used WIF 3.5 for this:

 

    private static string AuthenticateWithAcsWithCertificate(string accessControlUrl, string samlToken, string appliesTo)
        {
            //
            // Encode the SAML token into an OAuth request to ACS.
            //
            Dictionary<string, string> requestParameters = new Dictionary<string, string>();
            requestParameters[OAuth2Constants.Scope] = appliesTo;
            requestParameters[OAuth2Constants.GrantType] = WifTokenTypes.Saml2TokenProfile11;
            requestParameters[OAuth2Constants.Assertion] = samlToken;

            Dictionary<string, string> acsResponse = GetOAuth2ResponseFromAcs(requestParameters, accessControlUrl);

            //
            // Pass the access token returned by ACS to the protected resource.
            //
            return acsResponse[OAuth2Constants.AccessToken];
        }
    

 

The GetOAuth2ResponseFromAcs below actually makes the call to ACS and the value return in AuthenticateWithAcsWithCertificate is the access token that will be supplied to service bus.

 

           private static Dictionary<string, string> GetOAuth2ResponseFromAcs(Dictionary<string, string> requestParameters, string serviceBusNamepsace)
        {
            string acsOAuth2Endpoint = String.Format(CultureInfo.InvariantCulture, "{0}/v2/OAuth2-13", serviceBusNamepsace);

            WebRequest acsRequest = WebRequest.Create(acsOAuth2Endpoint);

            acsRequest.AuthenticationLevel = AuthenticationLevel.None;
            acsRequest.ContentType = OAuth2Constants.ContentTypes.UrlEncoded;
            acsRequest.Method = "POST";

            string requestData = requestParameters.Encode();

            acsRequest.ContentLength = requestData.Length;

            StreamWriter requestWriter = new StreamWriter(acsRequest.GetRequestStream(), Encoding.ASCII);

            requestWriter.Write(requestData);
            requestWriter.Close();

            HttpWebResponse acsResponse = acsRequest.GetResponse() as HttpWebResponse;

            Dictionary<string, string> acsResponseParameters = new Dictionary<string, string>();
            StreamReader sr = new StreamReader(acsResponse.GetResponseStream());

            acsResponseParameters.DecodeFromJson(sr.ReadToEnd());

            return acsResponseParameters;
        }
    

 

As I’m working with certificates I found that calling the ACS /v2/OAuth2-13 endpoint address worked for me – I couldn’t get the certificate to work when using the WRAPv0.9 method – I’ll go back and figure out why in the future. In my testing I could use the WRAPv0.9 method with passwords and symmetric keys.

 

I found the ACS Code Samples a good source for pulling all this code together.

 

Sending or receiving messages from Service Bus

So, finally, I had a token from ACS with my valid permissions for accessing the topic, the next step is to call the service bus itself, passing the token with the request.

 

To do that, I created a webclient and then added an Authorisation Header that contained the AC token.

 

The header value needed to have the format: WRAP access_token=”<ACSToken>”.

 

                WebClient webClient = new WebClient();
            webClient.Headers[HttpRequestHeader.Authorization] = String.Format("WRAP access_token=\"{0}\"", token);

 

The last step was to set the URL to call service bus.

 

The format for sending to service bus is:

 

http(s)://<servicebusnamespace>.servicebus.windows.net/<topicName>/messages

 

The format for receiving from service bus is:

 

http(s)://<servicebusnamespace>.servicebus.windows.net/<topicName>/Subscriptions/<subscription>/messages/head

 

I set the address to send to the topic I wanted and the message I wish to send:

 

    var address = String.Format("https://{0}.{1}/{2}/messages",
                serviceNamespace, "servicebus.windows.net", topicName);

 

            var messageBytes = Encoding.UTF8.GetBytes(message);
            byte[] uploadData = webClient.UploadData(address, "POST", messageBytes);

 

And upon calling UploadData on the webClient, one new message appeared on each subscription for the topic.

 

So, in summary – calling the service bus from .net using certificates for authentication is just a little bit involved, but is fairly straight forward – once ACS is set-up correctly the code needs to obtain a token for each request and attached that to the Service Bus HTTP request.

Written by Iain Quick at 14:00

Categories :

0 Comments :

Comment

Comments closed