Skip to content

Commit 7d013e0

Browse files
authored
Add call credential methods to client factory and flag to always use 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
1 parent a2c907f commit 7d013e0

File tree

10 files changed

+390
-3
lines changed

10 files changed

+390
-3
lines changed

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public sealed class GrpcChannel : ChannelBase, IDisposable
7070
internal ILoggerFactory LoggerFactory { get; }
7171
internal ILogger Logger { get; }
7272
internal bool ThrowOperationCanceledOnCancellation { get; }
73+
internal bool UnsafeUseInsecureChannelCallCredentials { get; }
7374
internal bool IsSecure => _isSecure;
7475
internal List<CallCredentials>? CallCredentials => _callCredentials;
7576
internal Dictionary<string, ICompressionProvider> CompressionProviders { get; }
@@ -159,6 +160,7 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
159160
MessageAcceptEncoding = GrpcProtocolHelpers.GetMessageAcceptEncoding(CompressionProviders);
160161
Logger = LoggerFactory.CreateLogger<GrpcChannel>();
161162
ThrowOperationCanceledOnCancellation = channelOptions.ThrowOperationCanceledOnCancellation;
163+
UnsafeUseInsecureChannelCallCredentials = channelOptions.UnsafeUseInsecureChannelCallCredentials;
162164
_createMethodInfoFunc = CreateMethodInfo;
163165
ActiveCalls = new HashSet<IDisposable>();
164166
if (channelOptions.ServiceConfig is { } serviceConfig)

src/Grpc.Net.Client/GrpcChannelOptions.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ public sealed class GrpcChannelOptions
3434
#endif
3535

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

186+
/// <summary>
187+
/// Gets or sets a value indicating whether a gRPC call's <see cref="CallCredentials"/> are used by an insecure channel.
188+
/// The default value is <c>false</c>.
189+
/// <para>
190+
/// Note: Experimental API that can change or be removed without any prior notice.
191+
/// </para>
192+
/// </summary>
193+
/// <remarks>
194+
/// <para>
195+
/// The default value for this property is <c>false</c>, which causes an insecure channel to ignore a gRPC call's <see cref="CallCredentials"/>.
196+
/// Sending authentication headers over an insecure connection has security implications and shouldn't be done in production environments.
197+
/// </para>
198+
/// <para>
199+
/// If this property is set to <c>true</c>, call credentials are always used by a channel.
200+
/// </para>
201+
/// </remarks>
202+
public bool UnsafeUseInsecureChannelCallCredentials { get; set; }
203+
186204
/// <summary>
187205
/// Gets or sets the service config for a gRPC channel. A service config allows service owners to publish parameters
188206
/// to be automatically used by all clients of their service. A service config can also be specified by a client

