Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/storage/Azure.Storage.Blobs/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "net",
"TagPrefix": "net/storage/Azure.Storage.Blobs",
"Tag": "net/storage/Azure.Storage.Blobs_b46f524eaf"
"Tag": "net/storage/Azure.Storage.Blobs_42626018f4"
}
65 changes: 65 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/src/BlobAudience.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using Azure.Core;

namespace Azure.Storage.Blobs
{
/// <summary>
/// Audiences available for Blobs
/// </summary>
public readonly partial struct BlobAudience : IEquatable<BlobAudience>
{
private readonly string _audience;

/// <summary>
/// Intializes new instance of <see cref="BlobAudience"/>.
/// </summary>
/// <param name="audience">
/// The Azure Active Directory audience to use when forming authorization scopes.
/// For the Language service, this value corresponds to a URL that identifies the Azure cloud where the resource is located.
/// For more information: <see href="https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory" />.
/// </param>
/// <remarks>Please use one of the static constant members over creating a custom value unless you have specific scenario for doing so.</remarks>
public BlobAudience(string audience)
{
Argument.AssertNotNullOrEmpty(audience, nameof(audience));
_audience = audience;
}

private const string _publicAudience = "https://storage.azure.com/.default";

/// <summary>
/// Default Audience. Use to acquire a token for authorizing requests to any Azure Storage account
///
/// Resource ID: &quot;https://storage.azure.com/.default &quot;.
///
/// If no audience is specified, this is the default value.
/// </summary>
public static BlobAudience PublicAudience { get; } = new(_publicAudience);
Comment thread
amnguye marked this conversation as resolved.

/// <summary>
/// The service endpoint for a given storage account.
/// Use this method to acquire a token for authorizing requests to that specific Azure Storage account and service only.
/// </summary>
/// <param name="storageAccountName"></param>
/// <returns></returns>
public static BlobAudience BlobServiceAccountAudience(string storageAccountName) => new($"https://{storageAccountName}.blob.core.windows.net/.default");
Comment thread
amnguye marked this conversation as resolved.
Outdated

/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object obj) => obj is BlobAudience other && Equals(other);
/// <inheritdoc />
public bool Equals(BlobAudience other) => string.Equals(_audience, other._audience, StringComparison.InvariantCultureIgnoreCase);

/// <inheritdoc />
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => _audience?.GetHashCode() ?? 0;
/// <inheritdoc />
public override string ToString() => _audience;
Comment thread
amnguye marked this conversation as resolved.
Outdated
}
}
14 changes: 8 additions & 6 deletions sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -330,12 +330,14 @@ public BlobBaseClient(Uri blobUri, AzureSasCredential credential, BlobClientOpti
/// </param>
public BlobBaseClient(Uri blobUri, TokenCredential credential, BlobClientOptions options = default)
: this(
blobUri,
credential.AsPolicy(options),
options,
storageSharedKeyCredential: null,
sasCredential: null,
tokenCredential: credential)
blobUri,
credential.AsPolicy(
options?.Audience != null ? options.Audience.Value.ToString() : BlobAudience.PublicAudience.ToString(),
options),
options,
storageSharedKeyCredential: null,
sasCredential: null,
tokenCredential: credential)
{
Errors.VerifyHttpsTokenAuth(blobUri);
}
Expand Down
6 changes: 6 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/src/BlobClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,5 +339,11 @@ internal HttpPipeline Build(object credentials)

/// <inheritdoc />
public bool EnableTenantDiscovery { get; set; }

/// <summary>
/// Gets or sets the Audience to use for authentication with Azure Active Directory (AAD). The audience is not considered when using a shared key.
/// </summary>
/// <value>If <c>null</c>, <see cref="BlobAudience.PublicAudience" /> will be assumed.</value>
public BlobAudience? Audience { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,10 @@ public BlobContainerClient(Uri blobContainerUri, TokenCredential credential, Blo
Errors.VerifyHttpsTokenAuth(blobContainerUri);
Argument.AssertNotNull(blobContainerUri, nameof(blobContainerUri));
_uri = blobContainerUri;
_authenticationPolicy = credential.AsPolicy(options);

BlobAudience audience = options?.Audience != null ? options.Audience.Value : BlobAudience.PublicAudience;

_authenticationPolicy = credential.AsPolicy(audience.ToString(), options);
options ??= new BlobClientOptions();

_clientConfiguration = new BlobClientConfiguration(
Expand Down
8 changes: 7 additions & 1 deletion sdk/storage/Azure.Storage.Blobs/src/BlobServiceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,13 @@ public BlobServiceClient(Uri serviceUri, AzureSasCredential credential, BlobClie
/// every request.
/// </param>
public BlobServiceClient(Uri serviceUri, TokenCredential credential, BlobClientOptions options = default)
: this(serviceUri, credential.AsPolicy(options), credential, options ?? new BlobClientOptions())
: this(
serviceUri,
credential.AsPolicy(
options?.Audience != null ? options.Audience.Value.ToString() : BlobAudience.PublicAudience.ToString(),
options),
credential,
options ?? new BlobClientOptions())
{
Errors.VerifyHttpsTokenAuth(serviceUri);
}
Expand Down
113 changes: 113 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/tests/AppendBlobClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,119 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri()
e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature"));
}

[RecordedTest]
public async Task Ctor_DefaultAudience()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));
await blob.CreateIfNotExistsAsync();

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience);

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
bool exists = await aadBlob.ExistsAsync();
Assert.IsTrue(exists);
}

