Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access a blob file via URI over a web browser using new AAD based access control

With the announcement of Azure Storage support for Azure Active Directory based access control, is it possible to serve a blob (a specific file) over a web browser just by it's URI?

The use case I want to simplify is giving a few people access to files on the blob without the need of having to append a SAS token to the URI. Instead it would be brilliant to have the typical OAuth flow started when trying to open the plain URI in his/her web browser.

In my case we want to give access to files that have been uploaded to the blob storage by users through our support bot, build on Microsoft Bot framework. Links in our support system should be accessible by a support agent in their web browser of choice.

It this use case supported by this announcement or does this only work for coded OAuth flows, meaning we still have to implement some code?

If so, is there a good sample on how to start the OAuth flow from a Azure Function app and use the resulting token to download the file (over Azure Storage REST endpoint)?

like image 951
Sebastian Zolg Avatar asked Mar 26 '19 08:03

Sebastian Zolg


People also ask

How do I access Azure blob storage from my browser?

One way to find the URL of the blob is by using the Azure portal by going to Home > Storage Account > Container > Blob > Properties. However, probably the easiest way is to find the blob in the Storage Explorer, right-click, then select 'Copy URL'. This will copy the resource URL directly to the clipboard.

How do I access files in BLOB storage?

View a blob container's contentsOpen Storage Explorer. In the left pane, expand the storage account containing the blob container you wish to view. Expand the storage account's Blob Containers. Right-click the blob container you wish to view, and - from the context menu - select Open Blob Container Editor.

What is Blob URI in Azure?

Azure Blob storage is Microsoft's object storage solution for the cloud. Blob storage is optimized for storing massive amounts of unstructured data. Unstructured data is data that doesn't adhere to a particular data model or definition, such as text or binary data.


1 Answers

While this answer is technically correct, it wasn't a direct response to my initial question.

I was looking for a way to provide the direct uri of any blob to business users, so they can simply open it in any web browser and see the file.

In my case we wanted to give access to files that have been uploaded to the blob storage by users through our support bot, build on Microsoft Bot framework. E.g. serving the attachment as a link in our support system to be accessed by a support agent.

After digging into this, I can answer the question my self:

With the announcement of Azure Storage support for Azure Active Directory based access control, is it possible to serve a blob (a specific file) over a web browser just by it's URI?

No, this is not possible. More specifically, simply opening the direct uri to a blob in the browser doesn't trigger the OAuth flow. Instead it will always give you ResourceNotFound response unless you provide a SAS query token or set the blob to public. Both solutions are bad from security perspective (when normal users involved) and obviously bad UX.

Solution

Looking for a way to achieve exactly what I want, I came up with the idea of a azure function serving the attachment to any business user by passing the fileName as url parameter and constructing the path using a route template.

Thinking of security and the need for an access token anyway, you could protect the function app through platform authentication (a.k.a. easyAuth).

However, this is not enough and configuring all parts of the solution is not straight forward. That is why I'm sharing it.

TL;DR high-level steps:

  1. Create a new Function App (v2 recommended)
  2. Enable the function App for authentication (easyAuth)
  3. Create a service principal (a.k.a. app registration) for the function app (implicit by step 2)
  4. Add additional allowed token audience https://storage.microsoft.com on the app registration
  5. Edit the manifest of the app registration to include Azure Storage API permission (see special remarks below)
  6. Modify authSettings in Azure Resource explorer to include additionalLoginParams for token response and resourceId
  7. Give at least the Storage Blob Data Reader permission on the blob to all users accessing the files
  8. Deploy your function app, call it, access the user token, call the blob storage and present the result to the user (see code samples below)

Remarks on Azure Storage API permission and access token (Step 5 & 6)

As stated in the latest documentation for AAD authentication support on azure storage, the app must grand user_impersonation permission scope for resourceId https://storage.azure.com/. Unfortunately the documentation did not state on how to set this API permission as it is not visible in the portal (at least I did not find it).

So the only way is to set it through its global GUID (can be found on the internet) by editing the app registration manifest directly in the azure portal.

Update: As it turned out, not finding the right permission in the portal is a bug. See my answer here. Modifying the manifest manually results in the same, but directly doing it in the portal is much more convenient.

Manifest

"requiredResourceAccess": [
    {
        "resourceAppId": "e406a681-f3d4-42a8-90b6-c2b029497af1",
        "resourceAccess": [
            {
                "id": "03e0da56-190b-40ad-a80c-ea378c433f7f",
                "type": "Scope"
            }
        ]
    },
    {
        "resourceAppId": "00000002-0000-0000-c000-000000000000",
        "resourceAccess": [
            {
                "id": "311a71cc-e848-46a1-bdf8-97ff7156d8e6",
                "type": "Scope"
            }
        ]
    }
]

The first one is the user_impersonation scope on Azure Storage and the second is the graph permission for User.Read, which in most cases is helpful or needed.

After you uploaded your modified manifest, you can verify it on the API Permissions tab on your app registration.

As easyAuth is using the v1 endpoint of AAD, your app needs to request those permission statically by passing resource=https://storage.azure.com/ when triggering the OAuth flow.

Additionally Azure Storage requires the bearer schema for authentication header and therefore a JWT token is needed. To get a JWT token from the endpoint, we need to pass response_type=code id_token as an additional login parameter.

Both can only be done through Azure Resource explorer or powershell.

Using Azure Resource explorer you have to navigate all your way down to the authSettings on your function app and set the additionalLoginParams accordingly.

enter image description here

"additionalLoginParams": [
  "response_type=code id_token",
  "resource=https://storage.azure.com/"
]

enter image description here

Code Sample

Here is a complete code sample for an easy azure function using all aboves mechanisms.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;

namespace Controller.Api.v1.Org
{
    public static class GetAttachment
    {
        private const string defaultContentType = "application/octet-stream";

        [FunctionName("GetAttachment")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "v1/attachments")] HttpRequest req,
            ILogger log)    
        {
            if (!req.Query.ContainsKey("fileName"))
                return new BadRequestResult();

            // Set the file name from query parameter
            string fileName = req.Query["fileName"];

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);

            fileName = fileName ?? data?.name;

            // Construct the final uri. In this sample we have a applicaiton setting BLOB_URL
            // set on the function app to store the target blob
            var blobUri = Environment.GetEnvironmentVariable("BLOB_URL") + $"/{fileName}";

            // The access token is provided as this special header by easyAuth.
            var accessToken = req.Headers.FirstOrDefault(p => p.Key.Equals("x-ms-token-aad-access-token", StringComparison.OrdinalIgnoreCase));

            // Construct the call against azure storage and pass the user token we got from easyAuth as bearer
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Value.FirstOrDefault());
                client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate");
                client.DefaultRequestHeaders.Add("Accept", "*/*");
                client.DefaultRequestHeaders.Add("x-ms-version", "2017-11-09");

                // Serve the response directly in users browser. This code works against any browser, e.g. chrome, edge or even internet explorer
                var response = await client.GetAsync(blobUri);
                var contentType = response.Content.Headers.FirstOrDefault(p => p.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase));
                var byteArray = await response.Content.ReadAsByteArrayAsync();

                var result = new FileContentResult(byteArray, contentType.Value.Any() ? contentType.Value.First() : defaultContentType);

                return result;
            }
        }
    }
}
like image 56
Sebastian Zolg Avatar answered Sep 19 '22 05:09

Sebastian Zolg