You're testing a download from Azure Blob Storage and you've generated a SAS token that you've double-checked—it looks fine. The URL is right, the container exists, but you get a 403 AuthorizationFailure. The weird part? The token hasn't expired, and you just created it. Sound familiar?
This error often shows up when you're running a script that generates a SAS token on one machine and then uses it on another—say, a dev machine and a server. Or when you're using a SAS token in an app that runs on a VM with a misconfigured clock. The token is valid, but Azure sees it as expired or not yet active because the request time doesn't match.
Root Cause: Clock Skew and SAS Scope
Azure checks the timestamp on the SAS token against the current UTC time on the server. If your client's clock is off by even a few minutes, the token might appear to be from the future or past, triggering an AuthorizationFailure. People rarely suspect their server clock because it's usually the last thing on their mind.
Another common cause is that the SAS token was generated with a specific IP range or protocol (HTTPS only) and your request doesn't match. For example, you might generate a SAS with an IP restriction to your office, then try to use it from a cloud VM—that'll give you a 403 even though the token is technically valid.
The Fix: Step by Step
- Check the server clock. On the machine making the request, open a terminal and run
date -u(Linux/macOS) orw32tm /stripchart /computer:time.windows.com /samples:1on Windows. Compare the output to the actual UTC time from a service like time.gov. If it's off by more than a couple of minutes, that's your problem. - Sync the clock. On Linux, use
sudo ntpdate pool.ntp.orgor enable NTP withsudo timedatectl set-ntp true. On Windows, runw32tm /resync. If you're in a VM, make sure the VM time sync is enabled. For example, in Hyper-V, check integration services. On AWS EC2, the Amazon Time Sync service should be configured. - Verify SAS token scope. Go to the Azure portal, navigate to your storage account, and under "Shared access signature," check the allowed IP addresses and protocols. If you set an IP range, make sure your current public IP falls within it. Also confirm the protocol is HTTPS if you're using an HTTP endpoint (though you should be using HTTPS always).
- Regenerate a fresh SAS token. Sometimes the issue is that the start time of the SAS is in the future, even by a few seconds. When generating a SAS programmatically, use a start time of
DateTime.UtcNow.AddMinutes(-5)to allow for clock skew. Here's a C# example:var sasBuilder = new AccountSasBuilder { Services = AccountSasServices.Blobs, ResourceTypes = AccountSasResourceTypes.Object, ExpiresOn = DateTime.UtcNow.AddHours(1), StartsOn = DateTime.UtcNow.AddMinutes(-5) // the key fix }; - Test with a simple tool. Use AzCopy or the Azure Storage Explorer to download the blob with the same SAS URL. If that works, the issue is in your code. If it fails, the token is the problem.
What If It Still Fails?
If the clock is synced, the token scope looks right, and a fresh token still gives a 403, then you're dealing with a different layer. Check these:
- Storage account firewall. If you've enabled "Allow access from selected networks" on the storage account, and the client IP isn't on the allowlist, you'll get a 403. Even if the SAS is valid, the network rule blocks the request. Go to Networking > Firewalls and virtual networks and add the client's public IP.
- Shared access policy mismatch. If you're using a stored access policy, make sure the policy exists and hasn't been deleted. A deleted policy will invalidate all SAS tokens that reference it.
- Signing key rotation. If you've rotated the storage account key after generating the SAS, the token is dead. Regenerate the SAS with the current key.
In my experience, 9 out of 10 times it's the clock. It's such a simple thing that people overlook it, but Azure's time validation is strict. So check that first, and you'll save yourself an hour of head-scratching.