[RecordedTest]
public async Task Ctor_CustomAudience()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));
await blob.CreateIfNotExistsAsync();

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{test.Container.AccountName}.blob.core.windows.net/.default"));

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
bool exists = await aadBlob.ExistsAsync();
Assert.IsTrue(exists);
}

[RecordedTest]
public async Task Ctor_StorageAccountAudience()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));
await blob.CreateIfNotExistsAsync();

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(BlobAudience.BlobServiceAccountAudience(test.Container.AccountName));

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
bool exists = await aadBlob.ExistsAsync();
Assert.IsTrue(exists);
}

[RecordedTest]
public async Task Ctor_AudienceError()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

AppendBlobClient blob = InstrumentClient(test.Container.GetAppendBlobClient(GetNewBlobName()));
await blob.CreateIfNotExistsAsync();

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(new BlobAudience("https://badaudience.blob.core.windows.net"));

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

AppendBlobClient aadBlob = InstrumentClient(new AppendBlobClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
await TestHelper.AssertExpectedExceptionAsync<Identity.AuthenticationFailedException>(
aadBlob.ExistsAsync(),
e => Assert.IsTrue(e.Message.Contains("ClientSecretCredential authentication")));
}

[RecordedTest]
public void WithSnapshot()
{
Expand Down
129 changes: 129 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/tests/BlobBaseClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,135 @@ public async Task Ctor_AzureSasCredential_VerifyNoSasInUri()
e => e.Message.Contains($"You cannot use {nameof(AzureSasCredential)} when the resource URI also contains a Shared Access Signature"));
}

[RecordedTest]
public async Task Ctor_DefaultAudience()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

var data = GetRandomBuffer(Constants.KB);
BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName()));
using (var stream = new MemoryStream(data))
{
await blob.UploadAsync(stream);
}

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(BlobAudience.PublicAudience);

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
bool exists = await aadBlob.ExistsAsync();
Assert.IsTrue(exists);
}

[RecordedTest]
public async Task Ctor_CustomAudience()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

var data = GetRandomBuffer(Constants.KB);
BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName()));
using (var stream = new MemoryStream(data))
{
await blob.UploadAsync(stream);
}

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(new BlobAudience($"https://{test.Container.AccountName}.blob.core.windows.net/.default"));

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
bool exists = await aadBlob.ExistsAsync();
Assert.IsTrue(exists);
}

[RecordedTest]
public async Task Ctor_StorageAccountAudience()
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

var data = GetRandomBuffer(Constants.KB);
BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName()));
using (var stream = new MemoryStream(data))
{
await blob.UploadAsync(stream);
}

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(BlobAudience.BlobServiceAccountAudience(test.Container.AccountName));

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
bool exists = await aadBlob.ExistsAsync();
Assert.IsTrue(exists);
}

[RecordedTest]
public async Task Ctor_AudienceError()
Comment thread
amnguye marked this conversation as resolved.
{
// Arrange
await using DisposingContainer test = await GetTestContainerAsync();

var data = GetRandomBuffer(Constants.KB);
BlobClient blob = InstrumentClient(test.Container.GetBlobClient(GetNewBlobName()));
using (var stream = new MemoryStream(data))
{
await blob.UploadAsync(stream);
}

// Act - Create new blob client with the OAuth Credential and Audience
BlobClientOptions options = GetOptionsWithAudience(new BlobAudience("https://badaudience.blob.core.windows.net"));

BlobUriBuilder uriBuilder = new BlobUriBuilder(new Uri(Tenants.TestConfigOAuth.BlobServiceEndpoint))
{
BlobContainerName = blob.BlobContainerName,
BlobName = blob.Name
};

BlobBaseClient aadBlob = InstrumentClient(new BlobBaseClient(
uriBuilder.ToUri(),
Tenants.GetOAuthCredential(),
options));

// Assert
await TestHelper.AssertExpectedExceptionAsync<AuthenticationFailedException>(
aadBlob.ExistsAsync(),
e => Assert.IsTrue(e.Message.Contains("ClientSecretCredential authentication")));
}

#region Sequential Download

[RecordedTest]
Expand Down
Loading