Skip to content

Commit 1e9bb3b

Browse files
committed
Add docs for new gRPC call credentials features
1 parent 52bc48c commit 1e9bb3b

File tree

3 files changed

+98
-64
lines changed

3 files changed

+98
-64
lines changed

aspnetcore/grpc/authn-and-authz.md

Lines changed: 75 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ public bool DoAuthenticatedCall(
7373
}
7474
```
7575

76-
Configuring `ChannelCredentials` on a channel is an alternative way to send the token to the service with gRPC calls. A `ChannelCredentials` can include `CallCredentials`, which provide a way to automatically set `Metadata`.
76+
Configuring `ChannelCredentials` on a channel is an alternative way to send the token to the service with gRPC calls. A `ChannelCredentials` can include `CallCredentials`, which provide a way to automatically set `Metadata`. `CallCredentials` is run each time a gRPC call is made, which avoids the need to write code in multiple places to pass the token yourself.
7777

78-
`CallCredentials` is run each time a gRPC call is made, which avoids the need to write code in multiple places to pass the token yourself. Note that `CallCredentials` are only applied if the channel is secured with TLS. `CallCredentials` aren't applied on unsecured non-TLS channels.
78+
> [!NOTE]
79+
> `CallCredentials` are only applied if the channel is secured with TLS. Sending authentication headers over an insecure connection has security implications and shouldn't be done in production environments. An app can configure a channel to ignore this behavior and always use `CallCredentials` by setting `UnsafeUseInsecureChannelCallCredentials` on a channel.
7980
8081
The credential in the following example configures the channel to send the token with every gRPC call:
8182

@@ -91,8 +92,6 @@ private static GrpcChannel CreateAuthenticatedChannel(string address)
9192
return Task.CompletedTask;
9293
});
9394

94-
// SslCredentials is used here because this channel is using TLS.
95-
// CallCredentials can't be used with ChannelCredentials.Insecure on non-TLS channels.
9695
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
9796
{
9897
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
@@ -103,70 +102,76 @@ private static GrpcChannel CreateAuthenticatedChannel(string address)
103102

104103
#### Bearer token with gRPC client factory
105104

106-
gRPC client factory can create clients that send a bearer token using `ChannelCredentials`. When configuring a client, assign the `CallCredentials` the client should use with the `ConfigureChannel` method.
105+
gRPC client factory can create clients that send a bearer token using `AddCallCredentials`. The delegate passed to `AddCallCredentials` is executed for each gRPC call:
107106

108107
```csharp
109108
builder.Services
110109
.AddGrpcClient<Greeter.GreeterClient>(o =>
111110
{
112111
o.Address = new Uri("https://localhost:5001");
113112
})
114-
.ConfigureChannel(o =>
113+
.AddCallCredentials((context, metadata) =>
115114
{
116-
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
115+
if (!string.IsNullOrEmpty(_token))
117116
{
118-
if (!string.IsNullOrEmpty(_token))
119-
{
120-
metadata.Add("Authorization", $"Bearer {_token}");
121-
}
122-
return Task.CompletedTask;
123-
});
124-
125-
o.Credentials = ChannelCredentials.Create(new SslCredentials(), credentials);
117+
metadata.Add("Authorization", $"Bearer {_token}");
118+
}
119+
return Task.CompletedTask;
126120
});
127121
```
128122

129-
A gRPC interceptor can also be used to configure a bearer token. An advantage to using an interceptor is the client factory can be configured to create a new interceptor for each client. This allows an interceptor to be [constructed from DI using scoped and transient services](/dotnet/core/extensions/dependency-injection#service-lifetimes).
123+
Dependency injection (DI) can be combined with `AddCallCredentials`. An overload passes `IServiceProvider` to the delegate, which can be used to get a service [constructed from DI using scoped and transient services](/dotnet/core/extensions/dependency-injection#service-lifetimes).
130124

131125
Consider an app that has:
126+
132127
* A user-defined `ITokenProvider` for getting a bearer token. `ITokenProvider` is registered in DI with a scoped lifetime.
133128
* gRPC client factory is configured to create clients that are injected into gRPC services and Web API controllers.
134129
* gRPC calls should use `ITokenProvider` to get a bearer token.
135130

136131
```csharp
137-
public class AuthInterceptor : Interceptor
132+
public interface ITokenProvider
138133
{
139-
private readonly ITokenProvider _tokenProvider;
140-
141-
public AuthInterceptor(ITokenProvider tokenProvider)
142-
{
143-
_tokenProvider = tokenProvider;
144-
}
145-
146-
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
147-
TRequest request,
148-
ClientInterceptorContext<TRequest, TResponse> context,
149-
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
134+
Task<string> GetTokenAsync();
135+
}
136+
137+
public class AppTokenProvider : ITokenProvider
138+
{
139+
private string _token;
140+
141+
public async Task<string> GetTokenAsync()
150142
{
151-
context.Options.Metadata.Add("Authorization", $"Bearer {_tokenProvider.GetToken()}");
152-
return continuation(request, context);
143+
if (_token == null)
144+
{
145+
// App code to resolve the token here.
146+
}
147+
148+
return _token;
153149
}
154150
}
155151
```
156152

157153
```csharp
154+
builder.Services.AddScoped<ITokenProvider, AppTokenProvider>();
155+
158156
builder.Services
159157
.AddGrpcClient<Greeter.GreeterClient>(o =>
160158
{
161159
o.Address = new Uri("https://localhost:5001");
162160
})
163-
.AddInterceptor<AuthInterceptor>(InterceptorScope.Client);
161+
.AddCallCredentials(async (context, metadata, serviceProvider) =>
162+
{
163+
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
164+
var token = await provider.GetTokenAsync();
165+
metadata.Add("Authorization", $"Bearer {token}");
166+
}));
164167
```
165168

166-
The preceeding code:
167-
* Defines `AuthInterceptor` which is constructed using the user defined `ITokenProvider`.
169+
The preceding code:
170+
171+
* Defines `ITokenProvider` and `AppTokenProvider`. These types handle resolving the authentication token for gRPC calls.
172+
* Registers the `AppTokenProvider` type with DI in a scoped lifetime. `AppTokenProvider` caches the token so that only the first call in the scope is required to calculate it.
168173
* Registers the `GreeterClient` type with client factory.
169-
* Configures the `AuthInterceptor` for this client using `InterceptorScope.Client`. A new interceptor is created for each client instance. When a client is created for a gRPC service or Web API controller, the scoped `ITokenProvider` is injected into the interceptor.
174+
* Configures `AddCallCredentials` for this client. The delegate is executed each time a call is made and adds the token returned by `ITokenProvider` to the metadata.
170175

171176
### Client certificate authentication
172177

@@ -360,70 +365,76 @@ private static GrpcChannel CreateAuthenticatedChannel(string address)
360365

361366
#### Bearer token with gRPC client factory
362367

363-
gRPC client factory can create clients that send a bearer token using `ChannelCredentials`. When configuring a client, assign the `CallCredentials` the client should use with the `ConfigureChannel` method.
368+
gRPC client factory can create clients that send a bearer token using `AddCallCredentials`. The delegate passed to `AddCallCredentials` is executed for each gRPC call:
364369

365370
```csharp
366371
services
367372
.AddGrpcClient<Greeter.GreeterClient>(o =>
368373
{
369374
o.Address = new Uri("https://localhost:5001");
370375
})
371-
.ConfigureChannel(o =>
376+
.AddCallCredentials((context, metadata) =>
372377
{
373-
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
378+
if (!string.IsNullOrEmpty(_token))
374379
{
375-
if (!string.IsNullOrEmpty(_token))
376-
{
377-
metadata.Add("Authorization", $"Bearer {_token}");
378-
}
379-
return Task.CompletedTask;
380-
});
381-
382-
o.Credentials = ChannelCredentials.Create(new SslCredentials(), credentials);
380+
metadata.Add("Authorization", $"Bearer {_token}");
381+
}
382+
return Task.CompletedTask;
383383
});
384384
```
385385

386-
A gRPC interceptor can also be used to configure a bearer token. An advantage to using an interceptor is the client factory can be configured to create a new interceptor for each client. This allows an interceptor to be [constructed from DI using scoped and transient services](/dotnet/core/extensions/dependency-injection#service-lifetimes).
386+
Dependency injection (DI) can be combined with `AddCallCredentials`. An overload passes `IServiceProvider` to the delegate, which can be used to get a service [constructed from DI using scoped and transient services](/dotnet/core/extensions/dependency-injection#service-lifetimes).
387387

388388
Consider an app that has:
389+
389390
* A user-defined `ITokenProvider` for getting a bearer token. `ITokenProvider` is registered in DI with a scoped lifetime.
390391
* gRPC client factory is configured to create clients that are injected into gRPC services and Web API controllers.
391392
* gRPC calls should use `ITokenProvider` to get a bearer token.
392393

393394
```csharp
394-
public class AuthInterceptor : Interceptor
395+
public interface ITokenProvider
395396
{
396-
private readonly ITokenProvider _tokenProvider;
397-
398-
public AuthInterceptor(ITokenProvider tokenProvider)
399-
{
400-
_tokenProvider = tokenProvider;
401-
}
402-
403-
public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
404-
TRequest request,
405-
ClientInterceptorContext<TRequest, TResponse> context,
406-
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
397+
Task<string> GetTokenAsync();
398+
}
399+
400+
public class AppTokenProvider : ITokenProvider
401+
{
402+
private string _token;
403+
404+
public async Task<string> GetTokenAsync()
407405
{
408-
context.Options.Metadata.Add("Authorization", $"Bearer {_tokenProvider.GetToken()}");
409-
return continuation(request, context);
406+
if (_token == null)
407+
{
408+
// App code to resolve the token here.
409+
}
410+
411+
return _token;
410412
}
411413
}
412414
```
413415

414416
```csharp
417+
services.AddScoped<ITokenProvider, AppTokenProvider>();
418+
415419
services
416420
.AddGrpcClient<Greeter.GreeterClient>(o =>
417421
{
418422
o.Address = new Uri("https://localhost:5001");
419423
})
420-
.AddInterceptor<AuthInterceptor>(InterceptorScope.Client);
424+
.AddCallCredentials(async (context, metadata, serviceProvider) =>
425+
{
426+
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
427+
var token = await provider.GetTokenAsync();
428+
metadata.Add("Authorization", $"Bearer {token}");
429+
}));
421430
```
422431

423-
The preceeding code:
424-
* Defines `AuthInterceptor` which is constructed using the user defined `ITokenProvider`.
432+
The preceding code:
433+
434+
* Defines `ITokenProvider` and `AppTokenProvider`. These types handle resolving the authentication token for gRPC calls.
435+
* Registers the `AppTokenProvider` type with DI in a scoped lifetime. `AppTokenProvider` caches the token so that only the first call in the scope is required to calculate it.
425436
* Registers the `GreeterClient` type with client factory.
426-
* Configures the `AuthInterceptor` for this client using `InterceptorScope.Client`. A new interceptor is created for each client instance. When a client is created for a gRPC service or Web API controller, the scoped `ITokenProvider` is injected into the interceptor.
437+
* Configures `AddCallCredentials` for this client. The delegate is executed each time a call is made and adds the token returned by `ITokenProvider` to the metadata.
427438

428439
### Client certificate authentication
429440

aspnetcore/grpc/clientfactory.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,28 @@ builder.Services
136136
>
137137
> These values can be overriden by `ConfigureChannel`.
138138
139+
## Call credentials
140+
141+
An authentication header can be added to gRPC calls using the `AddCallCredentials` method:
142+
143+
```csharp
144+
builder.Services
145+
.AddGrpcClient<Greeter.GreeterClient>(o =>
146+
{
147+
o.Address = new Uri("https://localhost:5001");
148+
})
149+
.AddCallCredentials((context, metadata) =>
150+
{
151+
if (!string.IsNullOrEmpty(_token))
152+
{
153+
metadata.Add("Authorization", $"Bearer {_token}");
154+
}
155+
return Task.CompletedTask;
156+
});
157+
```
158+
159+
For more information about configuring call credentials, see [Bearer token with gRPC client factory](xref:grpc/authn-and-authz#bearer-token-with-grpc-client-factory).
160+
139161
## Deadline and cancellation propagation
140162

141163
gRPC clients created by the factory in a gRPC service can be configured with `EnableCallContextPropagation()` to automatically propagate the deadline and cancellation token to child calls. The `EnableCallContextPropagation()` extension method is available in the [Grpc.AspNetCore.Server.ClientFactory](https://www.nuget.org/packages/Grpc.AspNetCore.Server.ClientFactory) NuGet package.

aspnetcore/grpc/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ The following table describes options for configuring gRPC channels:
6969
| `Credentials` | `null` | A `ChannelCredentials` instance. Credentials are used to add authentication metadata to gRPC calls. |
7070
| `CompressionProviders` | gzip | A collection of compression providers used to compress and decompress messages. Custom compression providers can be created and added to the collection. The default configured providers support **gzip** compression. |
7171
| `ThrowOperationCanceledOnCancellation` | `false` | If set to `true`, clients throw <xref:System.OperationCanceledException> when a call is canceled or its deadline is exceeded. |
72+
| `UnsafeUseInsecureChannelCallCredentials` | `false` | If set to `true`, `CallCredentials` are applied to gRPC calls made by an insecure channel. Sending authentication headers over an insecure connection has security implications and shouldn't be done in production environments. |
7273
| `MaxRetryAttempts` | 5 | The maximum retry attempts. This value limits any retry and hedging attempt values specified in the service config. Setting this value alone doesn't enable retries. Retries are enabled in the service config, which can be done using `ServiceConfig`. A `null` value removes the maximum retry attempts limit. For more information about retries, see <xref:grpc/retries>. |
7374
| `MaxRetryBufferSize` | 16 MB | The maximum buffer size in bytes that can be used to store sent messages when retrying or hedging calls. If the buffer limit is exceeded, then no more retry attempts are made and all hedging calls but one will be canceled. This limit is applied across all calls made using the channel. A `null` value removes the maximum retry buffer size limit. |
7475
| `MaxRetryBufferPerCallSize` | 1 MB | The maximum buffer size in bytes that can be used to store sent messages when retrying or hedging calls. If the buffer limit is exceeded, then no more retry attempts are made and all hedging calls but one will be canceled. This limit is applied to one call. A `null` value removes the maximum retry buffer size limit per call. |

0 commit comments

Comments
 (0)