Skip to content

Commit fd7cc90

Browse files
kmjjcCJ HillbrandKrystal Culp
authored
Allow Virtual Client to use a certificate to authenticate to the proxy API (#554)
Add certificate issuer and subject parameters to proxy API URI for authentication --------- Co-authored-by: CJ Hillbrand <[email protected]> Co-authored-by: Krystal Culp <[email protected]>
1 parent d4f99e4 commit fd7cc90

File tree

11 files changed

+130
-41
lines changed

11 files changed

+130
-41
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.1.9
1+
2.1.10

src/VirtualClient/VirtualClient.Common/Rest/IRestClientBuilder.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace VirtualClient.Common.Rest
55
{
66
using System;
7+
using System.Security.Cryptography.X509Certificates;
78

89
/// <summary>
910
/// Interface for generic rest client builder.
@@ -30,9 +31,15 @@ public interface IRestClientBuilder : IDisposable
3031
/// <returns>The builder itself</returns>
3132
IRestClientBuilder AddAcceptedMediaType(MediaType mediaType);
3233

34+
/// <summary>
35+
/// Add client certificate.
36+
/// </summary>
37+
/// <param name="certificate">The certificate to add.</param>
38+
/// <returns>Builder itself.</returns>
39+
IRestClientBuilder AddCertificate(X509Certificate2 certificate);
40+
3341
/// <summary>
3442
/// Always trust the server certificate.
35-
/// Note that this method override the underlying httpclient and previous builder methods, so this builder methods should be used first.
3643
/// </summary>
3744
/// <returns>Builder itself.</returns>
3845
IRestClientBuilder AlwaysTrustServerCertificate();

src/VirtualClient/VirtualClient.Common/Rest/RestClientBuilder.cs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,65 @@ namespace VirtualClient.Common.Rest
77
using System.Collections.Generic;
88
using System.Net.Http;
99
using System.Net.Http.Headers;
10+
using System.Security.Cryptography.X509Certificates;
1011
using VirtualClient.Common.Extensions;
1112

1213
/// <summary>
1314
/// Builder for generic rest client
1415
/// </summary>
1516
public class RestClientBuilder : IRestClientBuilder
1617
{
17-
private RestClient restClient;
1818
private TimeSpan? httpTimeout;
1919
private bool disposed = false;
2020

21+
private List<MediaTypeWithQualityHeaderValue> acceptedMediaTypes;
22+
private AuthenticationHeaderValue authenticationHeader;
23+
24+
#pragma warning disable CA2213 // We will reuse these single objects for the lifetime of virtual client execution
25+
private HttpClientHandler handler;
26+
#pragma warning restore CA2213 // Disposable fields should be disposed
27+
2128
/// <summary>
2229
/// Constructor for the rest client builder
2330
/// </summary>
2431
/// <param name="timeout">The HTTP timeout to apply.</param>
2532
public RestClientBuilder(TimeSpan? timeout = null)
2633
{
27-
this.restClient = new RestClient();
2834
this.httpTimeout = timeout;
35+
this.handler = new HttpClientHandler();
36+
this.acceptedMediaTypes = new List<MediaTypeWithQualityHeaderValue>();
2937
}
3038

3139
/// <inheritdoc/>
3240
public IRestClientBuilder AddAuthorizationHeader(string authToken, string headerName = "Bearer")
3341
{
34-
this.restClient.SetAuthorizationHeader(new AuthenticationHeaderValue(headerName, authToken));
42+
this.authenticationHeader = new AuthenticationHeaderValue(authToken, headerName);
3543
return this;
3644
}
3745

3846
/// <inheritdoc/>
3947
public IRestClientBuilder AddAcceptedMediaType(MediaType mediaType)
4048
{
4149
mediaType.ThrowIfNull(nameof(mediaType));
42-
this.restClient.AddAcceptedMediaTypeHeader(new MediaTypeWithQualityHeaderValue(mediaType.FieldName));
50+
this.acceptedMediaTypes.Add(new MediaTypeWithQualityHeaderValue(mediaType.FieldName));
4351
return this;
4452
}
4553

4654
/// <inheritdoc/>
4755
public IRestClientBuilder AlwaysTrustServerCertificate()
4856
{
49-
HttpClientHandler handler = new HttpClientHandler();
50-
handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) =>
57+
this.handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) =>
5158
{
5259
return true;
5360
};
5461

55-
HttpClient client = new HttpClient(handler);
56-
this.restClient = new RestClient(client);
62+
return this;
63+
}
64+
65+
/// <inheritdoc/>
66+
public IRestClientBuilder AddCertificate(X509Certificate2 certificate)
67+
{
68+
this.handler.ClientCertificates.Add(certificate);
5769
return this;
5870
}
5971

@@ -63,15 +75,28 @@ public IRestClientBuilder AlwaysTrustServerCertificate()
6375
/// <returns>The built rest client.</returns>
6476
public IRestClient Build()
6577
{
66-
RestClient output = this.restClient;
67-
this.restClient = new RestClient();
78+
HttpClient client = new HttpClient(this.handler);
79+
RestClient restClient = new RestClient(client);
80+
81+
if (this.authenticationHeader != null)
82+
{
83+
restClient.SetAuthorizationHeader(this.authenticationHeader);
84+
}
85+
86+
if (this.acceptedMediaTypes.Count > 0)
87+
{
88+
foreach (var mediaType in this.acceptedMediaTypes)
89+
{
90+
restClient.AddAcceptedMediaTypeHeader(mediaType);
91+
}
92+
}
6893

6994
if (this.httpTimeout != null)
7095
{
71-
output.Client.Timeout = this.httpTimeout.Value;
96+
restClient.Client.Timeout = this.httpTimeout.Value;
7297
}
7398

74-
return output;
99+
return restClient;
75100
}
76101

77102
/// <inheritdoc/>
@@ -88,7 +113,6 @@ protected virtual void Dispose(bool disposing)
88113
{
89114
if (disposing)
90115
{
91-
this.restClient.Dispose();
92116
}
93117

94118
this.disposed = true;

src/VirtualClient/VirtualClient.Contracts/Proxy/VirtualClientProxyApiClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace VirtualClient.Contracts.Proxy
1010
using System.Net;
1111
using System.Net.Http;
1212
using System.Net.Http.Headers;
13+
using System.Security.Cryptography.X509Certificates;
1314
using System.Text;
1415
using System.Text.RegularExpressions;
1516
using System.Threading;

src/VirtualClient/VirtualClient.Core/ApiClientManager.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace VirtualClient
77
using System.Collections.Generic;
88
using System.Linq;
99
using System.Net;
10+
using System.Security.Cryptography.X509Certificates;
1011
using VirtualClient.Common.Extensions;
1112
using VirtualClient.Contracts;
1213
using VirtualClient.Contracts.Proxy;
@@ -244,11 +245,12 @@ public IApiClient GetOrCreateApiClient(string id, Uri uri)
244245
/// </summary>
245246
/// <param name="id">The ID of the proxy API client to use for lookups.</param>
246247
/// <param name="uri">The URI of the target Virtual Client API service including the port (e.g. http://any.server.uri:4500).</param>
248+
/// <param name="certificate">The certificate to authenticate to the proxy API</param>
247249
/// <returns>
248250
/// An <see cref="IProxyApiClient"/> that can be used to interface with a target
249251
/// Virtual Client API service.
250252
/// </returns>
251-
public IProxyApiClient GetOrCreateProxyApiClient(string id, Uri uri)
253+
public IProxyApiClient GetOrCreateProxyApiClient(string id, Uri uri, X509Certificate2 certificate = null)
252254
{
253255
IProxyApiClient apiClient = null;
254256

@@ -257,7 +259,7 @@ public IProxyApiClient GetOrCreateProxyApiClient(string id, Uri uri)
257259
apiClient = this.GetProxyApiClient(id);
258260
if (apiClient == null)
259261
{
260-
apiClient = DependencyFactory.CreateVirtualClientProxyApiClient(uri);
262+
apiClient = DependencyFactory.CreateVirtualClientProxyApiClient(uri, certificate: certificate);
261263
this.proxyApiClients[id] = apiClient;
262264
}
263265
}

src/VirtualClient/VirtualClient.Core/DependencyFactory.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace VirtualClient
99
using System.IO.Abstractions;
1010
using System.Linq;
1111
using System.Net;
12+
using System.Security.Cryptography.X509Certificates;
1213
using System.Threading.Tasks;
1314
using Azure.Core;
1415
using Azure.Messaging.EventHubs.Producer;
@@ -412,11 +413,12 @@ public static IFirewallManager CreateFirewallManager(PlatformID platform, Proces
412413
/// <param name="storeDescription">Describes the type of blob store (e.g. Content, Packages).</param>
413414
/// <param name="source">An explicit source to use for blob uploads/downloads through the proxy API.</param>
414415
/// <param name="logger">A logger to use for capturing information related to blob upload/download operations.</param>
415-
public static IBlobManager CreateProxyBlobManager(DependencyProxyStore storeDescription, string source = null, Microsoft.Extensions.Logging.ILogger logger = null)
416+
/// <param name="certificate">The certificate to authenticate to the proxy API</param>
417+
public static IBlobManager CreateProxyBlobManager(DependencyProxyStore storeDescription, string source = null, Microsoft.Extensions.Logging.ILogger logger = null, X509Certificate2 certificate = null)
416418
{
417419
storeDescription.ThrowIfNull(nameof(storeDescription));
418420

419-
VirtualClientProxyApiClient proxyApiClient = DependencyFactory.CreateVirtualClientProxyApiClient(storeDescription.ProxyApiUri, TimeSpan.FromHours(6));
421+
VirtualClientProxyApiClient proxyApiClient = DependencyFactory.CreateVirtualClientProxyApiClient(storeDescription.ProxyApiUri, TimeSpan.FromHours(6), certificate);
420422
ProxyBlobManager blobManager = new ProxyBlobManager(storeDescription, proxyApiClient, source);
421423

422424
if (logger != null)
@@ -675,16 +677,29 @@ public static VirtualClientApiClient CreateVirtualClientApiClient(IPAddress ipAd
675677
/// </summary>
676678
/// <param name="proxyApiUri">The URI for the proxy API/service including its port (e.g. http://any.uri:5000).</param>
677679
/// <param name="timeout">A timeout to use for the underlying HTTP client.</param>
678-
public static VirtualClientProxyApiClient CreateVirtualClientProxyApiClient(Uri proxyApiUri, TimeSpan? timeout = null)
680+
/// <param name="certificate">The certificate to authenticate to the proxy API</param>
681+
public static VirtualClientProxyApiClient CreateVirtualClientProxyApiClient(Uri proxyApiUri, TimeSpan? timeout = null, X509Certificate2 certificate = null)
679682
{
680683
proxyApiUri.ThrowIfNull(nameof(proxyApiUri));
681684

682-
IRestClient restClient = new RestClientBuilder(timeout)
685+
if (!string.IsNullOrWhiteSpace(proxyApiUri.Query))
686+
{
687+
// e.g.
688+
// https://any.service.azure.com/?miid=307591a4-abb2-4559-af59-b47177d140cf -> https://any.service.azure.com/
689+
690+
proxyApiUri = new Uri(proxyApiUri.OriginalString.Substring(0, proxyApiUri.OriginalString.IndexOf("?")));
691+
}
692+
693+
IRestClientBuilder builder = new RestClientBuilder(timeout)
683694
.AlwaysTrustServerCertificate()
684-
.AddAcceptedMediaType(MediaType.Json)
685-
.Build();
695+
.AddAcceptedMediaType(MediaType.Json);
686696

687-
return new VirtualClientProxyApiClient(restClient, proxyApiUri);
697+
if (certificate != null)
698+
{
699+
builder.AddCertificate(certificate);
700+
}
701+
702+
return new VirtualClientProxyApiClient(builder.Build(), proxyApiUri);
688703
}
689704

690705
/// <summary>
@@ -696,7 +711,7 @@ public static VirtualClientProxyApiClient CreateVirtualClientProxyApiClient(Uri
696711
public static void FlushTelemetry(TimeSpan? timeout = null)
697712
{
698713
Parallel.ForEach(DependencyFactory.telemetryChannels, channel => channel.Flush(timeout));
699-
}
714+
}
700715

701716
/// <summary>
702717
/// Applies a filter to the logger generated by the provider that will handle the logging

src/VirtualClient/VirtualClient.Core/EndpointUtility.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,27 @@ public static bool IsPackageUri(Uri endpointUri, string storeName)
377377
return storeName == DependencyStore.Packages && packageUri;
378378
}
379379

380+
/// <summary>
381+
/// Parses the subject name and issuer from the provided uri. If the uri does not contain the correctly formatted certificate subject name
382+
/// and issuer information the method will return false, and keep the two out parameters as null.
383+
/// Ex. https://vegaprod01proxyapi.azurewebsites.net?crti=issuerName&amp;crts=certSubject
384+
/// </summary>
385+
/// <param name="uri">The uri to attempt to parse the values from.</param>
386+
/// <param name="issuer">The issuer of the certificate.</param>
387+
/// <param name="subject">The subject of the certificate.</param>
388+
/// <returns>True/False if the method was able to successfully parse both the subject name and the issuer of the certificate.</returns>
389+
public static bool TryParseCertificateReference(Uri uri, out string issuer, out string subject)
390+
{
391+
string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");
392+
393+
IDictionary<string, string> queryParameters = TextParsingExtensions.ParseDelimitedValues(queryString)?.ToDictionary(
394+
entry => entry.Key,
395+
entry => entry.Value?.ToString(),
396+
StringComparer.OrdinalIgnoreCase);
397+
398+
return TryGetCertificateReferenceForUri(queryParameters, out issuer, out subject);
399+
}
400+
380401
/// <summary>
381402
/// Returns the endpoint by verifying package uri checks.
382403
/// if the endpoint is a package uri without http or https protocols then append the protocol else return the endpoint value.
@@ -430,7 +451,7 @@ private static DependencyBlobStore CreateBlobStoreReference(string storeName, Ur
430451
// Basic URI without any query parameters
431452
// 1) If the given endpoint uri is a package uri (e.g. https://packages.virtualclient.microsoft.com ) then the package is retrieved from storage via CDN
432453
// 2) If the given endpoint uri is a blob storage (e.g https://any.blob.core.windows.net) then the packages is retrieved from blob storage
433-
store = IsPackageUri(endpointUri, storeName)
454+
store = IsPackageUri(endpointUri, storeName)
434455
? new DependencyBlobStore(storeName, endpointUri, DependencyStore.StoreTypeAzureCDN)
435456
: new DependencyBlobStore(storeName, endpointUri);
436457
}
@@ -947,9 +968,9 @@ private static async Task<TokenCredential> CreateIdentityTokenCredentialAsync(IC
947968
else
948969
{
949970
certificate = await certificateManager.GetCertificateFromStoreAsync(
950-
certificateIssuer,
951-
certificateSubject,
952-
storeLocations,
971+
certificateIssuer,
972+
certificateSubject,
973+
storeLocations,
953974
storeName);
954975
}
955976

src/VirtualClient/VirtualClient.Core/IApiClientManager.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace VirtualClient
66
using System;
77
using System.Collections.Generic;
88
using System.Net;
9-
using System.Threading.Tasks;
9+
using System.Security.Cryptography.X509Certificates;
1010
using VirtualClient.Contracts;
1111
using VirtualClient.Contracts.Proxy;
1212

@@ -104,11 +104,12 @@ public interface IApiClientManager
104104
/// </summary>
105105
/// <param name="id">The ID of the proxy API client to use for lookups.</param>
106106
/// <param name="uri">The URI of the target Virtual Client API service including the port (e.g. http://any.server.uri:4500).</param>
107+
/// <param name="certificate">The certificate to authenticate to the proxy API</param>
107108
/// <returns>
108109
/// An <see cref="IProxyApiClient"/> that can be used to interface with a target
109110
/// Virtual Client proxy API service.
110111
/// </returns>
111-
IProxyApiClient GetOrCreateProxyApiClient(string id, Uri uri);
112+
IProxyApiClient GetOrCreateProxyApiClient(string id, Uri uri, X509Certificate2 certificate);
112113

113114
/// <summary>
114115
/// Creates new API clients from the ones that are already cached.

0 commit comments

Comments
 (0)