I'm working on developing an API that allows users to retrieve blobs from Azure Storage (RBAC) using Azure AD OAuth2 authentication. Here's the scenario:
I need to ensure that the API returns a blob is user has access else return Auth Error if the authenticated user doesn't have permission to access a specific blob in the requested storage account and container.
I'm creating a blob client based on these details with this .NET Code.
Uri blobUri = new Uri($"https://{storageAccountName}.blob.core.windows.net/{containerName}/{filePath}");
BlobClient blobClient = new BlobClient(blobUri, new DefaultAzureCredential());
The issue is I'm able to access the required blob if I'm running this in my local dev setup (I have to do az login in Azure Cli). But when I try to run it in Deployed app on Azure then it gives 403 error.
I've have made sure that user at least have Blob Reader Access & reader access in resource group. I have given delegation user_impersonation api permissions for Azure Storage
Doc used for reference: https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/?tabs=command-line
Any guidance or code examples on how to implement this securely and efficiently would be greatly appreciated. Thank you!
The reason why the following code works in your local environment is because there it is using your credentials (assuming you are already logged in) and not using the token you sent through API call.
BlobClient blobClient = new BlobClient(blobUri, new DefaultAzureCredential());
For the same reason, it fails when your code is running in the App Service. Because your App Service does not have any RBAC role assigned on the storage account, you will get 403 error.
If you want to use the user's access token, the way you would solve this problem is by creating a custom class extending TokenCredential
(DefaultAzureCredential
extends TokenCredential
).
The code would be something like the following:
/// <summary>
/// Creates a token credential class using an access token.
/// </summary>
public class AccessTokenCredential : TokenCredential
{
/// <summary>
/// Creates an instance of <see cref="AccessTokenCredential"/>.
/// </summary>
/// <param name="accessToken">
/// JWT encoded access token.
/// </param>
public AccessTokenCredential(string accessToken)
{
AccessToken = accessToken;
}
/// <summary>
/// Gets the access token.
/// </summary>
private string AccessToken { get; }
/// <summary>
///
/// </summary>
/// <param name="requestContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(GetAccessToken());
}
/// <summary>
///
/// </summary>
/// <param name="requestContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return GetAccessToken();
}
/// <summary>
/// Validates access token and returns <see cref="AccessToken"/>.
/// </summary>
/// <returns>
/// <see cref="AccessToken"/>.
/// </returns>
/// <exception cref="ArgumentException">
/// Access token is invalid.
/// </exception>
private AccessToken GetAccessToken()
{
JwtSecurityToken token = new JwtSecurityToken(AccessToken);
return new AccessToken(AccessToken, token.ValidTo);
}
}
and your code will be something like:
BlobClient blobClient = new BlobClient(blobUri, new AccessTokenCredential("your-access-token"));
Auth Token can be exchanged via this obo flow: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow