Added in Unreleased.

ZeeKayDa.Auth exposes an OpenID Connect discovery document as defined by OpenID Connect Discovery 1.0 Section 3 and Section 4.

For setup steps, see Configure discovery. For design rationale, see Why discovery matters.

Endpoint URL

Method: GET

Route:

  • Root issuer: /.well-known/openid-configuration
  • Path-bearing issuer: {issuer-path}/.well-known/openid-configuration

The route is constrained to the configured issuer host. A request for the same path on a different host is not handled by ZeeKayDa.Auth. Requests over HTTP are rejected for non-loopback hosts.

Examples:

  • Issuer: https://id.example.com
    Discovery URL: https://id.example.com/.well-known/openid-configuration
  • Issuer: https://id.example.com/tenant-a
    Discovery URL: https://id.example.com/tenant-a/.well-known/openid-configuration

This path behavior follows OpenID Connect Discovery 1.0 Section 4.1 and RFC 9207 Section 4.

Registration

Register services with AddZeeKayDaAuth(...), then map endpoints with app.MapZeeKayDaAuth().

using ZeeKayDa.Auth;
using ZeeKayDa.Auth.AspNetCore.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddZeeKayDaAuth(options =>
{
    options.Issuer = "https://id.example.com";
});

var app = builder.Build();

app.UseRouting();
app.MapZeeKayDaAuth();

app.Run();

Response

Status code

  • 200 OK when discovery is configured correctly
  • 421 Misdirected Request when the request is HTTP on a non-loopback host

Response headers

Header Value
Content-Type application/json
Cache-Control public, max-age=3600, must-revalidate by default; no-store when DiscoveryDocument.CacheMaxAge is below one second
Access-Control-Allow-Origin * when CorsOrigins is empty; the matched allowlist entry when non-empty
Vary Origin (only when CorsOrigins is non-empty), appended to any existing Vary value
X-Content-Type-Options nosniff (default; disable with SecurityHeaders.ContentTypeOptionsNoSniff = false)
Referrer-Policy no-referrer (default; configurable via SecurityHeaders.ReferrerPolicy)
Cross-Origin-Resource-Policy cross-origin (default; configurable via SecurityHeaders.CrossOriginResourcePolicy)
X-ZeeKayDa-Insecure-Issuer true (only when AllowInsecureIssuer = true)

CORS configuration

By default ZeeKayDa.Auth returns Access-Control-Allow-Origin: *, which allows any browser-based client to fetch the discovery document. This is intentional: the discovery document is public information with no credentials and no user-specific data.

To restrict CORS to a known set of origins, populate the server-wide CorsOrigins, which governs the discovery document, the JWKS and userinfo alike:

options.CorsOrigins.Add("https://app.example.com");
options.CorsOrigins.Add("https://admin.example.com");

When the list is non-empty:

  • Access-Control-Allow-Origin is set to the matching allowlist entry (lowercased, authority-only canonical form).
  • Vary: Origin is appended additively so that shared caches never serve the wrong Access-Control-Allow-Origin to a different origin.
  • Requests with an absent or non-matching Origin header receive no Access-Control-Allow-Origin header.

