Authentication
Presenting an access token on outbound calls as this service or as the person it is serving, and whose fault it is when one cannot be had.
ApricotFramework.Grpc.Client.Authentication puts an access token on the calls this service makes,
obtained through ApricotFramework.Authentication. It
is about what a client presents, never about who may call this service — that is the server's business
and this package has nothing to do with it.
services.AddGrpcClient<Orders.OrdersClient>(…)
.AddGrpcCallCredentials(credentials =>
{
credentials.Resource = "urn:svc:orders";
credentials.Scopes = ["orders.read", "orders.write"];
});Per client, because the point of a resource and a scope set is that they differ per callee. Leaving either null asks for this service's configured default.
Whose token
AddGrpcCallCredentials presents this service's own token, from the client credentials grant. It is
the right one wherever the callee is answering this service rather than a person — a background job, a
cache warm, anything with nobody waiting on it.
AddGrpcExchangedCallCredentials presents a token obtained on behalf of whoever this service is
serving, through
token exchange. The callee then applies
that person's authority, so a gateway or an agent surface can reach no more than the person behind it
could.
services.AddGrpcClient<Content.ContentClient>(…)
.AddGrpcExchangedCallCredentials(credentials =>
{
credentials.Resource = "urn:svc:content";
credentials.Scopes = ["content.agent"];
});The choice is per client because it is a property of the callee, and one host commonly does both. It
needs AddTokenExchangeAuthentication in the host; a client configured for the exchange without it
fails loudly on the first call rather than quietly going out as the service.
A call with nobody to act for fails too, as internal below. That is the point: the alternative is a
background job reaching what only a signed-in person should.
AddGrpcCallCredentials<TAuthenticator> is the open form, for a grant this package does not name.
The header
Every call carries Authorization: <scheme> <token>, with the scheme the provider named rather than a
hard-coded Bearer.
Where something between the caller and the callee claims that header for itself — a gateway that authenticates the edge, a mesh that rewrites it — name another:
{
"GrpcClients": {
"AuthorizationHeader": "x-service-authorization"
}
}services.ConfigureGrpcCallCredentials(builder.Configuration); // binds the sectionThat call is only needed to change the header; attaching credentials to a client does not require it.
gRPC lowercases the name and accepts letters, digits, _, - and .. A name it would reject — or one
ending in -bin, which it reserves for binary values — fails at startup rather than on the first call.
So does a blank one, which gRPC would otherwise accept and send the token under no name at all.
Whose fault it is
A token this service could not obtain is never reported as not_authenticated. The caller
presented a perfectly good credential; it is this service that cannot present one of its own, and
telling the caller to authenticate again is both useless and a lie about whose fault it is.
| Why no token | Reported as |
|---|---|
| The provider could not be reached, or faulted | unavailable — waiting may help |
| Anything else: bad client id, bad secret, bad authority, refused scope, nobody to act for | internal — a person has to change something |
The provider's own message is not repeated. It names the provider and can quote what it refused, and neither belongs in a failure a caller reads.
Note
This package does not depend on the error contract. It fails the call with a plain
RpcException carrying the right status code, and if
error translation is switched on the interceptor classifies
it from that — so the two are useful together and neither needs the other.