src/Grpc.Net.Client/Internal/GrpcCall.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ private async Task ReadCredentials(HttpRequestMessage request)
886886
// In C-Core the call credential auth metadata is only applied if the channel is secure
887887
// The equivalent in grpc-dotnet is only applying metadata if HttpClient is using TLS
888888
// HttpClient scheme will be HTTP if it is using H2C (HTTP2 without TLS)
889-
if (Channel.IsSecure)
889+
if (Channel.IsSecure || Channel.UnsafeUseInsecureChannelCallCredentials)
890890
{
891891
var configurator = new DefaultCallCredentialsConfigurator();
892892

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using Grpc.Core;
20+
21+
namespace Grpc.Net.ClientFactory
22+
{
23+
/// <summary>
24+
/// Context used to update <see cref="Grpc.Core.CallOptions"/> for a gRPC call.
25+
/// </summary>
26+
public sealed class CallOptionsContext
27+
{
28+
internal CallOptionsContext(CallOptions callOptions, IServiceProvider serviceProvider)
29+
{
30+
CallOptions = callOptions;
31+
ServiceProvider = serviceProvider;
32+
}
33+
34+
/// <summary>
35+
/// Gets or sets the call options.
36+
/// </summary>
37+
public CallOptions CallOptions { get; set; }
38+
39+
/// <summary>
40+
/// Gets the service provider.
41+
/// </summary>
42+
public IServiceProvider ServiceProvider { get; }
43+
}
44+
}

src/Grpc.Net.ClientFactory/GrpcClientFactoryOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public class GrpcClientFactoryOptions
3737
/// </summary>
3838
public IList<Action<GrpcChannelOptions>> ChannelOptionsActions { get; } = new List<Action<GrpcChannelOptions>>();
3939

40+
/// <summary>
41+
/// Gets a list of operations used to configure a <see cref="CallOptions"/>.
42+
/// </summary>
43+
public IList<Action<CallOptionsContext>> CallOptionsActions { get; } = new List<Action<CallOptionsContext>>();
44+
4045
/// <summary>
4146
/// Gets a list of <see cref="Interceptor"/> instances used to configure a gRPC client pipeline.
4247
/// </summary>

src/Grpc.Net.ClientFactory/GrpcHttpClientBuilderExtensions.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,82 @@ public static IHttpClientBuilder AddInterceptor(this IHttpClientBuilder builder,
129129
return builder;
130130
}
131131

132+
/// <summary>
133+
/// Adds delegate that will be used to create <see cref="CallCredentials"/> for a gRPC call.
134+
/// </summary>
135+
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
136+
/// <param name="authInterceptor">A delegate that is used to create <see cref="CallCredentials"/> for a gRPC call.</param>
137+
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
138+
public static IHttpClientBuilder AddCallCredentials(this IHttpClientBuilder builder, Func<AuthInterceptorContext, Metadata, Task> authInterceptor)
139+
{
140+
if (builder == null)
141+
{
142+
throw new ArgumentNullException(nameof(builder));
143+
}
144+
145+
if (authInterceptor == null)
146+
{
147+
throw new ArgumentNullException(nameof(authInterceptor));
148+
}
149+
150+
ValidateGrpcClient(builder);
151+
152+
builder.Services.Configure<GrpcClientFactoryOptions>(builder.Name, options =>
153+
{
154+
options.CallOptionsActions.Add((callOptionsContext) =>
155+
{
156+
var credentials = CallCredentials.FromInterceptor((context, metadata) => authInterceptor(context, metadata));
157+
158+
callOptionsContext.CallOptions = ResolveCallOptionsCredentials(callOptionsContext.CallOptions, credentials);
159+
});
160+
});
161+
162+
return builder;
163+
}
164+
165+
/// <summary>
166+
/// Adds delegate that will be used to create <see cref="CallCredentials"/> for a gRPC call.
167+
/// </summary>
168+
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
169+
/// <param name="authInterceptor">A delegate that is used to create <see cref="CallCredentials"/> for a gRPC call.</param>
170+
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
171+
public static IHttpClientBuilder AddCallCredentials(this IHttpClientBuilder builder, Func<AuthInterceptorContext, Metadata, IServiceProvider, Task> authInterceptor)
172+
{
173+
if (builder == null)
174+
{
175+
throw new ArgumentNullException(nameof(builder));
176+
}
177+
178+
if (authInterceptor == null)
179+
{
180+
throw new ArgumentNullException(nameof(authInterceptor));
181+
}
182+
183+
ValidateGrpcClient(builder);
184+
185+
builder.Services.Configure<GrpcClientFactoryOptions>(builder.Name, options =>
186+
{
187+
options.CallOptionsActions.Add((callOptionsContext) =>
188+
{
189+
var credentials = CallCredentials.FromInterceptor((context, metadata) => authInterceptor(context, metadata, callOptionsContext.ServiceProvider));
190+
191+
callOptionsContext.CallOptions = ResolveCallOptionsCredentials(callOptionsContext.CallOptions, credentials);
192+
});
193+
});
194+
195+
return builder;
196+
}
197+
198+
private static CallOptions ResolveCallOptionsCredentials(CallOptions callOptions, CallCredentials credentials)
199+
{
200+
if (callOptions.Credentials != null)
201+
{
202+
credentials = CallCredentials.Compose(callOptions.Credentials, credentials);
203+
}
204+
205+
return callOptions.WithCredentials(credentials);
206+
}
207+
132208
/// <summary>
133209
/// Adds a delegate that will be used to create an additional inteceptor for a gRPC client.
134210
/// The interceptor scope is <see cref="InterceptorScope.Channel"/>.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
using Grpc.Core;
20+
21+
namespace Grpc.Net.ClientFactory.Internal
22+
{
23+
internal sealed class CallOptionsConfigurationInvoker : CallInvoker
24+
{
25+
private readonly IServiceProvider _serviceProvider;
26+
private readonly IList<Action<CallOptionsContext>> _callOptionsActions;
27+
private readonly CallInvoker _innerInvoker;
28+
29+
public CallOptionsConfigurationInvoker(CallInvoker innerInvoker, IList<Action<CallOptionsContext>> callOptionsActions, IServiceProvider serviceProvider)
30+
{
31+
_innerInvoker = innerInvoker;
32+
_callOptionsActions = callOptionsActions;
33+
_serviceProvider = serviceProvider;
34+
}
35+
36+
private CallOptions ResolveCallOptions(CallOptions callOptions)
37+
{
38+
var context = new CallOptionsContext(callOptions, _serviceProvider);
39+
40+
for (var i = 0; i < _callOptionsActions.Count; i++)
41+
{
42+
_callOptionsActions[i](context);
43+
}
44+
45+
return context.CallOptions;
46+
}
47+
48+
public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options)
49+
{
50+
return _innerInvoker.AsyncClientStreamingCall(method, host, ResolveCallOptions(options));
51+
}
52+
53+
public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options)
54+
{
55+
return _innerInvoker.AsyncDuplexStreamingCall(method, host, ResolveCallOptions(options));
56+
}
57+
58+
public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options, TRequest request)
59+
{
60+
return _innerInvoker.AsyncServerStreamingCall(method, host, ResolveCallOptions(options), request);
61+
}
62+
63+
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options, TRequest request)
64+
{
65+
return _innerInvoker.AsyncUnaryCall(method, host, ResolveCallOptions(options), request);
66+
}
67+
68+
public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string? host, CallOptions options, TRequest request)
69+
{
70+
return _innerInvoker.BlockingUnaryCall(method, host, ResolveCallOptions(options), request);
71+
}
72+
}
73+
}

