Azure Storage User Delegation SAS Token signature does not match

Nais 0 Reputation points
2024-09-27T15:22:09.0933333+00:00

I'm trying to connect my server (Salesforce) to Azure.

I have set up a connected app (app registration in Entra).

I'm trying to get my delegation key and generate a SAS token for my user(s).

In Salesforce, I'm calling the Get User Delegation Key endpoint and receiving a key.

I pass the user delegation key into my SAS token builder class.

Then I generate my SAS token and sign it with the value from the user delegation key.

Apex (the backend language used on the Salesforce platform) can't import any libraries like Java can, so I'm writing it out instead of using the SDKs.

I've gone through the docs line-by-line and I just don't see where my code is deviating from what I'm being instructed to do. I've tried so many permutations and read through so many other solutions and I don't see where mine is differing, so I'm here to beg for help, sorry!

So, when I call the backend method that issues a token, everything works and it does issue a token (just hardcoded stuff for now in the URL format for easy testing). When I use the URL, Azure claims that the signature does not match. Here's my code class:

    private class TokenGenerator {
        private UserDelegationKey userDelegationKey;
        private String accountName = 'teststorageaccount1g43gw';
        private String containerName = 'test-partners-1';
        private String signedVersion = '2022-11-02';
        private String signedResource = 'c';
        private String signedPermissions = 'racwdl';
        private String signedStart = DateTime.now().formatGmt('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'');
        private String signedExpiry = DateTime.now().addHours(1).formatGmt('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'');
        private String signedProtocol = 'https';
        private String signature;
        private String token;
        private Exception error;

        private Boolean execute() {
            List<String> parameters = new List<String>();
            parameters.add('sp=' + this.signedPermissions);
            parameters.add('st=' + this.signedStart);
            parameters.add('se=' + this.signedExpiry);
            parameters.add('skoid=' + this.userDelegationKey.signedKeyObjectId);
            parameters.add('sktid=' + this.userDelegationKey.signedKeyTenantId);
            parameters.add('skt=' + this.userDelegationKey.signedKeyStart);
            parameters.add('ske=' + this.userDelegationKey.signedKeyExpiry);
            parameters.add('sks=' + this.userDelegationKey.signedKeyService);
            parameters.add('skv=' + this.userDelegationKey.signedKeyVersion);
            parameters.add('spr=' + this.signedProtocol);
            parameters.add('sv=' + this.signedVersion);
            parameters.add('sr=' + this.signedResource);

            String stringToSign = String.join(new List<String>{
                this.signedPermissions,
                this.signedStart,
                this.signedExpiry,
                '/blob/' + this.accountName + '/' + this.containerName,
                this.userDelegationKey.signedKeyObjectId,
                this.userDelegationKey.signedKeyTenantId,
                this.userDelegationKey.signedKeyStart,
                this.userDelegationKey.signedKeyExpiry ,
                this.userDelegationKey.signedKeyService,
                this.userDelegationKey.signedKeyVersion,
                /* signedAuthorizedUserObjectId */ '',
                /* signedUnauthorizedUserObjectId */ '',
                /* signedCorrelationId */ '',
                /* signedIP */ '',
                this.signedProtocol,
                this.signedVersion,
                this.signedResource,
                /* signedSnapshotTime */ '',
                /* signedEncryptionScope */ '',
                /* rscc */ '',
                /* rscd */ '',
                /* rsce */ '',
                /* rscl */ '',
                /* rsct */ ''
            }, '\n');
            System.debug(stringToSign);

            String signature = EncodingUtil.base64Encode(Crypto.generateMac('HMACSHA256', Blob.valueOf(EncodingUtil.urlDecode(stringToSign, 'UTF-8')), this.userDelegationKey.signedKeyValue));
            parameters.add('sig=' + EncodingUtil.urlEncode(signature, 'UTF-8'));

            this.token = 'https://' + this.accountName + '.blob.core.windows.net/' + this.containerName + '?' + String.join(parameters, '&');
            this.token += '&restype=container&comp=list'; // TODO: Remove this bit and maybe the URL part
            System.debug(this.token);
            return true;
        }
    }

Of note is the stringToSign variable, I've copied the structure directly from the docs. It's the latest one since I'm using a version past the last update to that structure. The docs don't specify whether to add the new lines between values that I'm not providing (I've filled out all values marked as 'required' but not the others). Due to that, also tried it with and without those new lines, with and without a trailing new line too just in case etc. When it claims that the signature doesn't match, it shows the signature it produced. I have tried to copy that and just replace the dates and redo it and it still fails. I saw in a few threads that it may erroneously produce that error if you're not looking at a valid resource, so I am using the full URL format for testing with a proper REST request there.

I'm really not sure what to do at this point. Here's an example of my request and the response:

stringToSign:

racwdl

2024-09-27T15:11:10Z

2024-09-27T16:11:10Z

/blob/teststorageaccountXXXXXXXXXXXXXXXXXX/test-partners-1

49f2f4d0-07c3-4575XXXXXXXXXXXXXXXXXX

2c04bea5-efb6-4f90XXXXXXXXXXXXXXXXXX

2024-09-27T15:11:10Z

2024-09-27T16:11:10Z

b

2022-11-02

(+4 new lines)

https

2022-11-02

c

(+7 new lines)
https://teststorageaccount1g43gw.blob.core.windows.net/test-partners-1?restype=container&comp=list&sp=racwdl&st=2024-09-27T15:11:10Z&se=2024-09-27T16:11:10Z&skoid=49f2f4d0-07c3-4575-bbbc-5a5553e39372&sktid=2c04bea5-efb6-4f90-b029-0da543580ff1&skt=2024-09-27T15:11:10Z&ske=2024-09-27T16:11:10Z&sks=b&skv=2022-11-02&spr=https&sv=2022-11-02&sr=c&sig=OKBth875zAp%2FJ3fKhGg5kMnxCmN11KJZOAlApif4EYY%3D

Error 403
<Error>

<Code>AuthenticationFailed</Code>

<Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. RequestId:c9c2c9b3-601e-00e6-25ef-10555c000000 Time:2024-09-27T15:12:34.6514107Z</Message>

<AuthenticationErrorDetail>Signature did not match. String to sign used was racwdl 2024-09-27T15:11:10Z 2024-09-27T16:11:10Z /blob/teststorageaccountXXXXXXXXXXXXXXXXXX/test-partners-1 49f2f4d0-07c3-4575XXXXXXXXXXXXXXXXXX 2c04bea5-efb6-4f90XXXXXXXXXXXXXXXXXX 2024-09-27T15:11:10Z 2024-09-27T16:11:10Z b 2022-11-02 https 2022-11-02 c </AuthenticationErrorDetail>

</Error>

(And just to reiterate, I have tried with new lines, truncating repeated new lines, using spaces instead like the one it shows me, etc)

Thank you!

Azure Storage Accounts
Azure Storage Accounts
Globally unique resources that provide access to data management services and serve as the parent namespace for the services.
3,183 questions
Azure Blob Storage
Azure Blob Storage
An Azure service that stores unstructured data in the cloud as blobs.
2,876 questions
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.