Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CDMS-181 attempts to proxy connections to login.microsoftonline.com v… #7

Merged
merged 2 commits into from
Dec 9, 2024
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
11 changes: 8 additions & 3 deletions Btms.Azure/AzureService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
using Azure.Core.Diagnostics;
using Azure.Core.Pipeline;
using Azure.Identity;
using Btms.Azure.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Client;

namespace Btms.Azure;


public abstract class AzureService
{
protected readonly TokenCredential Credentials;
protected readonly HttpClientTransport? Transport;
protected readonly ILogger Logger;

protected AzureService(ILogger logger, IAzureConfig config, IHttpClientFactory? clientFactory = null)
protected AzureService(IServiceProvider serviceProvider, ILogger logger, IAzureConfig config, IHttpClientFactory? clientFactory = null)
{
Logger = logger;
using AzureEventSourceListener listener = AzureEventSourceListener.CreateConsoleLogger(EventLevel.Verbose);
Expand All @@ -22,9 +26,10 @@ protected AzureService(ILogger logger, IAzureConfig config, IHttpClientFactory?
{
logger.LogDebug("Creating azure credentials based on config vars for {AzureClientId}",
config.AzureClientId);

Credentials =
new ClientSecretCredential(config.AzureTenantId, config.AzureClientId, config.AzureClientSecret);

new ConfidentialClientApplicationTokenCredential(serviceProvider, config);
logger.LogDebug("Created azure credentials");
}
else
Expand Down
45 changes: 45 additions & 0 deletions Btms.Azure/ConfidentialClientApplicationTokenCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Azure.Core;
using Btms.Azure.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Client;

namespace Btms.Azure;

/// <summary>
/// Takes care of retriving a token via ConfidentialClientApplicationBuilder
/// which allows us to inject our CDP_HTTPS_PROXY based http client.
///
/// It's unclear why this isn't available out of the box!
/// - IMsalHttpClientFactory isn't used by ClientSecretCredential
/// - The ClientSecretCredential has an internal constructor accepting MsalConfidentialClient but nothing seems to use it
/// - MsalConfidentialClient is itself internal
/// </summary>
/// <param name="token"></param>
/// <param name="expiresOn"></param>
public class ConfidentialClientApplicationTokenCredential : TokenCredential
{
private readonly string[] _scopes = { "https://storage.azure.com/.default" };

private readonly IConfidentialClientApplication _app;
public ConfidentialClientApplicationTokenCredential(IServiceProvider serviceProvider, IAzureConfig config)
{
var httpClientFactory = serviceProvider.GetRequiredService<MsalHttpClientFactoryAdapter>();

_app = ConfidentialClientApplicationBuilder.Create(config.AzureClientId)
.WithHttpClientFactory(httpClientFactory)
.WithTenantId(config.AzureTenantId)
.WithClientSecret(config.AzureClientSecret)
.Build();
}
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
var authResult = _app.AcquireTokenForClient(_scopes).ExecuteAsync(cancellationToken).Result;
return ValueTask.FromResult(new AccessToken(authResult.AccessToken, authResult.ExpiresOn));
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
var authResult = _app.AcquireTokenForClient(_scopes).ExecuteAsync(cancellationToken).Result;
return new AccessToken(authResult.AccessToken, authResult.ExpiresOn);
}
}
45 changes: 45 additions & 0 deletions Btms.Azure/Extensions/IServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

using System.Net.Http.Headers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;

namespace Btms.Azure.Extensions;

/// <summary>
/// The Azure SDK doesn't use the CDP proxied Http Client by default, so previously we used the HTTPS_CLIENT env var to
/// send the requests via CDPs squid proxy. This code is intended to use the http client we already setup that uses the proxy
/// when the CDP_HTTPS_PROXY env var is set.
/// Code borrowed from https://anthonysimmon.com/overriding-msal-httpclient-with-ihttpclientfactory/
/// </summary>
/// <param name="httpClientFactory"></param>
public class MsalHttpClientFactoryAdapter(IHttpClientFactory httpClientFactory) : IMsalHttpClientFactory
{
public HttpClient GetHttpClient()
{
return httpClientFactory.CreateClient("Msal");
}
}

public static class IServiceExtensions
{
public static void AddMsalHttpProxyClient(this IServiceCollection services, Func<IServiceProvider, HttpClientHandler> configurePrimaryHttpMessageHandler)
{
// Dependency injection registration
services.AddHttpClient("Msal")
.ConfigurePrimaryHttpMessageHandler(configurePrimaryHttpMessageHandler)
.ConfigureHttpClient(httpClient =>
{
// Default MSAL settings:
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.61.3/src/client/Microsoft.Identity.Client/Http/HttpClientConfig.cs#L18-L20
httpClient.MaxResponseContentBufferSize = 1024 * 1024;
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});

services.AddSingleton<MsalHttpClientFactoryAdapter>();

// services.AddSingleton(sp => new MsalCl
}
}
3 changes: 2 additions & 1 deletion Btms.Backend/Config/ApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class ApiOptions
// This is used by the azure library when connecting to auth related services
// when connecting to blob storage
[ConfigurationKeyName("HTTPS_PROXY")]
public string? HttpsProxy => CdpHttpsProxy?.Contains("://") == true ? CdpHttpsProxy[(CdpHttpsProxy.IndexOf("://", StringComparison.Ordinal) + 3)..] : null;
public string? HttpsProxy { get; set; }
// public string? HttpsProxy => CdpHttpsProxy?.Contains("://") == true ? CdpHttpsProxy[(CdpHttpsProxy.IndexOf("://", StringComparison.Ordinal) + 3)..] : null;

public Dictionary<string, string?> Credentials { get; set; } = [];

Expand Down
4 changes: 4 additions & 0 deletions Btms.Backend/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
using System.Reflection;
using System.Security.Claims;
using System.Text.Json.Serialization;
using Btms.Azure.Extensions;
using Environment = System.Environment;

using OpenTelemetry.Extensions.Hosting;
Expand Down Expand Up @@ -101,6 +102,9 @@ static void ConfigureWebApplication(WebApplicationBuilder builder)
// calls outside the platform should be done using the named 'proxy' http client.
builder.Services.AddHttpProxyClient();

// The azure client has it's own way of proxying :|
builder.Services.AddMsalHttpProxyClient(Proxy.ConfigurePrimaryHttpMessageHandler);

builder.Services.AddValidatorsFromAssemblyContaining<Program>();

// This uses grafana for metrics and tracing and works with the local docker compose setup as well as in CDP
Expand Down
19 changes: 13 additions & 6 deletions Btms.Backend/Utils/Http/Proxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ public static class Proxy
public static void AddHttpProxyClient(this IServiceCollection services)
{
// Some .net connections use this http client
services.AddHttpClient(ProxyClient).ConfigurePrimaryHttpMessageHandler(sp =>
{
var options = sp.GetRequiredService<IOptions<ApiOptions>>();
var proxy = sp.GetRequiredService<IWebProxy>();
return CreateHttpClientHandler(proxy, options.Value.CdpHttpsProxy!);
});
services.AddHttpClient(ProxyClient).ConfigurePrimaryHttpMessageHandler(ConfigurePrimaryHttpMessageHandler);
// {
// var options = sp.GetRequiredService<IOptions<ApiOptions>>();
// var proxy = sp.GetRequiredService<IWebProxy>();
// return CreateHttpClientHandler(proxy, options.Value.CdpHttpsProxy!);
// });

// Others, including the Azure SDK, rely on this, falling back to HTTPS_PROXY.
// HTTPS_PROXY if used must not include the protocol
Expand All @@ -39,6 +39,13 @@ public static void AddHttpProxyClient(this IServiceCollection services)
});
}

