Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Data.AppConfiguration
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Data.AppConfiguration.Samples.Tests", "samples\Azure.Data.AppConfiguration.Samples.Tests.csproj", "{4C4126CB-6EC1-4DFB-9A1F-C52C440BC5CC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Identity", "..\..\identity\Azure.Identity\src\Azure.Identity.csproj", "{042736B0-D082-4366-BD2B-02D49FEC6073}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -39,6 +41,10 @@ Global
{4C4126CB-6EC1-4DFB-9A1F-C52C440BC5CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4C4126CB-6EC1-4DFB-9A1F-C52C440BC5CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4C4126CB-6EC1-4DFB-9A1F-C52C440BC5CC}.Release|Any CPU.Build.0 = Release|Any CPU
{042736B0-D082-4366-BD2B-02D49FEC6073}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{042736B0-D082-4366-BD2B-02D49FEC6073}.Debug|Any CPU.Build.0 = Debug|Any CPU
{042736B0-D082-4366-BD2B-02D49FEC6073}.Release|Any CPU.ActiveCfg = Release|Any CPU
{042736B0-D082-4366-BD2B-02D49FEC6073}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ public partial class ConfigurationClient
protected ConfigurationClient() { }
public ConfigurationClient(string connectionString) { }
public ConfigurationClient(string connectionString, Azure.Data.AppConfiguration.ConfigurationClientOptions options) { }
public ConfigurationClient(System.Uri endpoint, Azure.Core.TokenCredential credential) { }
public ConfigurationClient(System.Uri endpoint, Azure.Core.TokenCredential credential, Azure.Data.AppConfiguration.ConfigurationClientOptions options) { }
public virtual Azure.Response<Azure.Data.AppConfiguration.ConfigurationSetting> AddConfigurationSetting(Azure.Data.AppConfiguration.ConfigurationSetting setting, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Response<Azure.Data.AppConfiguration.ConfigurationSetting> AddConfigurationSetting(string key, string value, string label = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Data.AppConfiguration.ConfigurationSetting>> AddConfigurationSettingAsync(Azure.Data.AppConfiguration.ConfigurationSetting setting, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,62 +23,76 @@ public AuthenticationPolicy(string credential, byte[] secret)
_secret = secret;
}

public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
{
await ProcessAsync(message, async: true).ConfigureAwait(false);
string contentHash = CreateContentHash(message);
AddHeaders(message, contentHash);
ProcessNext(message, pipeline);
}

public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
{
string contentHash = await CreateContentHashAsync(message).ConfigureAwait(false);
AddHeaders(message, contentHash);
await ProcessNextAsync(message, pipeline).ConfigureAwait(false);
}

private async ValueTask ProcessAsync(HttpMessage message, bool async)
private static string CreateContentHash(HttpMessage message)
{
string contentHash;
using var alg = SHA256.Create();

using (var alg = SHA256.Create())
using (var memoryStream = new MemoryStream())
using (var contentHashStream = new CryptoStream(memoryStream, alg, CryptoStreamMode.Write))
{
using (var memoryStream = new MemoryStream())
using (var contentHashStream = new CryptoStream(memoryStream, alg, CryptoStreamMode.Write))
{
if (message.Request.Content != null)
{
if (async)
{
await message.Request.Content.WriteToAsync(contentHashStream, message.CancellationToken).ConfigureAwait(false);
}
else
{
message.Request.Content.WriteTo(contentHashStream, message.CancellationToken);
}
}
}

contentHash = Convert.ToBase64String(alg.Hash);
message.Request.Content?.WriteTo(contentHashStream, message.CancellationToken);
}

using (var hmac = new HMACSHA256(_secret))
return Convert.ToBase64String(alg.Hash);
}

private static async ValueTask<string> CreateContentHashAsync(HttpMessage message)
{
using var alg = SHA256.Create();

using (var memoryStream = new MemoryStream())
using (var contentHashStream = new CryptoStream(memoryStream, alg, CryptoStreamMode.Write))
{
Uri uri = message.Request.Uri.ToUri();
var host = uri.Host;
var pathAndQuery = uri.PathAndQuery;

string method = message.Request.Method.Method;
DateTimeOffset utcNow = DateTimeOffset.UtcNow;
var utcNowString = utcNow.ToString("r", CultureInfo.InvariantCulture);
var stringToSign = $"{method}\n{pathAndQuery}\n{utcNowString};{host};{contentHash}";
var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.ASCII.GetBytes(stringToSign))); // Calculate the signature
string signedHeaders = "date;host;x-ms-content-sha256"; // Semicolon separated header names

message.Request.Headers.SetValue("Date", utcNowString);
message.Request.Headers.SetValue("x-ms-content-sha256", contentHash);
message.Request.Headers.SetValue("Authorization", $"HMAC-SHA256 Credential={_credential}&SignedHeaders={signedHeaders}&Signature={signature}");
if (message.Request.Content != null)
{
await message.Request.Content.WriteToAsync(contentHashStream, message.CancellationToken).ConfigureAwait(false);
}
}

return Convert.ToBase64String(alg.Hash);
}

public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
private void AddHeaders(HttpMessage message, string contentHash)
{
ProcessAsync(message, async: false).GetAwaiter().GetResult();
var utcNowString = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
var authorization = GetAuthorizationHeader(message.Request, contentHash, utcNowString);

ProcessNext(message, pipeline);
message.Request.Headers.SetValue("x-ms-content-sha256", contentHash);
message.Request.Headers.SetValue(HttpHeader.Names.Date, utcNowString);
message.Request.Headers.SetValue(HttpHeader.Names.Authorization, authorization);
}

private string GetAuthorizationHeader(Request request, string contentHash, string date) {
const string signedHeaders = "date;host;x-ms-content-sha256"; // Semicolon separated header names

var uri = request.Uri.ToUri();
var host = uri.Host;
var pathAndQuery = uri.PathAndQuery;
var method = request.Method.Method;

var stringToSign = $"{method}\n{pathAndQuery}\n{date};{host};{contentHash}";
var signature = ComputeHash(stringToSign); // Calculate the signature
return $"HMAC-SHA256 Credential={_credential}&SignedHeaders={signedHeaders}&Signature={signature}";
}

private string ComputeHash(string value)
{
using var hmac = new HMACSHA256(_secret);
return Convert.ToBase64String(hmac.ComputeHash(Encoding.ASCII.GetBytes(value)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -15,7 +16,7 @@ namespace Azure.Data.AppConfiguration
/// </summary>
public partial class ConfigurationClient
{
private readonly Uri _baseUri;
private readonly Uri _endpoint;
private readonly HttpPipeline _pipeline;
private readonly ClientDiagnostics _clientDiagnostics;

Expand Down Expand Up @@ -47,19 +48,49 @@ public ConfigurationClient(string connectionString, ConfigurationClientOptions o
if (options == null)
throw new ArgumentNullException(nameof(options));

ParseConnectionString(connectionString, out _baseUri, out var credential, out var secret);
ParseConnectionString(connectionString, out _endpoint, out var credential, out var secret);

_pipeline = HttpPipelineBuilder.Build(options,
new HttpPipelinePolicy[] { new CustomHeadersPolicy() },
new HttpPipelinePolicy[] {
new ApiVersionPolicy(options.GetVersionString()),
new AuthenticationPolicy(credential, secret),
new SyncTokenPolicy() },
new ResponseClassifier());
_pipeline = CreatePipeline(options, new AuthenticationPolicy(credential, secret));

_clientDiagnostics = new ClientDiagnostics(options);
}

/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationClient"/> class.
/// </summary>
/// <param name="endpoint">The <see cref="Uri"/> referencing the app configuration storage.</param>
/// <param name="credential">The token credential used to sign requests.</param>
public ConfigurationClient(Uri endpoint, TokenCredential credential)
: this(endpoint, credential, new ConfigurationClientOptions())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationClient"/> class.
/// </summary>
/// <param name="endpoint">The <see cref="Uri"/> referencing the app configuration storage.</param>
/// <param name="credential">The token credential used to sign requests.</param>
/// <param name="options">Options that allow configuration of requests sent to the configuration store.</param>
public ConfigurationClient(Uri endpoint, TokenCredential credential, ConfigurationClientOptions options)
{
Argument.AssertNotNull(endpoint, nameof(endpoint));
Argument.AssertNotNull(credential, nameof(credential));

_endpoint = endpoint;
_pipeline = CreatePipeline(options, new BearerTokenAuthenticationPolicy(credential, GetDefaultScope(endpoint)));

_clientDiagnostics = new ClientDiagnostics(options);
}

private static HttpPipeline CreatePipeline(ConfigurationClientOptions options, HttpPipelinePolicy authenticationPolicy)
=> HttpPipelineBuilder.Build(options,
new HttpPipelinePolicy[] { new CustomHeadersPolicy() },
new HttpPipelinePolicy[] { new ApiVersionPolicy(options.GetVersionString()), authenticationPolicy, new SyncTokenPolicy() },
new ResponseClassifier());

private static string GetDefaultScope(Uri uri)
=> $"{uri.GetComponents(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped)}/.default";

/// <summary>
/// Creates a <see cref="ConfigurationSetting"/> if the setting, uniquely identified by key and label, does not already exist in the configuration store.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private void BuildUriForKvRoute(RequestUriBuilder builder, ConfigurationSetting

private void BuildUriForKvRoute(RequestUriBuilder builder, string key, string label)
{
builder.Reset(_baseUri);
builder.Reset(_endpoint);
builder.AppendPath(KvRoute, escape: false);
builder.AppendPath(key);

Expand All @@ -104,7 +104,7 @@ private void BuildUriForKvRoute(RequestUriBuilder builder, string key, string la

private void BuildUriForLocksRoute(RequestUriBuilder builder, string key, string label)
{
builder.Reset(_baseUri);
builder.Reset(_endpoint);
builder.AppendPath(LocksRoute, escape: false);
builder.AppendPath(key);

Expand Down Expand Up @@ -172,14 +172,14 @@ internal static void BuildBatchQuery(RequestUriBuilder builder, SettingSelector

private void BuildUriForGetBatch(RequestUriBuilder builder, SettingSelector selector, string pageLink)
{
builder.Reset(_baseUri);
builder.Reset(_endpoint);
builder.AppendPath(KvRoute, escape: false);
BuildBatchQuery(builder, selector, pageLink);
}

private void BuildUriForRevisions(RequestUriBuilder builder, SettingSelector selector, string pageLink)
{
builder.Reset(_baseUri);
builder.Reset(_endpoint);
builder.AppendPath(RevisionsRoute, escape: false);
BuildBatchQuery(builder, selector, pageLink);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\identity\Azure.Identity\src\Azure.Identity.csproj" />
<ProjectReference Include="..\src\Azure.Data.AppConfiguration.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Core.Testing;
using Azure.Identity;
using NUnit.Framework;

namespace Azure.Data.AppConfiguration.Tests
Expand Down Expand Up @@ -34,6 +36,14 @@ private ConfigurationClient GetClient()
Recording.InstrumentClientOptions(new ConfigurationClientOptions())));
}

private ConfigurationClient GetAADClient()
{
string endpoint = Recording.RequireVariableFromEnvironment("APPCONFIGURATION_ENDPOINT_STRING");
TokenCredential credential = Recording.GetCredential(new DefaultAzureCredential());
ConfigurationClientOptions options = Recording.InstrumentClientOptions(new ConfigurationClientOptions());
return InstrumentClient(new ConfigurationClient(new Uri(endpoint), credential, options));
}

private ConfigurationSetting CreateSetting()
{
return new ConfigurationSetting()
Expand Down Expand Up @@ -1264,5 +1274,22 @@ public async Task ClearReadOnlySettingNotFound()
await service.DeleteConfigurationSettingAsync(testSetting.Key, testSetting.Label);
}
}

[Test]
public async Task AddSettingDefaultAAD()
{
ConfigurationClient service = GetAADClient();
ConfigurationSetting testSetting = CreateSetting();

try
{
ConfigurationSetting setting = await service.AddConfigurationSettingAsync(testSetting);
Assert.True(ConfigurationSettingEqualityComparer.Instance.Equals(testSetting, setting));
}
finally
{
await service.DeleteConfigurationSettingAsync(testSetting.Key, testSetting.Label);
}
}
}
}
Loading