Introduction

Recently I decided to deploy a Laravel site so that customers can upload large files privately and securely. Similar to Dropbox but without its bells and whistles. I did not want to share access keys or other secrets. I did not want uploads to go to my website and then my website uploads to Azure storage. I wanted a secure method for the customer to directly upload files to a private storage blob container.

The Laravel Filesystem interface makes working with Azure blob storage almost too easy. Basically, you can treat blob storage as local files. Reading, writing, creating, files and listing directories becomes so easy. This simplifies testing and software development.

I first implemented an HTTP redirect for HTTP PUT. Then I realized that the HTTP body was being sent to my server first and then the redirect occurred. I should be able to fix that with an HTTP 100 Continue, but I decided to implement direct uploads to Azure instead of a redirect to Azure. I will revisit the HTTP 100 Continue feature another time.

I implemented Azure Blob Storage uploads and downloads using Shared Access Signatures. First I create an account for the user. The user receives account details via email. I wrote a frontend in Javascript that requests a URL with a SAS signature from my site and then uses that URL to upload a file directly to Azure blob storage. Downloads use SAS URLs as well. This method implements strong security without requiring access keys or other secrets in the browser. The SAS URL is created for a specific object with specific permissions (write or read) and the URL has a short expiration. Objects are also stored relative to the user’s home directory (storage prefix). I use the user’s ID for the object prefix name. The only thing the customer can do is either upload a specific file or download a specific file. Access to my SAS Signing API endpoint requires logging into my site.

There is a very good Laravel package for Azure Blob Storage written by Matthew Daly called Laravel Azure Storage. This package provides a laravel filesystem interface for the PHP League Flysystem Azure Blob Storage package which sits on top of the Microsoft Azure Storage PHP Client Libraries.

The documentation to install the Laravel Azure Storage package is very clean and well implemented. However, documentation is lacking for examples on how to use the package with Azure Storage, in particular setting up the credentials and creating SAS URLs. This article will show how.

Summary of the Azure blob storage packages that will be installed (directly and through dependencies):

  • matthewbdaly/laravel-azure-storage version 1.6.2 [link]
  • league/flysystem-azure-blob-storage version 1.0 [link]
    • There is a version 2 but I do not see compatibility yet for Azure. Maybe a future project.
  • microsoft/azure-storage-blob version 1.5.2 [link]

Installing the Laravel Azure Storage package

Note: I found a bug with the Microsoft Azure Storage PHP Client Libraries version 1.5.2 for PHP 8. If you are using PHP 7, you can ignore this issue. Otherwise, I will show the changes to fix this bug later in this article. Microsoft has fixed the bug in the GitHub repository but has not released a new version since December 2020.

UPDATE: Microsoft released version 1.5.3 after I started writing this article. I can confirm the bug fix was released in 1.5.3.

Requirements:

  • php_fileinfo.dll
  • php_mbstring.dll
  • php_openssl.dll
  • php_xsl.dll
  • php_curl.dll – Recommended

I recommend creating a new Laravel project to test with first before adding Azure storage to an existing project.

Create a new Laravel project and configure the database and .env file. Optionally install Breeze or Jetstream to provide user site authorization.

Execute the following command to install Matthew Daly’s package which will also install the other Azure storage dependencies:

composer require matthewbdaly/laravel-azure-storage

Configure Laravel Filesystems for Azure Storage

Edit the file config/filesystems.php

Add the following section. If you want to enable caching, uncomment the cache section. I found issues with the Laravel filesystem remembering directories at one point in time and not updating for 600 seconds (configurable) when modified outside of Laravel. This is a typical cache synchronization issue. Enabling caching does improve performance.

 'azure' => [
    'driver'    => 'azure',
    'name'      => env('AZURE_STORAGE_NAME'),
    'key'       => env('AZURE_STORAGE_KEY'),
    'container' => env('AZURE_STORAGE_CONTAINER'),
    'url'       => env('AZURE_STORAGE_URL'),
    'prefix'    => env('AZURE_STORAGE_PREFIX'),
    'retry'     => [
        'tries' => 3,
        'interval' => 500,
        'increase' => 'exponential'
    ],
/*
    'cache'     => [
        'store' => 'filecache',
        'expire' => 600,
        'prefix' => 'filecache',
    ]
*/
],

