Article summary
Shared access signatures (SAS) are a seemingly straightforward way an application can authorize requests to an Azure storage account.
The SAS token is the query string that includes all the information that’s required to authorize a request. The token specifies the resource that a client may access, the permissions granted, and the time period during which the signature is valid.
But when you start digging into the Microsoft docs about creating a service SAS, you will be met with an overwhelming wall of information.
I’ll walk you through the steps to write a TypeScript function that can generate a SAS for a couple of common operations. We’ll be using the 2022-11-02 version of the API.
Skeleton Function
First, let’s create a function that accepts the necessary input fields and returns an empty string.
const generateSas = (
accessKey: string,
resourceType: 'container' | 'blob',
canonicalizedResource: string,
signedPermissions: string
): string => {
return '';
}
Before we can move on, let’s make sure we understand what each of those inputs are.
accessKey
: Key used to authorize the request. You can find it by navigating to your storage account in portal.azure.com and selecting the “Access keys” blade on the left. Click the “Show” button for key 1 and copy the value.resourceType
: The type of resource you are accessing in the API call. The Authorization section in the documentation for the API call should tell you which type of resource to use.canonicalizedResource
: This sounds very complicated but it’s just the path to the resource you’re accessing. If it’s a container, it will be/blob/${storageAccountName}/${containerName}
. If it’s a blob, it will be/blob/${storageAccountName}/${containerName}/${blobName}
.signedPermissions
: This is the permissions you are granting via the SAS. We’ll discuss this in more detail later.
Format the SAS
Now that we have our skeleton function, let’s start to build the actual query parameter string that we need to return.
import *as crypto from 'crypto';
const generateSas = (
accessKey: string,
resourceType: 'container' | 'blob',
canonicalizedResource: string,
signedPermissions: string
): string => {
// Pick the correct value based on the resource type
const signedResource = resource === 'blob' ? 'b' : 'c';
// You will probably want to use the current time as the signedStart
// and make it expire after a short time - like 1 or 2 minutes.
const signedStart = '2023-11-07T00:00:00Z';
const signedEnd = '2023-11-07T00:01:00Z';
// These are constants.
const signedProtocol = 'https';
const signedVersion = '2022-11-02';
const dataToSign = ''; // We'll fill this in later
const signature = encodeURIComponent(
crypto
.createHmac('sha256', Buffer.from(accessKey, 'base64'))
.update(dataToSign, 'utf8')
.digest('base64')
);
return `sp=${signedPermissions}&st=${signedStart}&se=${signedExpiry}&`
+ `spr=${signedProtocol}$&sv=${signedVersion}$&sr=${signedResource}&`
+ `sig=${signature}`;
}
Here, we are using the crypto module to create an HMAC over dataToSign
using our accessKey
. We are then constructing and returning a query parameter string.
This version of the function is really close to working, but it will still be rejected as an unauthorized request by Azure since the signature will not match, as we signed an empty string.
Signing the Correct Data
This might be the trickiest part of generating a valid SAS. The dataToSign
has to follow a very specific format that requires the fields to be in the correct order and newline-separated. Let’s add it to our function.
import as crypto from 'crypto';
const generateSas = (
accessKey: string,
resourceType: 'container' | 'blob',
canonicalizedResource: string,
signedPermissions: string
): string => {
// Pick the correct value based on the resource type
const signedResource = resourceType === 'blob' ? 'b' : 'c';
// You will probably want to use the current time as the signedStart
// and make it expire after a short time - like 1 or 2 minutes.
const signedStart = '2023-11-07T00:00:00Z';
const signedExpiry = '2023-11-07T00:01:00Z';
// These are constants.
const signedProtocol = 'https';
const signedVersion = '2022-11-02';
// These are also constants.
const signedIdentifier = '';
const ipRange = '';
const versionId = '';
const encryptionScope = '';
const cacheControl = '';
const contentDisposition = '';
const contentEncoding = '';
const contentLanguage = '';
const contentType = '';
const dataToSign = [
signedPermissions,
signedStart,
signedExpiry,
canonicalizedResource,
signedIdentifier,
ipRange,
signedProtocol,
signedVersion,
signedResource,
versionId,
encryptionScope,
cacheControl,
contentDisposition,
contentEncoding,
contentLanguage,
contentType,
].join('\n');
const signature = encodeURIComponent(
crypto
.createHmac('sha256', Buffer.from(accessKey, 'base64'))
.update(dataToSign, 'utf8')
.digest('base64')
);
return `sp=${signedPermissions}&st=${signedStart}&se=${signedExpiry}&`
+ `spr=${signedProtocol}$&sv=${signedVersion}$&sr=${signedResource}`
+ `&sig=${signature}`;
}
You’ll notice that we just added a bunch of empty string variables, which may seem silly. This basic SAS implementation doesn’t need to specify values for any of those fields. But if we did want to use them, having the variable names already in place makes it easy to figure out where we need to update the code. It’s also helpful for ensuring we haven’t missed a required field.
Permissions
We skipped over the permissions early, but they are a crucial part of building a correct SAS. You can find the full documentation here and in the Authorization for Service SAS section for each request, but we’ll briefly discuss how to build the permissions string for two different operations.
Reading Contents of Blob
If you want to get the contents of a blob, you will need to send a request like this:
GET https://myaccount.blob.core.windows.net/mycontainer/myblob
The permissions are simple in this case. It’s just r
, for “read.”
Creating a Blob with Tags
If you want to create a new blob and add some tags to it, you will need to send a request like this:
PUT https://myaccount.blob.core.windows.net/mycontainer/myblob?x-ms-tags=tag1=value1
The permissions are a little trickier. This PUT
request can be used to both create a new blob and update an existing blob, so we should use cw
for “create” and “write”, to handle both scenarios. If we want to read or update the tags for a blob, we also have to provide t
for “tags.” So the final permissions string will be cwt
.
Example Usage
Let’s use the two examples above to generate two different SAS strings. Both examples will use the final version of our function from above.
// In real life, you should load this from an environment variable or
// some other configurable source instead of hard-coding it in
const accessKey = 'k7sBH0ieZyPkuRyGjH+7ibx932DzECxl';
// We can use this SAS to create a blob in the "mycontainer"
// container of the storage account "myaccount"
const createBlobSas = generateSas(
accessKey, 'container', '/blob/myaccount/mycontainer', 'cwt'
);
console.log(createBlobSas);
// sp=cwt&st=2023-11-07T00:00:00Z&se=2023-11-07T00:01:00Z&
// spr=https$&sv=2022-11-02$&sr=c&
// sig=bHm6ZdepzJTi%2BVsY9cMl%2Bn0QnU70sD68%2B%2FDikKsSz%2BA%3D
// We can use this SAS to read the contents of a blob called "app-data.json"
// in the "mycontainer" container of the storage account "myaccount"
const readBlobSas = generateSas(
accessKey, 'blob', '/blob/myaccount/mycontainer/app-data.json', 'r'
);
console.log(readBlobSas);
// sp=r&st=2023-11-07T00:00:00Z&se=2023-11-07T00:01:00Z&
// spr=https$&sv=2022-11-02$&sr=b&
// sig=RPVW8GAoOnv3jdoeLtyn8M5JM7QQ1AWCFmyM2ntCvxc%3
These steps get you a TypeScript function that can generate a SAS for a couple of common operations, and which can be extended for any APIs you need to use.