0
votes

I have this function running in an azure function to get a sas token for a browser application to upload to azure blob storage:

var azure = require('azure-storage');

module.exports = function(context, req) {
  if (req.body.container) {
    // The following values can be used for permissions:
    // "a" (Add), "r" (Read), "w" (Write), "d" (Delete), "l" (List)
    // Concatenate multiple permissions, such as "rwa" = Read, Write, Add
    context.res = generateSasToken(
      context,
      req.body.container,
      req.body.blobName,
      req.body.permissions
    );
  } else {
    context.res = {
      status: 400,
      body: "Specify a value for 'container'"
    };
  }

  context.done(null, context);
};

function generateSasToken(context, container, blobName, permissions) {
  var connString = process.env.AzureWebJobsStorage;
  var blobService = azure.createBlobService(connString);

  // Create a SAS token that expires in an hour
  // Set start time to five minutes ago to avoid clock skew.
  var startDate = new Date();
  startDate.setMinutes(startDate.getMinutes() - 5);
  var expiryDate = new Date(startDate);
  expiryDate.setMinutes(startDate.getMinutes() + 60);

  permissions = azure.BlobUtilities.SharedAccessPermissions.READ +
                azure.BlobUtilities.SharedAccessPermissions.WRITE +
                azure.BlobUtilities.SharedAccessPermissions.DELETE +
                azure.BlobUtilities.SharedAccessPermissions.LIST;

  var sharedAccessPolicy = {
    AccessPolicy: {
      Permissions: permissions,
      Start: startDate,
      Expiry: expiryDate
    }
  };

  var sasToken = blobService.generateSharedAccessSignature(
    container,
    blobName,
    sharedAccessPolicy
  );

  context.log(sasToken);

  return {
    token: sasToken,
    uri: blobService.getUrl(container, blobName, sasToken, true)
  };
}

I am then calling this url in the client and I try and upload with this code:

const search = new URLSearchParams(`?${token}`);

const sig = encodeURIComponent(search.get('sig'));

const qs = `?sv=${search.get('sv')}&ss=b&srt=sco&sp=rwdlac&se=${search.get('sv')}&st=${search.get(
  'st'
)}&spr=https&sig=${sig}`;

return `${url}/${containerName}/${filename}${qs}`;

Which generates a url like this:

https://mystorage.blob.core.windows.net/mycontainer/latest.png?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2018-03-28&st=2019-01-30T19:11:10Z&spr=https&sig=g0sceq3EkiAQTvyaZ07C+C4SZQz9FaGTV4Zwq4HkAnc=

Which returns this error:

403 (Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.)

If I generate the sas token from the azure portal it works, so the generated url looks like this:

https://mystorage.blob.core.windows.net/mycontainer/latest.png?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-01-31T03:01:43Z&st=2019-01-30T19:01:43Z&spr=https&sig=ayE4gt%2FDfDzjv5DjMaD7AS%2F176Bi4Q6DWJNlnDzl%2FGc%3D

but my url looks like this:

https://mystorage.blob.core.windows.net/mycontainer/latest.png?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-01-31T03:34:21Z&st=2019-01-30T19:34:21Z&spr=https&sig=Dx8Vm4XPnD1rn9uyzIAXZEfcdbWb0HjmOq%2BIq42Q%2FOM%3D

I have no idea what to do to get this working

3
Could you mark one useful answer for others to refer if your problem has been solved?Jerry Liu

3 Answers

0
votes

Your Azure Function code is correct, and

var sasToken = blobService.generateSharedAccessSignature(
    container,
    blobName,
    sharedAccessPolicy
);

is exactly the sasToken you need to upload blob. No need to process the token again(mishandle actually) as you have done in the 2nd code snippet.

It's expected that the sas token from the Azure portal(Account SAS) is different from the one generated in your code(Service SAS). Have a look at the doc.

To conclude,

  1. Make sure the connection string belongs to the Storage you want to connect. You could avoid trouble and directly replace var connString = process.env.AzureWebJobsStorage; with var connString = "connectionStringGotFromPortal";

  2. If 1 is confirmed, your Azure function code is correct and returns token as expected

    {
      token: sasToken,
      uri: blobService.getUrl(container, blobName, sasToken, true)
    };  
    
  3. Based on the 2nd code snippet you provide, you only need

    return `${url}/${containerName}/${filename}?${token}`; 
    

    if the token is identical to what function returns.

0
votes

The problem is that in your server-side code you're creating a Service SAS and then taking only signature portion of the code (sig) and creating an Account SAS on the client.

Since the parameters used to create token has now changed (in the original one, you didn't have parameters like ss, srt etc. but when you're creating your own URL, you're inserting these parameters), when you use the modified SAS URL you will get 403 error. This is happening because server again computes the signature based on the URL parameters and compare that with the signature passed in the URL. Since the two signatures won't match, you're getting the 403 error.

Since you're returning the SAS URL of the blob, there's no need for you to create the URL on the client. You can simply use the uri you're returning from your API layer on the client and use that to upload.

0
votes

As Jerry Liu's answer explained your Azure function generates the correct token and already gives you the the correct uri to use which includes your blob name and token.

In your client side you can also use azure-sdk-for-js

// This is the response from your api with token and uri    
const uri = response.uri;

const pipeline = StorageURL.newPipeline(new AnonymousCredential());

// Your uri already includes the full blob url with SAS signature
const blockBlobURL = BlockBlobURL.fromBlobURL(new BlobURL(uri, pipeline));
const uploadBlobResponse = await blockBlobURL.upload(
  Aborter.none,
  file,
  file.size,
  { blobHTTPHeaders: { blobContentType: `${mime}; charset=utf-8`} }
);