Skip to content

Add docs for new gRPC call credentials features #25734

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

Merged
merged 3 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
141 changes: 77 additions & 64 deletions aspnetcore/grpc/authn-and-authz.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ public bool DoAuthenticatedCall(
}
```

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`.
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.

`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.
> [!NOTE]
> `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.

The credential in the following example configures the channel to send the token with every gRPC call:

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

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

#### Bearer token with gRPC client factory

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.
gRPC client factory can create clients that send a bearer token using `AddCallCredentials`. This method is available in [Grpc.Net.ClientFactory](https://www.nuget.org/packages/Grpc.Net.ClientFactory) version 2.46.0 or later.

The delegate passed to `AddCallCredentials` is executed for each gRPC call:

```csharp
builder.Services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
})
.ConfigureChannel(o =>
.AddCallCredentials((context, metadata) =>
{
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
if (!string.IsNullOrEmpty(_token))
{
if (!string.IsNullOrEmpty(_token))
{
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});

o.Credentials = ChannelCredentials.Create(new SslCredentials(), credentials);
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});
```

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).
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).

Consider an app that has:

* A user-defined `ITokenProvider` for getting a bearer token. `ITokenProvider` is registered in DI with a scoped lifetime.
* gRPC client factory is configured to create clients that are injected into gRPC services and Web API controllers.
* gRPC calls should use `ITokenProvider` to get a bearer token.

```csharp
public class AuthInterceptor : Interceptor
public interface ITokenProvider
{
private readonly ITokenProvider _tokenProvider;

public AuthInterceptor(ITokenProvider tokenProvider)
{
_tokenProvider = tokenProvider;
}

public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
Task<string> GetTokenAsync();
}

public class AppTokenProvider : ITokenProvider
{
private string _token;

public async Task<string> GetTokenAsync()
{
context.Options.Metadata.Add("Authorization", $"Bearer {_tokenProvider.GetToken()}");
return continuation(request, context);
if (_token == null)
{
// App code to resolve the token here.
}

return _token;
}
}
```

```csharp
builder.Services.AddScoped<ITokenProvider, AppTokenProvider>();

builder.Services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
})
.AddInterceptor<AuthInterceptor>(InterceptorScope.Client);
.AddCallCredentials(async (context, metadata, serviceProvider) =>
{
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
var token = await provider.GetTokenAsync();
metadata.Add("Authorization", $"Bearer {token}");
}));
```

The preceeding code:
* Defines `AuthInterceptor` which is constructed using the user defined `ITokenProvider`.
The preceding code:

* Defines `ITokenProvider` and `AppTokenProvider`. These types handle resolving the authentication token for gRPC calls.
* 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.
* Registers the `GreeterClient` type with client factory.
* 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.
* 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.

### Client certificate authentication

Expand Down Expand Up @@ -360,70 +367,76 @@ private static GrpcChannel CreateAuthenticatedChannel(string address)

#### Bearer token with gRPC client factory

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.
gRPC client factory can create clients that send a bearer token using `AddCallCredentials`. The delegate passed to `AddCallCredentials` is executed for each gRPC call:

```csharp
services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
})
.ConfigureChannel(o =>
.AddCallCredentials((context, metadata) =>
{
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
if (!string.IsNullOrEmpty(_token))
{
if (!string.IsNullOrEmpty(_token))
{
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});

o.Credentials = ChannelCredentials.Create(new SslCredentials(), credentials);
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});
```

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).
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).

Consider an app that has:

* A user-defined `ITokenProvider` for getting a bearer token. `ITokenProvider` is registered in DI with a scoped lifetime.
* gRPC client factory is configured to create clients that are injected into gRPC services and Web API controllers.
* gRPC calls should use `ITokenProvider` to get a bearer token.

```csharp
public class AuthInterceptor : Interceptor
public interface ITokenProvider
{
private readonly ITokenProvider _tokenProvider;

public AuthInterceptor(ITokenProvider tokenProvider)
{
_tokenProvider = tokenProvider;
}

public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
TRequest request,
ClientInterceptorContext<TRequest, TResponse> context,
AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
Task<string> GetTokenAsync();
}

public class AppTokenProvider : ITokenProvider
{
private string _token;

public async Task<string> GetTokenAsync()
{
context.Options.Metadata.Add("Authorization", $"Bearer {_tokenProvider.GetToken()}");
return continuation(request, context);
if (_token == null)
{
// App code to resolve the token here.
}

return _token;
}
}
```

```csharp
services.AddScoped<ITokenProvider, AppTokenProvider>();

services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
})
.AddInterceptor<AuthInterceptor>(InterceptorScope.Client);
.AddCallCredentials(async (context, metadata, serviceProvider) =>
{
var provider = serviceProvider.GetRequiredService<ITokenProvider>();
var token = await provider.GetTokenAsync();
metadata.Add("Authorization", $"Bearer {token}");
}));
```

The preceeding code:
* Defines `AuthInterceptor` which is constructed using the user defined `ITokenProvider`.
The preceding code:

* Defines `ITokenProvider` and `AppTokenProvider`. These types handle resolving the authentication token for gRPC calls.
* 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.
* Registers the `GreeterClient` type with client factory.
* 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.
* 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.

### Client certificate authentication

Expand Down
22 changes: 22 additions & 0 deletions aspnetcore/grpc/clientfactory.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,28 @@ builder.Services
>
> These values can be overriden by `ConfigureChannel`.

## Call credentials

An authentication header can be added to gRPC calls using the `AddCallCredentials` method:

```csharp
builder.Services
.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
})
.AddCallCredentials((context, metadata) =>
{
if (!string.IsNullOrEmpty(_token))
{
metadata.Add("Authorization", $"Bearer {_token}");
}
return Task.CompletedTask;
});
```

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).

## Deadline and cancellation propagation

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.
Expand Down
1 change: 1 addition & 0 deletions aspnetcore/grpc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ The following table describes options for configuring gRPC channels:
| `Credentials` | `null` | A `ChannelCredentials` instance. Credentials are used to add authentication metadata to gRPC calls. |
| `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. |
| `ThrowOperationCanceledOnCancellation` | `false` | If set to `true`, clients throw <xref:System.OperationCanceledException> when a call is canceled or its deadline is exceeded. |
| `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. |
| `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>. |
| `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. |
| `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. |
Expand Down