Skip to content

Commit

Permalink
Add call credential methods to client factory and flag to always use …
Browse files Browse the repository at this point in the history
…call credentials (#1705)

* Add auth interceptor helpers to client factory and flag to always use

* Clean up

* Clean up

* Refactor to use context

* Clean up
  • Loading branch information
JamesNK authored Apr 28, 2022
1 parent a2c907f commit 7d013e0
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public sealed class GrpcChannel : ChannelBase, IDisposable
internal ILoggerFactory LoggerFactory { get; }
internal ILogger Logger { get; }
internal bool ThrowOperationCanceledOnCancellation { get; }
internal bool UnsafeUseInsecureChannelCallCredentials { get; }
internal bool IsSecure => _isSecure;
internal List<CallCredentials>? CallCredentials => _callCredentials;
internal Dictionary<string, ICompressionProvider> CompressionProviders { get; }
Expand Down Expand Up @@ -159,6 +160,7 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
MessageAcceptEncoding = GrpcProtocolHelpers.GetMessageAcceptEncoding(CompressionProviders);
Logger = LoggerFactory.CreateLogger<GrpcChannel>();
ThrowOperationCanceledOnCancellation = channelOptions.ThrowOperationCanceledOnCancellation;
UnsafeUseInsecureChannelCallCredentials = channelOptions.UnsafeUseInsecureChannelCallCredentials;
_createMethodInfoFunc = CreateMethodInfo;
ActiveCalls = new HashSet<IDisposable>();
if (channelOptions.ServiceConfig is { } serviceConfig)
Expand Down
22 changes: 20 additions & 2 deletions src/Grpc.Net.Client/GrpcChannelOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ public sealed class GrpcChannelOptions
#endif

/// <summary>
/// Gets or sets the credentials for the channel. This setting is used to set <see cref="CallCredentials"/> for
/// Gets or sets the credentials for the channel. This setting is used to set <see cref="ChannelCredentials"/> for
/// a channel. Connection transport layer security (TLS) is determined by the address used to create the channel.
/// </summary>
/// <remarks>
/// <para>
/// The channel credentials you use must match the address TLS setting. Use <see cref="ChannelCredentials.Insecure"/>
/// for an "http" address and <see cref="SslCredentials"/> with no arguments for "https".
/// for an "http" address and <see cref="ChannelCredentials.SecureSsl"/> for "https".
/// </para>
/// <para>
/// The underlying <see cref="System.Net.Http.HttpClient"/> used by the channel automatically loads root certificates
Expand Down Expand Up @@ -183,6 +183,24 @@ public sealed class GrpcChannelOptions
/// </summary>
public bool ThrowOperationCanceledOnCancellation { get; set; }

/// <summary>
/// Gets or sets a value indicating whether a gRPC call's <see cref="CallCredentials"/> are used by an insecure channel.
/// The default value is <c>false</c>.
/// <para>
/// Note: Experimental API that can change or be removed without any prior notice.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// The default value for this property is <c>false</c>, which causes an insecure channel to ignore a gRPC call's <see cref="CallCredentials"/>.
/// Sending authentication headers over an insecure connection has security implications and shouldn't be done in production environments.
/// </para>
/// <para>
/// If this property is set to <c>true</c>, call credentials are always used by a channel.
/// </para>
/// </remarks>
public bool UnsafeUseInsecureChannelCallCredentials { get; set; }

/// <summary>
/// Gets or sets the service config for a gRPC channel. A service config allows service owners to publish parameters
/// to be automatically used by all clients of their service. A service config can also be specified by a client
Expand Down
2 changes: 1 addition & 1 deletion src/Grpc.Net.Client/Internal/GrpcCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@ private async Task ReadCredentials(HttpRequestMessage request)
// In C-Core the call credential auth metadata is only applied if the channel is secure
// The equivalent in grpc-dotnet is only applying metadata if HttpClient is using TLS
// HttpClient scheme will be HTTP if it is using H2C (HTTP2 without TLS)
if (Channel.IsSecure)
if (Channel.IsSecure || Channel.UnsafeUseInsecureChannelCallCredentials)
{
var configurator = new DefaultCallCredentialsConfigurator();

Expand Down
44 changes: 44 additions & 0 deletions src/Grpc.Net.ClientFactory/CallOptionsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using Grpc.Core;

namespace Grpc.Net.ClientFactory
{
/// <summary>
/// Context used to update <see cref="Grpc.Core.CallOptions"/> for a gRPC call.
/// </summary>
public sealed class CallOptionsContext
{
internal CallOptionsContext(CallOptions callOptions, IServiceProvider serviceProvider)
{
CallOptions = callOptions;
ServiceProvider = serviceProvider;
}

/// <summary>
/// Gets or sets the call options.
/// </summary>
public CallOptions CallOptions { get; set; }

/// <summary>
/// Gets the service provider.
/// </summary>
public IServiceProvider ServiceProvider { get; }
}
}
5 changes: 5 additions & 0 deletions src/Grpc.Net.ClientFactory/GrpcClientFactoryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public class GrpcClientFactoryOptions
/// </summary>
public IList<Action<GrpcChannelOptions>> ChannelOptionsActions { get; } = new List<Action<GrpcChannelOptions>>();

/// <summary>
/// Gets a list of operations used to configure a <see cref="CallOptions"/>.
/// </summary>
public IList<Action<CallOptionsContext>> CallOptionsActions { get; } = new List<Action<CallOptionsContext>>();

/// <summary>
/// Gets a list of <see cref="Interceptor"/> instances used to configure a gRPC client pipeline.
/// </summary>
Expand Down
76 changes: 76 additions & 0 deletions src/Grpc.Net.ClientFactory/GrpcHttpClientBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,82 @@ public static IHttpClientBuilder AddInterceptor(this IHttpClientBuilder builder,
return builder;
}

/// <summary>
/// Adds delegate that will be used to create <see cref="CallCredentials"/> for a gRPC call.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="authInterceptor">A delegate that is used to create <see cref="CallCredentials"/> for a gRPC call.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddCallCredentials(this IHttpClientBuilder builder, Func<AuthInterceptorContext, Metadata, Task> authInterceptor)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (authInterceptor == null)
{
throw new ArgumentNullException(nameof(authInterceptor));
}

ValidateGrpcClient(builder);

builder.Services.Configure<GrpcClientFactoryOptions>(builder.Name, options =>
{
options.CallOptionsActions.Add((callOptionsContext) =>
{
var credentials = CallCredentials.FromInterceptor((context, metadata) => authInterceptor(context, metadata));

callOptionsContext.CallOptions = ResolveCallOptionsCredentials(callOptionsContext.CallOptions, credentials);
});
});

return builder;
}

/// <summary>
/// Adds delegate that will be used to create <see cref="CallCredentials"/> for a gRPC call.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="authInterceptor">A delegate that is used to create <see cref="CallCredentials"/> for a gRPC call.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddCallCredentials(this IHttpClientBuilder builder, Func<AuthInterceptorContext, Metadata, IServiceProvider, Task> authInterceptor)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (authInterceptor == null)
{
throw new ArgumentNullException(nameof(authInterceptor));
}

ValidateGrpcClient(builder);

builder.Services.Configure<GrpcClientFactoryOptions>(builder.Name, options =>
{
options.CallOptionsActions.Add((callOptionsContext) =>
{
var credentials = CallCredentials.FromInterceptor((context, metadata) => authInterceptor(context, metadata, callOptionsContext.ServiceProvider));

callOptionsContext.CallOptions = ResolveCallOptionsCredentials(callOptionsContext.CallOptions, credentials);
});
});

return builder;
}