public static HttpClientHandler ConfigurePrimaryHttpMessageHandler(IServiceProvider sp)
{
var options = sp.GetRequiredService<IOptions<ApiOptions>>();
var proxy = sp.GetRequiredService<IWebProxy>();
return CreateHttpClientHandler(proxy, options.Value.CdpHttpsProxy!);
}

public static HttpClientHandler CreateHttpClientHandler(IWebProxy proxy, string proxyUri)
{
return new HttpClientHandler { Proxy = proxy, UseProxy = proxyUri != null };
Expand Down
3 changes: 2 additions & 1 deletion Btms.BlobService/BlobService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
namespace Btms.BlobService;

public class BlobService(
IServiceProvider serviceProvider,
IBlobServiceClientFactory blobServiceClientFactory,
ILogger<BlobService> logger,
IOptions<BlobServiceOptions> options,
IHttpClientFactory clientFactory)
: AzureService(logger, options.Value, clientFactory), IBlobService
: AzureService(serviceProvider, logger, options.Value, clientFactory), IBlobService
{
private BlobContainerClient CreateBlobClient(int timeout = default, int retries = default)
{
Expand Down
3 changes: 2 additions & 1 deletion Btms.BlobService/BlobServiceClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
namespace Btms.BlobService;

public class BlobServiceClientFactory(
IServiceProvider serviceProvider,
IOptions<BlobServiceOptions> options,
ILogger<BlobServiceClientFactory> logger,
IHttpClientFactory? clientFactory = null)
: AzureService(logger, options.Value, clientFactory), IBlobServiceClientFactory
: AzureService(serviceProvider, logger, options.Value, clientFactory), IBlobServiceClientFactory
{
public BlobServiceClient CreateBlobServiceClient(int timeout = default, int retries = default)
{
Expand Down
Loading