How to Use SAS Tokens with Azure Blob Storage
14.07.2020, Christian Mäder

Azure Blob Storage is an Azure service to store files. It is comparable to the well-known S3 Storage by Amazon Web Services (AWS).

SAS Tokens grant arbitrary client applications permission to manipulate certain files on the Azure Blob Storage. These tokens’ validity is limited to a certain time-span and the actions that clients are allowed to perform are restricted as well.

This post explains how and when to use SAS Tokens.

Azure Blob Storage

When using the Azure Cloud, an instance of the Azure Storage is almost always involved. There are different characteristics of the corresponding product. But this article is all about the Azure Blob Storage.

The product’s main purpose is to store small and large files in it using a REST API. It is comparable to the product Simple Storage Service from Amazon Web Services (AWS), better known as S3, which works similarly and is also accessed via a REST API.

An instance of a Blob Storage is called an Account by Microsoft. An Account has several Containers, and various Blobs are stored in these Containers. (Blob is an abbreviation commonly used in computer science for Binary Large Object). In this article, one file corresponds to one blob, which is probably also the most common way of using it.

Visualization of the hierarchy of the terms Account, Container and Blob.

Each Storage Account has an Azure-wide unique and unchangeable name. The API of a specific Storage Account is accessed through this name. Requests are sent to the well-known URL http://storageaccountname.blob.core.windows.net.

Containers in a Storage Account can be made public. Then everyone can access the stored files through the Internet without authentication, provided they know (or guess) the respective path to the file. Normally, however, the Containers are not publicly accessible. Then you need a valid Access Token for each request.

There are exactly two of these Access Tokens per Storage Account. The tokens have to be treated like passwords: Whoever has the token has access to the corresponding files of the whole Storage Account. When users want to upload or retrieve files to the Blob Storage, then they should not be provided with one of the actual Access Tokens. Because then all users could see or change all files – or add ones – uncontrolled.

Shared Access Signature

This is where the Shared Access Signatures (short SAS) come into play, which is what this article is all about. This is a special type of Access Token, which can be transferred to a third party (e.g. a user application), to execute predefined actions on the Storage Account for a limited amount of time.

The statement predefined actions for a limited amount of time in the last sentence is very important. On the one hand, SAS tokens have a limited validity: It starts at a certain point in time (which can also be in the future) and ends eventually. On the other hand, one must specify exactly for what a SAS token may be used: For example for which Account, Container or Blob an action is permitted and which kind of actions are allowed (for example, create/upload, read, edit/overwrite, delete, rename, and so on).

There are different kinds of SAS tokens. These can be linked to a specific Azure service, to a user of the Azure Active Directory, or they can be Ad-Hoc. The latter means, that the tokens are anonymous and can also be issued to third parties, which are otherwise not known to Azure.

This article only covers Ad-Hoc tokens. To create such a token, the validity and the actions, that are allowed, must be defined. These are stored in Key/Value-pairs. All values are then combined to a string in a predefined format, which is signed with HMAC-SHA256. The key to the HMAC-SHA256 algorithm is the Storage Account Key.

The server application knows the Storage Account Key and uses it to calculate the SAS token, which is sent to the client application if requested. This means, that to perform actions on the Azure Blob Storage it does not need to know the actual Storage Account Key. But it can still use the SAS Token to perform the predefined actions on the Azure Blob Storage account. If the SAS token permits this, then it can use it to, for example, upload a file with an HTTP PUT action:

With the corresponding token, a file can then be uploaded using an HTTP PUT action, for example:

# To use the 'http' command, https://httpie.org/ must be installed.
cat sasblob.txt | \
 http PUT "https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt?sv=2019-02-02&st=2019-04-29T22%3A18%3A26Z&se=2019-04-30T02%3A23%3A26Z&sr=b&sp=rw&sip=168.1.5.60-168.1.5.70&spr=https&sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d"

Token Deep-Dive

Those that are not so much interested in the technical details can go directly to the Use Case.

This part describes how a SAS token is structured and how it is created. For example, the following URL contains a SAS token:

https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt?sv=2019-02-02&st=2019-04-29T22%3A18%3A26Z&se=2019-04-30T02%3A23%3A26Z&sr=b&sp=rw&sip=168.1.5.60-168.1.5.70&spr=https&sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d

All URL parameters that come after the question mark make up the SAS token. The individual parameters have the following meaning:

  • https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt is the URL to the corresponding Blob object.
  • sv=2019-02-02 means that the 2019-02-02 version of the REST API was used to calculate the signature.
  • st=2019-04-29T22%3A18%3A26Z specifies the begin of the validity period in UTC; here April 29, 2019, 22:18:26.

    Because of the URL encoding, the value looks very cryptic. Without URL encoding this is already noticeably more readable:
    st=2019-04-29T22:18:26Z

  • se=2019-04-30T02%3A23%3A26Z specifies the begin of the validity period in UTC; here April 30, 2019, 02:23:26.

    Because of the URL encoding, the value looks very cryptic. Without URL encoding this is already noticeably more readable:
    st=2019-04-29T02:23:26Z

  • sr=b indicates that the permissions apply to a specific Blob.
  • sp=rw indicates that the token allows reading and writing on the Blob.
  • sip=168.1.5.60-168.1.5.70 indicates that only users, who’s requests originate from the IP ranges 168.1.5.60 until 168.1.5.70, are allowed to use that token.
  • spr=https indicates that a TLS-encrypted connection is required.
  • sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d is the signature of the token, which is the Base64 encoded signature generated using by the HMAC-SHA256 algorithm.

