Table of Contents

Endpoints

Every path this server answers, what each is for, and the switches that turn them off. The code on this page is included from samples/Server.Hosting, which a test boots and asks for its discovery document. Setting the server up is getting started; this page is what that setup serves.

The pipeline

var app = builder.Build();

app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();   // every protocol endpoint, at the host's root: /.well-known/*, /connect/*
app.UseAuthorization();
app.MapRazorPages();       // the sign-in, consent, sign-out, device, backchannel and error pages

app.Run();

UseIdentityServer serves the protocol endpoints at fixed paths, /.well-known/* and /connect/*. Before it adds anything to the pipeline it refuses to start a server that could not work: no authentication scheme to sign users in with, a session scheme named in Authentication.CookieAuthenticationScheme that is not registered, automatic key management off with no signing credential, a signing algorithm automatic management cannot generate, CIBA switched on without the host's half of it, an extension grant that could never run (one named like a built-in grant, or two validators claiming one grant type), pushed authorization required server-wide with its endpoint off, and a negative ConsumedRefreshTokenGracePeriod. It then applies the protocol endpoints' CORS policy by name, so the host's own CORS configuration is untouched, and serves the endpoints.

The interactive pages — sign-in, consent, sign-out, device and backchannel approval, the error page — are the Fdyay.IdentityServer.UI Razor class library's, mapped by MapRazorPages. Where the server sends a browser for each is UserInteractionOptions.

Every path

Each protocol endpoint is registered by AddIdentityServer() whatever the options say, and the router reads EndpointsOptions per request, so a switch set after AddIdentityServer() still takes effect. A switched-off endpoint falls through the middleware — a 404 in a host with nothing else at that path — and drops out of discovery.

Path What it is Switch Recorded flow
/.well-known/openid-configuration OpenID Connect discovery: the document every client starts from EnableDiscoveryEndpoint Discovery and JWKS
/.well-known/openid-configuration/jwks The JSON Web Key Set: the public half of every signing key, current and retiring EnableDiscoveryEndpoint, and Discovery.ShowKeySet Discovery and JWKS
/.well-known/oauth-authorization-server The same document as OAuth 2.0 authorization server metadata (RFC 8414), below EnableOAuth2MetadataEndpoint —
/connect/authorize The authorization endpoint: a browser arrives, signs in, consents, and leaves with a code EnableAuthorizeEndpoint Authorization code with PKCE
/connect/token The token endpoint: every grant — code, refresh, client credentials, device, CIBA, token exchange, extension grants EnableTokenEndpoint Client credentials, refresh, token exchange, resource indicators, DPoP
/connect/userinfo The signed-in user's claims, for a bearer of an access token with openid EnableUserInfoEndpoint —
/connect/introspect Token introspection (RFC 7662), for an API holding a reference token or checking a JWT is still live EnableIntrospectionEndpoint Token introspection
/connect/revocation Token revocation (RFC 7009) EnableTokenRevocationEndpoint Token revocation
/connect/endsession RP-initiated logout: ends the session here and tells the clients it reached EnableEndSessionEndpoint RP-initiated logout
/connect/checksession The session management iframe a relying party embeds to notice a sign-out (why it may be framed) EnableCheckSessionEndpoint —
/connect/par Pushed authorization requests (RFC 9126) EnablePushedAuthorizationEndpoint PAR
/connect/deviceauthorization The device authorization grant (RFC 8628); the user approves at /device EnableDeviceAuthorizationEndpoint Device authorization
/connect/ciba Client-initiated backchannel authentication — off by default, until a host supplies the two services only it can EnableBackchannelAuthenticationEndpoint CIBA
/connect/mtls/token and the other aliases The endpoints a client authenticates at, again under a path a listener can demand a certificate on — see client certificates MutualTls.Enabled —
/saml/sso SAML 2.0 single sign-on, service-provider initiated (with AddSaml) EnableSamlSigninEndpoint SAML 2.0 single sign-on
/saml/sso/callback Where a SAML sign-on resumes after the sign-in page EnableSamlSigninCallbackEndpoint SAML 2.0 single sign-on
/saml/sso/initiate IdP-initiated SAML sign-on — off until SamlOptions.EnableIdpInitiatedSignOn EnableSamlSigninEndpoint —
/saml/slo SAML 2.0 single logout EnableSamlLogoutEndpoint —
/saml/metadata This server's SAML 2.0 identity provider metadata EnableSamlMetadataEndpoint —

The four SAML switches are off by default and switched on by AddSaml, as Duende's are; switch one off with a Configure<IdentityServerOptions> after AddSaml. The sign-on switch closes the IdP-initiated door too. The SAML sign-on, callback and logout paths move with SamlOptions.Endpoints (SAML identity provider); the metadata path stays fixed.

The demo host in src/Fdyay.IdentityServer.Host also answers /health; that is the host's own endpoint, not this library's.

The mTLS aliases are /connect/mtls/token, /connect/mtls/introspect, /connect/mtls/revocation, /connect/mtls/par, /connect/mtls/deviceauthorization and /connect/mtls/ciba — the endpoints a client calls directly with its credentials, read from one list by both the router and discovery, so an alias cannot answer without being advertised or be advertised without answering.

OAuth 2.0 authorization server metadata

New in 0.125.0. RFC 8414's /.well-known/oauth-authorization-server serves the discovery document, for a client that reads OAuth's well-known path rather than OpenID Connect's — Duende 8.0.1's endpoint, measured. It is on by default under its own flag, EndpointsOptions.EnableOAuth2MetadataEndpoint; turning discovery off leaves it on, as in Duende. GET only.

RFC 8414 §3.1 puts an issuer's path after the well-known segment: an issuer https://idp.example.com/tenant publishes at https://idp.example.com/.well-known/oauth-authorization-server/tenant. So the endpoint answers beneath its own path, and only when what follows is this server's issuer:

  • the path must satisfy IIssuerPathValidator — by default, no path at all, or the issuer's own path, ignoring case; register your own to accept others;
  • the issuer must then be exactly the request's scheme, host and that path — so a server whose configured IssuerUri names another host publishes no OAuth metadata at this one, while its OpenID document still answers;
  • and a request under a path base is a 404: RFC 8414's well-known path is at the host's root.

Anything else is a 404, so a client is never handed metadata naming an issuer other than the one it asked for (RFC 8414 §3.3).

Discovery options

// A member of the host's own beside the document's; "~/" makes it a URL under the issuer.
options.Discovery.CustomEntries["service_documentation"] = "~/docs";

// Cache-Control: max-age=3600 on the discovery document, its key set and the OAuth metadata.
options.Discovery.ResponseCacheInterval = 3600;

// Hold the document's JSON in the IDistributedCache for a minute rather than building it per request.
options.Discovery.EnableDiscoveryDocumentCache = true;
options.Discovery.DiscoveryDocumentCacheDuration = TimeSpan.FromMinutes(1);

New in 0.139.0, Duende 8.0.1's DiscoveryOptions, measured from its options, DiscoveryResponseGenerator and discovery endpoints. Every switch is on by default, as Duende's are; each is described under DiscoveryOptions. What the reference does not say:

  • The three authentication-method lists are one list — the methods the registered secret parsers implement — because the revocation and introspection endpoints take their caller's credential through the same parsers as the token endpoint. The client-assertion algorithms go beside a list only when private_key_jwt is on it and SupportedClientAssertionSigningAlgorithms is not empty, as Duende's do; until 0.139.0 the token endpoint's were always written.
  • A custom entry never replaces a member the document already has — it is skipped, with Duende's error logged — so configuration cannot rewrite the issuer or an endpoint every client is told to trust.
  • The cached document is the one built for the first request in its window: an option or a resource changed meanwhile shows when it expires. The cache is the host's IDistributedCache — the in-memory one by default, which no other instance shares.
  • ResponseCacheInterval sets the max-age of the discovery document, the key set and the OAuth metadata alike; 0 sends no-store; null (the default) sends no Cache-Control.

Changed in 0.139.0:

  • The discovery document, the key set and the OAuth metadata no longer send no-store by default. They were written as the token endpoint's answers are; a document that carries no secret now carries no Cache-Control unless ResponseCacheInterval asks for one, as in Duende. Set it to 0 to keep the old headers.
  • The key set is a 404 while ShowKeySet is off, as Duende's is. Until then it was served at a path the document no longer named.
  • Breaking for a host that constructs them itself: DiscoveryResponseGenerator takes an ILogger<DiscoveryResponseGenerator>, and DiscoveryEndpoint, DiscoveryKeyEndpoint and OAuthMetadataEndpoint take IOptions<IdentityServerOptions>. Resolved from DI, nothing changes.

Ours: one result writes both documents, where Duende has DiscoveryDocumentResult and JsonWebKeysResult; the cache is keyed by the issuer alone, which here is also the base of every URL in the document (Duende keys by both). Not taken: DynamicClientRegistration, which advertises a registration endpoint this server does not have (decision 6 in docs/surface-remeasure.md).