private static CallOptions ResolveCallOptionsCredentials(CallOptions callOptions, CallCredentials credentials)
{
if (callOptions.Credentials != null)
{
credentials = CallCredentials.Compose(callOptions.Credentials, credentials);
}

return callOptions.WithCredentials(credentials);
}

/// <summary>
/// Adds a delegate that will be used to create an additional inteceptor for a gRPC client.
/// The interceptor scope is <see cref="InterceptorScope.Channel"/>.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#endregion

using Grpc.Core;

namespace Grpc.Net.ClientFactory.Internal
{
internal sealed class CallOptionsConfigurationInvoker : CallInvoker
{
private readonly IServiceProvider _serviceProvider;
private readonly IList<Action<CallOptionsContext>> _callOptionsActions;
private readonly CallInvoker _innerInvoker;

public CallOptionsConfigurationInvoker(CallInvoker innerInvoker, IList<Action<CallOptionsContext>> callOptionsActions, IServiceProvider serviceProvider)
{
_innerInvoker = innerInvoker;
_callOptionsActions = callOptionsActions;
_serviceProvider = serviceProvider;
}

private CallOptions ResolveCallOptions(CallOptions callOptions)
{
var context = new CallOptionsContext(callOptions, _serviceProvider);

for (var i = 0; i < _callOptionsActions.Count; i++)
{
_callOptionsActions[i](context);
}

return context.CallOptions;
}

public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options)
{
return _innerInvoker.AsyncClientStreamingCall(method, host, ResolveCallOptions(options));
}

public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options)
{
return _innerInvoker.AsyncDuplexStreamingCall(method, host, ResolveCallOptions(options));
}

