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

3

10

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)?

Steppe answered 26/3, 2019 at 8:25 Comment(3)
Did my answer solve your issue?Leakey
@TonyJu It's not directly the answer to my question, but a good guide. I figured it out my self how to do it. Let me sum it up in my own answer. I eventually mark yours as the correct answer anyway. Give me 2 days.Steppe
Hello @TonyJu - I've posted my own answer. Maybe you could help to improve it or leave your thoughts on it. I've upvoted and referenced your answer but marked mine as the correct one.Steppe
S
10

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;
            }
        }
    }
}
Steppe answered 29/3, 2019 at 10:9 Comment(3)
I didn’t understand your solution. I am trying to do the same thing. Send an email with a link to blob but that cannot be done as you stated. I am not quite sure I understand your solution in layman terms. How will using a Function App help my user to download a file if I send the link to a file in an email?Yellow
I found this answer very helpful in pointing me in the right direction, but since Easy Auth now uses V2 for AAD you can no longer just add "resource=storage.azure.com" to your login parameters and I'm struggling to figure out how to get a valid token.Centenary
I finally figured it out. Instead of adding a resource parameter you have to add your scopes to login parameters in authsettingsv2. Importantly this seems to override the default scopes so make sure to include those like this: "loginParameters": ["scope=openid profile offline_access storage.azure.com/user_impersonation"]Centenary
L
5

If you want to use Azure Active Directory based access control for the storage, what you need to get is the access token. Here are the steps for your reference.

  1. Register an application

2.Assign a build-in RBAC role to this application It depends on you which role to be assigned to the application. enter image description here

3.Get the access token. enter image description here

4.With the access token, now you can call the storage rest api. enter image description here

Leakey answered 27/3, 2019 at 0:54 Comment(1)
I was getting error message, "Authentication scheme Bearer is not supported in this version." when accessing blob service Rest API. There was nothing wrong with my access token. It was the x-ms-version that was causing the error! I was able to read private blob with bearer access token and x-ms-version. Both x-ms-version 2018-03-28 and 2019-07-07 worked.Ventura
V
0

Just to expand a tiny bit on Lars' comment on the answer above about adding scopes in authsettingsV2, you need to add this under properties.identityProviders.login, for example:

"login": {
  "loginParameters": [
    "scope=openid https://storage.azure.com/.default"
  ],
  "disableWWWAuthenticate": false
}

Took me ages to figure out where to put it...

Varga answered 7/12, 2023 at 12:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.