To generate the signature, the constraints defined above are combined into a string. This string is UTF-8 encoded and then signed with the HMAC-SHA256 algorithm, where the Storage Account Key is used as the secret key.

To verify the signature, the Azure Blob Storage Server must perform the same calculation. It uses the transmitted values from the URL and the Storage Account Key known to it and calculates the signature itself using the same algorithm. The action is only executed if the transmitted signature matches the generated one.

For this reason the structure of the string to be signed is specified. Since that structure is a little different depending on the API version, the API version must always be specified in the svparameter in the URL. The string to be signed in the above example uses API version 2019-02-02 and is structured as follows:

StringToSign = signedPermissions + "\n" +
               signedStart + "\n" +
               signedExpiry + "\n" +
               canonicalizedResource + "\n" +
               signedIdentifier + "\n" +
               signedIP + "\n" +
               signedProtocol + "\n" +
               signedVersion + "\n" +
               signedResource + "\n"
               signedSnapshotTime + "\n" +
               rscc + "\n" +
               rscd + "\n" +
               rsce + "\n" +
               rscl + "\n" +
               rsct

The values of the individual variables are to be specified in the same way as they are specified in the URL. The signedPermissions must be replaced by rw, for example.

A speciality is the construction of the value for canonicalizedResource. The path to the file must be preceded by the prefix /blob and the account name, as shown below:

URL = "https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt"
canonicalizedResource = "/blob/storageaccountname/sascontainer/sasblob.txt"

Unused fields remain empty. In the example from above this results in the following StringToSign. It must then be encoded in UTF-8.

stringToSign = UTF8(
  "rw"                     + "\n" +   // signedPermissions
  "2019-04-29T22:18:26Z"   + "\n" +   // signedStart
  "2019-04-30T02:23:26Z"   + "\n" +   // signedExpiry
  "/blob/storageaccountname/sascontainer/sasblob.txt" + "\n" + // canonicalizedResource
  ""                       + "\n" +   // signedIdentifier
  "168.1.5.60-168.1.5.70"  + "\n" +   // signedIP
  "https"                  + "\n" +   // signedProtocol
  "2019-02-02"             + "\n" +   // signedVersion
  "b"                      + "\n" +   // signedResource
  ""                       + "\n" +   // signedSnapshotTime
  ""                       + "\n" +   // rscc
  ""                       + "\n" +   // rscd
  ""                       + "\n" +   // rsce
  ""                       + "\n" +   // rscl
  "")                                 // rsct

accountKey   = Base64_Decode("jkjRQqRC7Cp3dQhbBegWUOPTfSbDhpSRXslbIHi7XWaPoVEbKOACGhQO7ENqs4r+6wobqZXOEAznojEsWnbGJQ==")

hmac         = HMAC_SHA256(accountKey, stringToSign)
signature    = URL_Encode(Base64_Encode(hmac))
// ==> "koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d"

On cryptii.com pipelines with cryptographic functions can be created to recreate the above example relatively easily. Below you find the hex encoded binary version of the AccountKey, so that it can be pasted directly as HMAC secret key:

8e48 d142 a442 ec2a 7775 085b 05e8 1650
e3d3 7d26 c386 9491 5ec9 5b20 78bb 5d66
8fa1 511b 28e0 021a 140e ec43 6ab3 8afe
eb0a 1ba9 95ce 100c e7a2 312c 5a76 c625

Furthermore here’s the StringToSign – notice the six empty lines at the end!

rw
2019-04-29T22:18:26Z
2019-04-30T02:23:26Z
/blob/storageaccountname/sascontainer/sasblob.txt

168.1.5.60-168.1.5.70
https
2019-02-02
b






Code Example (Server)

However, the whole procedure of signing does not have to be implemented manually. Microsoft has already done this in its libraries. This also allows the tokens to be generated quickly and reliably. The following code example uses the information from the above examples:

private const string StorageAccountConnectionString =
  "DefaultEndpointsProtocol=https;" +
  "AccountName=storageaccountname;" +
  "AccountKey=jkjRQqRC7Cp3dQhbBegWUOPTfSbDhpSRXslbIHi7XWaPoVEbKOACGhQO7ENqs4r+6wobqZXOEAznojEsWnbGJQ==";
private readonly CloudStorageAccount storageAccount =
  CloudStorageAccount.Parse(StorageAccountConnectionString);