src/Grpc.Net.ClientFactory/Internal/DefaultGrpcClientFactory.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#endregion
1818

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

66+
if (clientFactoryOptions.CallOptionsActions.Count != 0)
67+
{
68+
resolvedCallInvoker = new CallOptionsConfigurationInvoker(resolvedCallInvoker, clientFactoryOptions.CallOptionsActions, _serviceProvider);
69+
}
70+
6571
if (clientFactoryOptions.Creator != null)
6672
{
6773
var c = clientFactoryOptions.Creator(resolvedCallInvoker);

test/Grpc.Net.Client.Tests/CallCredentialTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,37 @@ public async Task CallCredentialsWithHttp_NoMetadataOnRequest()
127127
Assert.AreEqual("The configured CallCredentials were not used because the call does not use TLS.", log.State.ToString());
128128
}
129129

130+
[Test]
131+
public async Task CallCredentialsWithHttp_UnsafeUseInsecureChannelCallCredentials_MetadataOnRequest()
132+
{
133+
// Arrange
134+
string? authorizationValue = null;
135+
var httpClient = ClientTestHelpers.CreateTestClient(async request =>
136+
{
137+
authorizationValue = request.Headers.GetValues("authorization").Single();
138+
139+
var reply = new HelloReply { Message = "Hello world" };
140+
var streamContent = await ClientTestHelpers.CreateResponseContent(reply).DefaultTimeout();
141+
return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
142+
}, new Uri("http://localhost"));
143+
var invoker = HttpClientCallInvokerFactory.Create(
144+
httpClient,
145+
configure: o => o.UnsafeUseInsecureChannelCallCredentials = true);
146+
147+
// Act
148+
var callCredentials = CallCredentials.FromInterceptor(async (context, metadata) =>
149+
{
150+
// The operation is asynchronous to ensure delegate is awaited
151+
await Task.Delay(50);
152+
metadata.Add("authorization", "SECRET_TOKEN");
153+
});
154+
var call = invoker.AsyncUnaryCall<HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, new CallOptions(credentials: callCredentials), new HelloRequest());
155+
await call.ResponseAsync.DefaultTimeout();
156+
157+
// Assert
158+
Assert.AreEqual("SECRET_TOKEN", authorizationValue);
159+
}
160+
130161
[Test]
131162
public async Task CompositeCallCredentialsWithHttps_MetadataOnRequest()
132163
{

0 commit comments

Comments
 (0)