public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options, TRequest request)
{
return _innerInvoker.AsyncServerStreamingCall(method, host, ResolveCallOptions(options), request);
}

public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options, TRequest request)
{
return _innerInvoker.AsyncUnaryCall(method, host, ResolveCallOptions(options), request);
}

public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options, TRequest request)
{
return _innerInvoker.BlockingUnaryCall(method, host, ResolveCallOptions(options), request);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#endregion

using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -62,6 +63,11 @@ public override TClient CreateClient<TClient>(string name) where TClient : class
}
#pragma warning restore CS0618 // Type or member is obsolete

if (clientFactoryOptions.CallOptionsActions.Count != 0)
{
resolvedCallInvoker = new CallOptionsConfigurationInvoker(resolvedCallInvoker, clientFactoryOptions.CallOptionsActions, _serviceProvider);
}

if (clientFactoryOptions.Creator != null)
{
var c = clientFactoryOptions.Creator(resolvedCallInvoker);
Expand Down
31 changes: 31 additions & 0 deletions test/Grpc.Net.Client.Tests/CallCredentialTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,37 @@ public async Task CallCredentialsWithHttp_NoMetadataOnRequest()
Assert.AreEqual("The configured CallCredentials were not used because the call does not use TLS.", log.State.ToString());
}

[Test]
public async Task CallCredentialsWithHttp_UnsafeUseInsecureChannelCallCredentials_MetadataOnRequest()
{
// Arrange
string? authorizationValue = null;
var httpClient = ClientTestHelpers.CreateTestClient(async request =>
{
authorizationValue = request.Headers.GetValues("authorization").Single();

var reply = new HelloReply { Message = "Hello world" };
var streamContent = await ClientTestHelpers.CreateResponseContent(reply).DefaultTimeout();
return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
}, new Uri("http://localhost"));
var invoker = HttpClientCallInvokerFactory.Create(
httpClient,
configure: o => o.UnsafeUseInsecureChannelCallCredentials = true);

// Act
var callCredentials = CallCredentials.FromInterceptor(async (context, metadata) =>
{
// The operation is asynchronous to ensure delegate is awaited
await Task.Delay(50);
metadata.Add("authorization", "SECRET_TOKEN");
});
var call = invoker.AsyncUnaryCall<HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, new CallOptions(credentials: callCredentials), new HelloRequest());
await call.ResponseAsync.DefaultTimeout();

// Assert
Assert.AreEqual("SECRET_TOKEN", authorizationValue);
}

[Test]
public async Task CompositeCallCredentialsWithHttps_MetadataOnRequest()
{
Expand Down
Loading

0 comments on commit 7d013e0

Please sign in to comment.