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

后端 未结 2 1558
Happy的楠姐
Happy的楠姐 2021-02-06 13:23

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

2条回答
  •  既然无缘
    2021-02-06 13:58

    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.

    "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.

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

    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 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;
                }
            }
        }
    }
    

提交回复
热议问题