Wiaoj.WellKnown 0.3.0-alpha.1

This is a prerelease version of Wiaoj.WellKnown.
dotnet add package Wiaoj.WellKnown --version 0.3.0-alpha.1
                    
NuGet\Install-Package Wiaoj.WellKnown -Version 0.3.0-alpha.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Wiaoj.WellKnown" Version="0.3.0-alpha.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Wiaoj.WellKnown" Version="0.3.0-alpha.1" />
                    
Directory.Packages.props
<PackageReference Include="Wiaoj.WellKnown" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Wiaoj.WellKnown --version 0.3.0-alpha.1
                    
#r "nuget: Wiaoj.WellKnown, 0.3.0-alpha.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Wiaoj.WellKnown@0.3.0-alpha.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Wiaoj.WellKnown&version=0.3.0-alpha.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Wiaoj.WellKnown&version=0.3.0-alpha.1&prerelease
                    
Install as a Cake Tool

Wiaoj.WellKnown

Publishes /.well-known/* documents: RFC 9728 OAuth 2.0 Protected Resource Metadata for an API, and RFC 8414 OAuth 2.0 Authorization Server Metadata for an authorization server such as Vaultex, and RFC 9116 security.txt for any site. A client that gets a 401 from your API reads this document to find out which authorization server issues tokens for it, and with which scopes. MCP clients follow this flow.

Installation

dotnet add package Wiaoj.WellKnown

Usage

builder.Services.AddOAuthProtectedResource(resource => {
    resource.Resource = "https://api.example.com";                 // required, see below
    resource.AuthorizationServers.Add("https://auth.example.com");
    resource.ResourceName = "Example API";
    resource.BearerMethodsSupported.Add("header");
});

// Each module publishes the scopes it defines
builder.Services.AddProtectedResourceScopes("assets:read", "assets:write");

app.MapOAuthProtectedResource();   // GET /.well-known/oauth-protected-resource

The response looks like this:

{
  "resource": "https://api.example.com",
  "authorization_servers": ["https://auth.example.com"],
  "scopes_supported": ["assets:read", "assets:write"],
  "bearer_methods_supported": ["header"],
  "resource_name": "Example API"
}
  • Status and headers. The response is 200 with application/json and Cache-Control: public, max-age=86400. CacheDuration changes the max-age, and TimeSpan.Zero sends no-cache.
  • Omitted values. A parameter that is unset, an empty list or false is left out, as RFC 9728 §3.2 requires.
  • Anonymous access. The endpoint allows anonymous access even under a fallback authorization policy.

Resource is required

Resource is the only parameter the RFC requires, and clients compare it exactly. A client that found the document through https://api.example.com discards the document unless resource is exactly https://api.example.com (§3.3). So configure it as the public URL.

It is never derived from the request. A proxy changes the scheme and host the application sees, and the Host header is supplied by the caller.

Resources with a path, and several on one host

The document URL comes from the identifier. The rule is: remove a trailing slash after the host, then insert the well-known suffix between the host and the path (§3).

Resource Served at
https://api.example.com /.well-known/oauth-protected-resource
https://api.example.com/v1 /.well-known/oauth-protected-resource/v1
https://api.example.com/v1/ /.well-known/oauth-protected-resource/v1/

Register several resources by name. MapOAuthProtectedResource() serves each one at its own path:

builder.Services.AddOAuthProtectedResource("public", r => r.Resource = "https://api.example.com/v1");
builder.Services.AddOAuthProtectedResource("admin", r => r.Resource = "https://api.example.com/admin");
builder.Services.AddProtectedResourceScopes("admin", ["users:manage"]);

ProtectedResourceMetadataUri.For(resource) returns the absolute document URL. The challenge in Wiaoj.WellKnown.JwtBearer uses the same computation, so the URL it advertises is always the route that answers.

What startup rejects

The options are validated when the application starts. Every failure is reported in a single OptionsValidationException, so you do not have to restart once per mistake.

Rule Why
Resource is set, absolute, https, and has no query, fragment or user info Clients compare it exactly. A fragment is not allowed (§1.2), and a query cannot be routed.
AuthorizationServers and JwksUri entries are absolute https URLs They are issuer and key locations
ResourceDocumentation, ResourcePolicyUri and ResourceTosUri are absolute URLs They are links
The algorithm lists do not contain none Forbidden by §2
BearerMethodsSupported contains only header, body or query These are the §2 values
Every scope is a valid scope token, with no spaces or quotes RFC 6749 §3.3

http is accepted on a loopback host (localhost, 127.0.0.1) for local development.

Also mapped at startup: two resources that produce the same document path throw.

Advertising the document on 401

See Wiaoj.WellKnown.JwtBearer. It adds resource_metadata to JwtBearer's challenge and keeps error="invalid_token" and the other parameters. ProtectedResourceChallenge.AddOnStarting does the same for any other authentication handler.

Authorization server metadata (RFC 8414)

An authorization server publishes where its endpoints are, and what it supports. A client that learned the issuer from a protected resource's authorization_servers reads this document to find the token endpoint.

builder.Services.AddOAuthAuthorizationServer(server => {
    server.Issuer = "https://vaultex.example.com";                          // required
    server.TokenEndpoint = "https://vaultex.example.com/connect/token";
    server.DeviceAuthorizationEndpoint = "https://vaultex.example.com/connect/device"; // RFC 8628, for the CLI
    server.JwksUri = "https://vaultex.example.com/.well-known/jwks.json";
    server.GrantTypesSupported.AddRange(["client_credentials", "urn:ietf:params:oauth:grant-type:device_code"]);
    server.ResponseTypesSupported.Add("code");                             // required
    server.TokenEndpointAuthMethodsSupported.Add("private_key_jwt");
    server.TokenEndpointAuthSigningAlgValuesSupported.Add("ES256");        // required with private_key_jwt
});

app.MapOAuthAuthorizationServer();   // GET /.well-known/oauth-authorization-server

Status, caching, omitted values and anonymous access work as for the protected resource document.

What is required

RFC 8414 §2 requires less than it seems, and more:

Parameter Required
issuer Always. Absolute https, no query or fragment. Clients compare it exactly (§3.3).
response_types_supported Always, even for a server with no authorization endpoint.
authorization_endpoint Unless no supported grant type uses it. authorization_code and implicit use it.
token_endpoint Unless implicit is the only supported grant.
…_auth_signing_alg_values_supported When private_key_jwt or client_secret_jwt is listed for that endpoint.

An empty GrantTypesSupported is not "no grant types": clients read it as authorization_code and implicit, so both endpoints are then required. A service-to-service server lists its grant types explicitly.

Startup also rejects:

  • endpoint URLs that are not absolute https. Documentation, policy and terms links may be http.
  • none in any algorithm list.
  • invalid scope tokens.
  • protected_resources entries that are not resource identifiers.
  • RequirePushedAuthorizationRequests without PushedAuthorizationRequestEndpoint.

Issuers with a path

RFC 8414 removes the terminating slash of the issuer's path. RFC 9728 removes only the slash after the host.

Issuer Served at
https://vaultex.example.com /.well-known/oauth-authorization-server
https://vaultex.example.com/tenant1 /.well-known/oauth-authorization-server/tenant1
https://vaultex.example.com/tenant1/ /.well-known/oauth-authorization-server/tenant1

Several issuers are registered by name, as resources are. Two issuers that differ only by that slash derive the same path, so mapping them throws. AuthorizationServerMetadataUri.For(issuer) returns the document URL.

Parameters

Typed options cover:

  • every RFC 8414 §2 parameter
  • device_authorization_endpoint (RFC 8628)
  • protected_resources (RFC 9728 §4)
  • pushed_authorization_request_endpoint and require_pushed_authorization_requests (RFC 9126)
  • authorization_response_iss_parameter_supported (RFC 9207)
  • dpop_signing_alg_values_supported (RFC 9449)
  • tls_client_certificate_bound_access_tokens (RFC 8705)

Any other registered parameter goes in AdditionalParameters, for example OpenID Connect's userinfo_endpoint or mtls_endpoint_aliases. A name that has a typed option is refused, because it would bypass validation.

Publishing at openid-configuration as well is not supported.

security.txt (RFC 9116)

security.txt tells security researchers how to report a vulnerability. It is served at /.well-known/security.txt.

builder.Services.AddSecurityTxt();
builder.Services.Configure<SecurityTxtOptions>(builder.Configuration.GetSection("SecurityTxt"));

app.MapSecurityTxt();
"SecurityTxt": {
  "Contact": [ "mailto:security@example.com", "https://example.com/security/report" ],
  "Expires": "2027-06-30T00:00:00Z",
  "Encryption": [ "https://example.com/pgp-key.txt" ],
  "Acknowledgments": [ "https://example.com/security/thanks" ],
  "PreferredLanguages": [ "en", "tr" ],
  "Canonical": [ "https://example.com/.well-known/security.txt" ],
  "Policy": [ "https://example.com/security/policy" ],
  "Hiring": [ "https://example.com/jobs" ]
}

Every field RFC 9116 defines has an option. AdditionalFields holds fields registered later, and a name RFC 9116 defines is refused there.

Expires

Expires is a fixed date you set. It isn't computed from the start time: a date that moved forward on every restart would never let the file look stale, however outdated its contacts became, and that is exactly what the field is for.

When the endpoint is mapped Result
Expires has passed Startup fails
More than a year ahead (RFC 9116 recommends less) Warning
Less than 30 days away Warning, so the date is renewed before it lapses

If the date passes while the application is running, the file is still served, since its contacts are the best available, and one warning is logged. The next start fails until the date is renewed.

What startup rejects

  • Contact: no Contact, or a Contact that is not a mailto:, tel: or https:// URI. A bare email address gets a hint to write it as mailto:.
  • URIs: an http:// URI in any field; RFC 9116 requires web URIs to begin with https://.
  • Encryption: a key pasted into Encryption instead of its URI.
  • Preferred-Languages: a value that isn't an RFC 5646 language tag. The values are written as one comma-separated field.
  • Line breaks: any value with a line break, which would inject another field.
  • Size: a file researchers may refuse to parse (§5.4): more than 32 KB, more than 1,000 lines, or a field longer than 2,048 characters.

Serving

  • Format: text/plain; charset=utf-8, lines ending with LF, Cache-Control from CacheDuration (one day by default).
  • Legacy path: /security.txt redirects to /.well-known/security.txt with 301. Turn it off with RedirectLegacyPath = false.
  • HTTPS: RFC 9116 requires the file to be retrieved over https. Serve the application over https (behind a TLS-terminating proxy, the proxy does this), and list the public URL in Canonical, since researchers shouldn't trust a file fetched from a URL it doesn't list.
  • Not supported: OpenPGP-signed files.

Discovering these documents

Wiaoj.WellKnown.Discovery is the client side. From a 401 or a resource identifier, it fetches and validates both documents.

Not supported

signed_metadata (JWS-signed metadata, RFC 9728 §2.2 and RFC 8414 §2.1) is not supported.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Wiaoj.WellKnown:

Package Downloads
Wiaoj.WellKnown.JwtBearer

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0-alpha.1 0 10/5/2026
0.2.0-alpha.3 57 9/24/2026
0.2.0-alpha.2 48 9/24/2026
0.2.0-alpha.1 52 9/24/2026
0.1.0-alpha.9 135 9/21/2026
0.1.0-alpha.8 51 9/21/2026
0.1.0-alpha.7 56 9/18/2026
0.1.0-alpha.6 57 9/16/2026
0.1.0-alpha.5 54 9/16/2026
0.1.0-alpha.4 52 9/16/2026
0.1.0-alpha.3 48 9/15/2026
0.1.0-alpha.2 50 9/15/2026
0.1.0-alpha.1 91 9/14/2026
0.0.1-alpha.112-preview 50 9/13/2026
0.0.1-alpha.111-preview 54 9/13/2026