Apricot Framework

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 section

That 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 tokenReported as
The provider could not be reached, or faultedunavailable — waiting may help
Anything else: bad client id, bad secret, bad authority, refused scope, nobody to act forinternal — 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.

On this page