Allowlist entries are validated at startup. Each entry must be an absolute origin (scheme://host[:port]) with no path, query, fragment, user information, wildcards, or the literal string null. Entries are canonicalized, deduplicated, and frozen into an immutable startup snapshot used by endpoint lookups. Invalid entries cause the host to fail fast at startup.

https:// origins are accepted by default. http:// origins are rejected unless AllowInsecureIssuer = true; when enabled, HTTP origins must still use loopback hosts.

Note: ZeeKayDa.Auth does not register an HTTP OPTIONS route. The discovery endpoint is a simple CORS request (GET with no custom request headers), so browsers do not send a preflight OPTIONS request before fetching the discovery document.

Metadata fields

The fields below are the ones the discovery document can publish. Some are fixed values, some come from AuthorizationServerOptions, and some are derived; the notes say which are omitted, and on what condition.

JSON field Source Default / notes
issuer Issuer Published verbatim as configured.
authorization_endpoint AuthorizationEndpoint.Uri or derived from Issuer Default is {issuer}/connect/authorize.
token_endpoint TokenEndpoint.Uri or derived from Issuer Default is {issuer}/connect/token.
jwks_uri JwksEndpoint.Uri or derived from Issuer Default is {issuer}/connect/jwks.
userinfo_endpoint UserInfoEndpoint.Uri or derived from Issuer Default is {issuer}/connect/userinfo. Omitted on a host whose GrantTypesSupported lacks authorization_code, which serves no userinfo route.
response_types_supported Response.TypesSupported Defaults to ["code"]. Required by OIDC Discovery 1.0 Section 3.
scopes_supported IScopeRepository By default, published from the built-in InMemoryScopeRepository seeded with StandardScopes.All (openid, profile, email, phone, address).
response_modes_supported Response.ModesSupported Defaults to ["query"].
grant_types_supported GrantTypesSupported Defaults to ["authorization_code"].
token_endpoint_auth_methods_supported TokenEndpoint.AuthMethodsSupported Defaults to ["client_secret_basic"].
subject_types_supported Fixed value Always ["public"]. Pairwise subject identifiers are not currently supported.
id_token_signing_alg_values_supported The configured signing keys Derived: the distinct algorithms of every published key, ascending by SigningAlgorithm value, optionally narrowed by IdToken.AdvertisedSigningAlgorithms. Required by OIDC Discovery 1.0 Section 3.
claims_supported IScopeRepository + the ID token’s protocol claims Derived: the ID-token and userinfo claims of every discoverable scope, plus iss, sub, aud, iat, exp, auth_time, at_hash, nonce, acr and amr. A scope’s access-token claims are not listed. Omitted on a host whose GrantTypesSupported lacks authorization_code, which issues no ID token.
code_challenge_methods_supported AuthorizationEndpoint.CodeChallengeMethodsSupported ["S256"] by default, the one method the token endpoint verifies. Omitted when null, which startup permits only on a host that does not serve the authorization code grant.

The recommended metadata fields are described by OpenID Connect Discovery 1.0 Section 3 and RFC 8414 Section 2.

To replace the default scope source, register a custom scope repository. For example:

using ZeeKayDa.Auth.Scopes;

var auth = builder.Services.AddZeeKayDaAuth(options =>
{
    options.Issuer = "https://id.example.com";
});

auth.AddInMemoryScopes(
[
    new ScopeDefinition
    {
        Name = StandardScopes.OpenId.Name,
        IdTokenClaims = ["sub"],
        AccessTokenClaims = ["scope"],
    },
    new ScopeDefinition
    {
        Name = StandardScopes.Profile.Name,
        IdTokenClaims = ["name", "family_name"],
        AccessTokenClaims = ["name"],
    },
    new ScopeDefinition
    {
        Name = "internal.admin",
        IsDiscoverable = false,
        AccessTokenClaims = ["scope"],
    },
]);

Only scopes with IsDiscoverable = true are included in scopes_supported.

Pre-alpha advertised endpoints

ZeeKayDa.Auth is pre-alpha. Every advertised endpoint is implemented; the jwks_uri endpoint is described in JWKS endpoint and userinfo_endpoint in UserInfo endpoint.

Endpoint Methods Serves
{issuer}/connect/authorize GET, POST The authorization code flow with PKCE, through the host’s login and consent pages
{issuer}/connect/token POST The authorization_code grant: an access token and an ID token, after client authentication and code_verifier verification
{issuer}/connect/userinfo GET, POST, OPTIONS The signed-in user’s claims, to a caller presenting an access token this server issued with the openid scope

Endpoint URI derivation

When endpoint overrides are not set, ZeeKayDa.Auth derives published endpoint URLs from the configured issuer using URI combination rules.

For example, with this issuer:

https://id.example.com/tenant-a

the default published endpoints are:

  • https://id.example.com/tenant-a/connect/authorize
  • https://id.example.com/tenant-a/connect/token
  • https://id.example.com/tenant-a/connect/jwks
  • https://id.example.com/tenant-a/connect/userinfo

This matters for issuers with path segments.

Example document

{
  "issuer": "https://id.example.com/tenant-a",
  "authorization_endpoint": "https://id.example.com/tenant-a/connect/authorize",
  "token_endpoint": "https://id.example.com/tenant-a/connect/token",
  "jwks_uri": "https://id.example.com/tenant-a/connect/jwks",
  "userinfo_endpoint": "https://id.example.com/tenant-a/connect/userinfo",
  "response_types_supported": ["code"],
  "scopes_supported": ["openid", "profile", "api.read"],
  "response_modes_supported": ["query"],
  "grant_types_supported": ["authorization_code"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"]
}

Startup validation

Discovery configuration is validated at startup.

Startup fails when:

  • Issuer is missing or empty
  • Issuer is not an absolute URI
  • Issuer uses HTTP and AllowInsecureIssuer is not enabled
  • Issuer uses HTTP with a non-loopback host
  • Issuer is non-canonical (uppercase scheme/host or explicit default port)
  • Issuer contains a query string or fragment
  • Issuer contains user information
  • an endpoint override authority differs from Issuer
  • Response.TypesSupported is null or empty
  • IdToken.AdvertisedSigningAlgorithms is a non-null empty collection
  • no signing key source is registered, so there is no key set to derive id_token_signing_alg_values_supported from
  • any supported metadata collection is null
  • a custom scope repository is configured with blank or duplicate scope names
  • AuthorizationEndpoint.CodeChallengeMethodsSupported is set to a non-null empty collection

Warning: AllowInsecureIssuer = true is for local loopback development and testing only. It does not permit HTTP issuers on non-loopback hosts.