Local API authentication
An API the host serves in the identity server's own process — an administration surface, a configuration endpoint, an
internal operation — is a resource server with no network between it and the token authority. It should not have to
point a JWT handler back at the server it is running inside, so it does not. APIs
shows the registration, the endpoint, the IdentityServerApi scope and a client allowed to call; this page is what to
know before relying on it. Its code is included from
samples/Protocols.Dpop,
which a test calls with a bearer token, which its API refuses, and with a DPoP-bound one, which it takes.
One registration, a scheme and a policy
AddLocalApiAuthentication registers an
authentication scheme and the matching authorization policy together, both named IdentityServerAccessToken
(AuthenticationScheme and
PolicyName, Duende's; the policy was IdentityServerApi
until 0.165.0 — see Breaking in 0.165.0).
Either alone is a half a host has to notice is missing: a scheme with no policy protects nothing until somebody writes
one, and a policy names a scheme that has to exist. The policy pins the scheme explicitly rather than trusting the
default, which in a host also serving this server's pages is a browser cookie. Both registration methods are safe to
call twice, so a library adding the local API for an application that also adds it does not take the host down at
startup.
The token is validated in this process by the same ITokenValidator the protocol endpoints use, against the same
keys and the same issuer — and it runs the sender-constraint check, so a DPoP- or certificate-bound token is only
accepted from the caller that can prove it holds the binding; see
presenting a bound token back to this server.
Four things worth knowing before you rely on it:
ExpectedScope is required |
There is no way to say "any token this server issued". That would be a door opened by every token in the deployment, including one minted for an unrelated API by a client you never meant near your administration surface |
A valid token without the scope gets 401, not 403 |
The scope is required by the handler, so AddLocalApi() on its own still guards the API. The cost is that a caller you can identify is told you cannot |
| A refused call is answered by the header alone | 401 with WWW-Authenticate naming the scheme and error="invalid_token" (or the DPoP error, with a DPoP-Nonce), and no body — even in a host that calls UseIdentityServerErrorPages(), which since 0.174.0 leaves any response carrying WWW-Authenticate alone; before, the local API's challenge was given the HTML status page (see the DPoP flow) |
| Read the saved token back by naming the scheme | The overload of GetTokenAsync without one reads the default authenticate scheme — the browser cookie — and answers null |
// A local API that calls on to something else reads the token it was called with back by naming the scheme.
// GetTokenAsync without one reads the default scheme, the browser cookie, and answers null.
app.MapGet("/api/field-report", async (HttpContext context) =>
{
var token = await context.GetTokenAsync(IdentityServerConstants.LocalApi.AuthenticationScheme, "access_token");
return Results.Json(new { forwarded = token is not null });
})
.RequireAuthorization(IdentityServerConstants.LocalApi.PolicyName);
Changed in 0.128.0: IProfileService.IsActiveAsync is consulted, as Duende's local API does. Until 0.128.0 it
was not — a decision that a resource server checks only the token it was given — but the local API lives inside the
server that knows the user, and a token that outlives its account should stop opening it there first. See
Users the profile service no longer calls active.
Which schemes it takes
Since 0.138.0, TokenMode, Duende 8.0.1's, measured:
// The local API takes DPoP-bound tokens only: a Bearer token is ignored, and the challenge is DPoP.
builder.Services.AddLocalApiAuthentication(options => options.TokenMode = LocalApiTokenMode.DPoPOnly);
| LocalApiTokenMode | Takes |
|---|---|
DPoPAndBearer (our default) |
a token under either scheme |
DPoPOnly |
DPoP only; a Bearer token is ignored and the challenge is DPoP |
BearerOnly |
Bearer only; a DPoP token is ignored and the challenge is Bearer |
An ignored token is no result rather than a failure, as Duende's, so another scheme on the endpoint can still answer,
and Duende's debug line is logged. Ours, recorded: the default takes both, where Duende's is BearerOnly, because this
handler has taken both since 0.45.0; a token bound to a key is refused under Bearer in every mode, where Duende's
BearerOnly accepts it; and the challenge names one scheme where Duende's lists every scheme the mode takes.
The rest of what the handler lets a host set — the expected scope, whether the token is saved, and the hook that shapes the principal the API sees — is LocalApiAuthenticationOptions.