private string GetSasParameters(
  string containerName, string blobName)
{
    var startsOn = DateTimeOffset.Parse("2019-04-29T22:18:26Z");
    var expiresOn = DateTimeOffset.Parse("2019-04-30T02:23:26Z");
    var ipRange = new SasIPRange(IPAddress.Parse("168.1.5.60"),
                                 IPAddress.Parse("168.1.5.70"));
    var blobSasBuilder = new BlobSasBuilder
    {
      BlobContainerName = containerName,
      BlobName = blobName,
      StartsOn = startsOn,
      ExpiresOn = expiresOn,
      Resource = "b",
      IPRange = ipRange,
      Protocol = SasProtocol.Https,
    };
    blobSasBuilder.SetPermissions(BlobSasPermissions.Write |
                                  BlobSasPermissions.Create);

    var storageAccountCredentials = storageAccount.Credentials;

    var incomingFileStorageCredentials =
      new StorageSharedKeyCredential(
        storageAccountCredentials.AccountName,
        storageAccountCredentials.ExportBase64EncodedKey());

    var blobSasQueryParameters =
      blobSasBuilder.ToSasQueryParameters(incomingFileStorageCredentials);
    return blobSasQueryParameters.ToString();
}

Use Case

At first, implementing and using SAS tokens looks like a lot of effort But there are some good reasons to design your application in this way. But first I would like to explain the conventional approach, to then demonstrate the advantages of the indirect approach using SAS tokens.

A simple document filing system shall serve as an example, that is accessible through the internet. Such an application has – traditionally – directly accepted all files itself, to write them to the local file system. If a document was requested, the application has read it from the local file system and delivered it to the client.

A graphical representation of how the flow of files is with a local file system.

When moving the application to the Azure Cloud it was extended, such that files are no longer read from the local file system, respectively written to it. Instead, the files are stored in an Azure Blob Storage and are read from it – respectively written to the same – by the application.

A graphical representation of what the flow of files is like when porting directly to Azure Blob Storage.

However, there are some disadvantages to this implementation:

  • Often there are restrictions on the size of the HTTP requests from the client to the application. For example, with Azure Functions, this is currently 100 MB per HTTP request.
  • In the cloud, the transfer of data is often invoiced. The application now receives a file and writes it to a remote file system. There’s now a double transfer fee – once from client to application and once from application to Azure Blob Storage. The same applies to reading the files, of course.
  • An application can only answer a certain number of requests at a time. And each upload and download process from/to the application ties up resources (like computing time, memory, and sockets). These resources are not available to answer other requests.
  • If several files are to be uploaded or downloaded simultaneously, then this ties up even more resources per user.
  • Depending on how the application is operated in the cloud, the computing time is billed to the second. Now, if the application is busy doing that, upload and download files, this costs money unnecessarily.

These disadvantages can all be eliminated when the application is changed in such a way, that it’s only task (at least regarding file up- and downloads) will be to issue a SAS token. The client then connects directly to the Azure Blob Storage and can get the necessary files itself – respectively transmits it there.

This allows file transfers to be parallelized as needed, without this putting pressure on the resources of the application. Up- and downloads that were canceled, because, for example, the mobile phone has switched from Wifi to the mobile network, can be resumed by the device. This allows even very large files to be uploaded without difficulties.

Furthermore, file transfers are no longer limited by the processing power of the application. Or in other words: At best, the application can be scaled down, so it has less computing power at its disposal because this is now sufficient for the remaining tasks. Again, this saves money.

A graphical representation of how the flow of files is when porting to Azure Blob Storage when SAS tokens are used

Code Example (Client)

The following example shows how a BlobClient is initialized to upload a file with it.

async System.Threading.Tasks.Task UploadFile() {
  const string sasToken = "sv=2019-02-02&st=2019-04-29T22%3A18%3A26Z&se=2019-04-30T02%3A23%3A26Z&sr=b&sp=rw&sip=168.1.5.60-168.1.5.70&spr=https&sig=koLniLcK0tMLuMfYeuSQwB%2bBLnWibhPqnrINxaIRbvU%3d"
  var file = new System.IO.FileInfo("sasblob.txt");
  var blobUri = new System.Uri("https://storageaccountname.blob.core.windows.net/sascontainer/sasblob.txt");

  var blobUriBuilder = new System.UriBuilder(blobUri)
  {
      Query = sasToken
  };

  var authorizedBlobUri = blobUriBuilder.Uri;
  var blobClient = new Azure.Storage.Blobs.BlobClient(authorizedBlobUri);

  await using var fileStream = file.OpenRead();
  var response = await blobClient.UploadAsync(fileStream, cancellationToken);
}

Experience Report

This article was not written out of pure curiosity – we had to learn these concepts the hard way. Because shortly after the direct port of one application of our customer Rodix to Azure we started to notice the limitations in the size of the HTTP requests.

As a result, we had to adopt the relevant application – and the corresponding client application – as described above. So far, our experience with the new solution has been almost exclusively good. (There were problems with one end user because the Azure Blob Storage URL was not yet allowed on the outgoing proxy server.)

Further Information

With the Azure Documentation, it is not always easy to find the information you’re looking for. Therefore the following is a list of Azure pages relevant for this article: