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 OKwhen discovery is configured correctly421 Misdirected Requestwhen 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-Originis set to the matching allowlist entry (lowercased, authority-only canonical form).Vary: Originis appended additively so that shared caches never serve the wrongAccess-Control-Allow-Originto a different origin.- Requests with an absent or non-matching
Originheader receive noAccess-Control-Allow-Originheader.
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
OPTIONSroute. The discovery endpoint is a simple CORS request (GETwith no custom request headers), so browsers do not send a preflightOPTIONSrequest 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/authorizehttps://id.example.com/tenant-a/connect/tokenhttps://id.example.com/tenant-a/connect/jwkshttps://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:
Issueris missing or emptyIssueris not an absolute URIIssueruses HTTP andAllowInsecureIssueris not enabledIssueruses HTTP with a non-loopback hostIssueris non-canonical (uppercase scheme/host or explicit default port)Issuercontains a query string or fragmentIssuercontains user information- an endpoint override authority differs from
Issuer Response.TypesSupportedis null or emptyIdToken.AdvertisedSigningAlgorithmsis a non-null empty collection- no signing key source is registered, so there is no key set to derive
id_token_signing_alg_values_supportedfrom - any supported metadata collection is null
- a custom scope repository is configured with blank or duplicate scope names
AuthorizationEndpoint.CodeChallengeMethodsSupportedis set to a non-null empty collection
Warning:
AllowInsecureIssuer = trueis for local loopback development and testing only. It does not permit HTTP issuers on non-loopback hosts.