Configuring Azure Secrets

This section will use the Azure CLI to get the configuration and secrets that we need to set up Laravel Azure Storage. These secrets are stored in the .env file.

Edit your Laravel .env file and add the following:

AZURE_STORAGE_NAME=
AZURE_STORAGE_KEY=
AZURE_STORAGE_CONTAINER=
AZURE_STORAGE_URL=
AZURE_STORAGE_PREFIX=
AZURE_SIGNING_URL=
AZURE_SIGNING_URL_PERMISSION=rw

In the next steps, we will add values for those variables.

Azure Storage Account

You will need the name of your Azure Storage Account. The following command will display the accounts in your subscription.

az storage account list  --query "[].name

Select the account and store the value for AZURE_STORAGE_NAME.

Azure Storage Key

When you create a storage account, Azure generates two 512-bit storage account access keys. We need one of those keys for AZURE_STORAGE_KEY.

Use the following command to list your storage account keys. You will need to modify the command with your Azure Resource Group and Azure Storage Account Name (from the previous command):

az storage account keys list --resource-group REPLACE_ME --account-name REPLACE_ME

Select one of the keys and store the value for AZURE_STORAGE_KEY.

Azure Storage Container

This is the Azure storage account container. To list the containers in your storage account:

az storage container list --account-name REPLACE_ME --query "[].name"

Select one of the containers and store the value for AZURE_STORAGE_CONTAINER.

Azure Storage URL

The Azure storage URL is used to generate URLs to access objects. This URL is constructed from your Azure Storage Account Name.

For example, your Azure storage account name is contosohttps://contoso.blob.core.windows.net/

Example:

AZURE_STORAGE_URL=https://contoso.blob.core.windows.net/

Azure Storage Prefix

This value is appended to the Azure Storage URL and Azure Storage Container when generating URLs.

Example: Your storage account name is contoso, the container name is laravel and the prefix is users:

The generated URLs will start with https://contoso.blob.core.windows.net/laravel/users.

My example code generates the prefix based upon the User ID. Leave this value blank.

AZURE_STORAGE_PREFIX=

Azure Signing URL

The Azure Signing URL is the API endpoint (Laravel route) in my code that generates a SAS URL based upon a filename sent from the browser. Later in my code examples, I will show you how to generate the Signed URL. For now, use this example to create the URL:

https://YOUR_SERVER_DOMAIN_OR_IP:PORT/azure-sign/

Example:

AZURE_SIGNING_URL=http://localhost:8000/azure-sign/

Azure Signing URL Permissions

This value is stored in the AZURE_SIGNING_URL_PERMISSION. This value determines the permissions that my signing code uses to generate the SAS URL.

Azure Signed Permissions:

  • a – Add (Message processing for Queues)
  • c – Create
  • d – Delete
  • l – List
  • p – Processing (Message processing for Queues)
  • r – Read
  • u – Update (Tables)
  • w – Write

I included all of the ones I know about including ones that apply to other services. I had to review the source code to find these values.

My code only supports reading and writing, therefore use AZURE_SIGNING_URL_PERMISSION=rw.

For listing files in a user’s account, I implement that feature in my server via a Laravel Blade. That is to prevent one user from listing another user’s files.

Azure Storage Bug in Version 1.5.2

For PHP 8, the following function has a bug with checking for zero-length strings. I commented out the problem line and appended the correction. See line 18 below.

This function begins at line 277 of vendor\microsoft\azure-storage-common\src\Common\SharedAccessSignatureHelper.php

Note: Microsoft has made this correction in the GitHub Repository. [link]

protected function validateAndSanitizeStringWithArray($input, array $array)
{
    $result = '';
    foreach ($array as $value) {
        if (strpos($input, $value) !== false) {
            //append the valid permission to result.
            $result .= $value;
            //remove all the character that represents the permission.
            $input = str_replace(
                $value,
                '',
                $input
            );
        }
    }

    Validate::isTrue(
        // **BUG FIX** strlen($input) == '',
        strlen($input) == 0,
        sprintf(
            Resources::STRING_NOT_WITH_GIVEN_COMBINATION,
            implode(', ', $array)
        )
    );
    return $result;
}

Summary

At this point, the Laravel Azure Storage package is installed and configured.

Listing objects stored in Azure Storage

This example does not use a controller or view. This is simply a quick test example to see if Laravel is now configured correctly with Azure storage.

Edit the file routes/web.php. Add the following content:

Route::get('/azure-test', function() {
        $path = '';

        // Get the Larvel disk for Azure
        $disk = \Storage::disk('azure');

        // List files in the container path
        $files = $disk->files($path);

        // create an array to store the names, sizes and last modified date
        $list = array();

        // Process each filename and get the size and last modified date
        foreach($files as $file) {
                $size = $disk->size($file);

                $modified = $disk->lastModified($file);
                $modified = date("Y-m-d H:i:s", $modified);

                $filename = "$path/$file";

                $item = array(
                        'name' => $filename,
                        'size' => $size,
                        'modified' => $modified,
                );

                array_push($list, $item);
        }

        $results = json_encode($list, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

        return response($results)->header('content-type', 'application/json');
});

I am Assuming that you are running Laravel on your local development system, on localhost:8000.

Open your web browser and go http://localhost:8000/azure-test/

The browser should display a JSON list of blobs in your bucket. Example:

[
    {
        "name": "/_testfile",
        "size": 1371884822,
        "modified": "2021-10-06 19:39:53"
    },
    {
        "name": "/favicon.ico",
        "size": 1150,
        "modified": "2021-10-07 21:28:16"
    },
    {
        "name": "/index.html",
        "size": 2848,
        "modified": "2021-10-06 19:51:29"
    },
    {
        "name": "/default-image.jpg",
        "size": 2721267,
        "modified": "2021-10-01 20:26:05"
    }
]

Creating a Shared Access Signature (SAS) URL

In this example, the client (web browser JavaScript) calls an endpoint with a blob name and the server-side code generates a SAS URL and returns a response with the URL to the client. This is of course very dangerous if you do not have authorization implemented.

This example does not use a controller or view.

Edit the file routes/web.php. Add the following content:

Route::get('/azure-sign/{file}', function($file) {
        // Get the Larvel disk for Azure
        $disk = \Storage::disk('azure');

        $path = "$file";

        // SAS Expires in one hour
        $ttl = now()->addHours(1);

        $options = [
                'signed_permissions' => env('AZURE_SIGNING_URL_PERMISSION')
        ];

        $url = $disk->temporaryUrl($path, $ttl, $options);

        return $url;
});

I am Assuming that you are running Laravel on your local development system, on localhost:8000.

Open your web browser and go http://localhost:8000/azure-sign/testfile

The browser should display a SAS URL. The first part is the AZURE_STORAGE_URL and the second part is the SAS token.

Example:

https://test.blob.core.windows.net/laravel/testfile?sv=2017-11-09&sr=b&se=2021-10-10T22:02:30Z&sp=rw&spr=https&sig=T8NHhxdAc%2BReabcdefghijklmnopj0ZQDIbGMGO7UylQ%3D

This code does not check if the blob exists. My code might be getting a SAS URL to upload an object. If you specify the name of an existing blob, the returned URL will allow you to read it in the browser. To upload a blob requires writing code to execute an HTTP PUT method.

Read an Azure Blob and Return the Contents

This example shows how to read an Azure storage blob and return the contents to the client. The server-side code will read the blob and return the contents to the user as the HTTP response.

Edit the file routes/web.php. Add the following content. This example shows setting the Content-Type to image/jpeg, you will want to add code that returns a real value for Content-Type or none at all and the browser will decide.

Route::get('/azure/{file}', function ($file) {
        $filename = "$file";

        $disk = Storage::disk('azure');

        if (!$disk->exists($filename))
        {
                abort(404);
        }

        $contents = $disk->get($filename);

        return response($contents)->header('content-type', 'image/jpeg');
});

In the browser, specify a valid azure storage blob name. Assuming that you have an Azure storage blob named testfile, the following URL will display or download the file in the browser.

http://localhost:8000/testfile

More Examples

I am packing examples into a GitHub repository. Once that is complete, I will add the URL here.

Photography Credits

I write free articles about technology. Recently, I learned about Pexels.com which provides free images. The image in this article is courtesy of Pixabay at Pexels.