APIs
Two things a resource server needs from an authorization server: tokens it can check, and — for a client that can hold
a key — tokens that are useless to anyone who copies them. The code on this page is included from
samples/Apis, which a test boots and
calls with and without a token.
An API in the server's own process
An administration surface, a configuration endpoint, an operation only your tools call: it lives in the identity server's host, and validating its tokens there needs no JWT handler pointed back at the same process.
// The authentication scheme and the authorization policy, both named for the local API. Tokens are validated in
// this process, against this server's own keys; DPoP-bound tokens are accepted with their proof.
builder.Services.AddLocalApiAuthentication();
AddLocalApiAuthentication registers an authentication scheme and an authorization policy, both PolicyName. The token is validated in this process, against this server's keys, and a DPoP- or certificate-bound token only from the caller holding the binding.
app.MapGet("/api/whoami", (ClaimsPrincipal caller) => Results.Json(new
{
client = caller.FindFirst(JwtClaimTypes.ClientId)?.Value,
scopes = caller.FindAll(JwtClaimTypes.Scope).Select(scope => scope.Value),
}))
.RequireAuthorization(IdentityServerConstants.LocalApi.PolicyName);
A token must carry the IdentityServerApi scope (ScopeName),
so register it and give it to the clients that may call:
// The scope a token must carry to reach the local API; register it like any other scope.
new ApiScope(IdentityServerConstants.LocalApi.ScopeName, "This server's own API"),
// An operations tool allowed to call the local API.
new Client
{
ClientId = "ops-tool",
ClientSecrets = { new Secret("ops-tool-secret".Sha256()) },
AllowedGrantTypes = { OidcConstants.GrantTypes.ClientCredentials },
AllowedScopes = { IdentityServerConstants.LocalApi.ScopeName },
},
Which token schemes the API takes, and the scope it expects, are LocalApiAuthenticationOptions.
Tokens bound to the client's key: DPoP
// A client whose tokens are bound to a key it holds: every token request must carry a DPoP proof, the
// access token is issued as token_type DPoP with a cnf.jkt claim, and an API must demand a proof made
// with the same key. Nonce mode also has the client sign a nonce this server hands out, so a proof
// cannot be made in advance.
new Client
{
ClientId = "telemetry-agent",
ClientSecrets = { new Secret("telemetry-agent-secret".Sha256()) },
AllowedGrantTypes = { OidcConstants.GrantTypes.ClientCredentials },
AllowedScopes = { "telemetry" },
RequireDPoP = true,
DPoPValidationMode = DPoPTokenExpirationValidationMode.Nonce,
},
With RequireDPoP the client must sign a proof (RFC 9449) with a key it holds
on every token request, and every token it receives — the access token and the refresh token — is bound to that key. A
request without a proof is refused. DPoPValidationMode decides whose clock a
proof is held to: the client's iat, a nonce this server issues
(DPoPTokenExpirationValidationMode), or both.
// How long a proof is good for after its iat, and how far ahead of this server a client's clock may run.
options.DPoP.ProofTokenValidityDuration = TimeSpan.FromSeconds(30);
options.DPoP.ClientClockSkew = TimeSpan.FromMinutes(1);
DPoPOptions sets those clocks server-wide, and the signing algorithms a proof may use, which the discovery document advertises.