Added in Unreleased.
ZeeKayDa.Auth requires at least one registered client before the server will start. This guide shows you how to register public and confidential clients using the built-in in-memory repository.
Quick start
Call AddInMemoryClients on the builder returned by AddZeeKayDaAuth and use the provided builder callbacks to register your clients:
var builder = services.AddZeeKayDaAuth(options =>
{
options.Issuer = "https://id.example.com";
// Allow public clients (no client authentication)
options.TokenEndpoint.AuthMethodsSupported.Add(TokenEndpointAuthMethods.None);
});
builder.AddInMemoryClients(clients =>
{
// A public client (SPA or native app using PKCE)
clients.AddPublic(
clientId: "my-spa",
redirectUris: ["https://app.example.com/callback"],
postLogoutRedirectUris: ["https://app.example.com/logout"],
allowedScopes: ["openid", "profile"]);
// A confidential client (server-side app)
clients.AddConfidential(
clientId: "my-server-app",
clientSecret: "replace-with-a-real-secret",
redirectUris: ["https://server.example.com/callback"],
postLogoutRedirectUris: [],
allowedScopes: ["openid", "api"]);
});
Warning: The
clientSecretparameter inAddConfidentialaccepts a plaintext string that is hashed at repository construction time. Never hardcode secrets in production code. Load them from environment variables, a secrets manager (e.g. Azure Key Vault), or a secure configuration provider instead.
Public clients
Public clients authenticate with no client credentials — they rely entirely on PKCE (RFC 7636) for authorization code security. Use AddPublic for single-page applications and native apps.
clients.AddPublic(
clientId: "my-spa",
redirectUris: ["https://app.example.com/callback"],
postLogoutRedirectUris: ["https://app.example.com/logout"],
allowedScopes: ["openid", "profile", "email"]);
To allow public clients, the server must advertise none as a supported token endpoint authentication method:
services.AddZeeKayDaAuth(options =>
{
options.Issuer = "https://id.example.com";
options.TokenEndpoint.AuthMethodsSupported.Add(TokenEndpointAuthMethods.None);
});
Confidential clients
Confidential clients authenticate at the token endpoint using a shared secret. Use AddConfidential for server-side web applications, background services, and APIs.
builder.AddInMemoryClients(clients =>
clients.AddConfidential(
clientId: "my-server-app",
clientSecret: configuration["ClientSecrets:MyServerApp"],
redirectUris: ["https://server.example.com/callback"],
postLogoutRedirectUris: ["https://server.example.com/logout"],
allowedScopes: ["openid"]));
The clientSecret value is hashed using the configured IClientSecretHasher (by default, PBKDF2-HMAC-SHA256 at 600,000 iterations) when the repository is first resolved from DI. The plaintext is not retained after hashing.
Changing a client’s other settings
AddPublic and AddConfidential take an optional last argument: a callback that receives the client’s settings, already filled with their defaults. Change only what you need:
builder.AddInMemoryClients(clients =>
clients.AddConfidential("first-party-web", secretValue,
["https://app.example.com/callback"], [], ["openid", "profile"],
options =>
{
options.RequireConsent = false;
options.DisplayName = "Example Web";
options.AccessTokenLifetime = TimeSpan.FromMinutes(2);
}));
A public client’s callback receives PublicClientOptions. A confidential client’s receives ConfidentialClientOptions, which adds RequirePkce and AllowedTokenEndpointAuthMethods — settings a public client cannot have.
Turn
RequireConsentoff only for your own first-party applications. The consent page is what lets a user notice an authorization request they never started.
Set InitiateLoginUri to an https address in your application that starts a new sign-in. When a login or consent page is submitted after its request is gone — a double click, or a page left open too long — the server sends the user there, with the server’s issuer as iss, instead of to its error page. Start a sign-in there only when iss is the server your application trusts:
options.InitiateLoginUri = "https://app.example.com/initiate-login";
The address must accept both GET and POST, and should not be frameable, so another site cannot start a sign-in the user does not see:
// In the application: an ASP.NET Core site using AddOpenIdConnect.
app.MapMethods("/initiate-login", [HttpMethods.Get, HttpMethods.Post], async (HttpRequest request) =>
{
var iss = request.HasFormContentType
? (await request.ReadFormAsync())["iss"].ToString()
: request.Query["iss"].ToString();
return iss == "https://login.example.com"
? Results.Challenge(new AuthenticationProperties { RedirectUri = "/" }, [OpenIdConnectDefaults.AuthenticationScheme])
: Results.BadRequest();
});
Registering a pre-built client
If you already have a registration built elsewhere, construct a ClientRegistration directly and use Add:
using ZeeKayDa.Auth.Clients;
var customClient = ClientRegistration.CreatePublic(
clientId: "custom-client",
redirectUris: ["https://app.example.com/callback"],
postLogoutRedirectUris: [],
allowedScopes: ["openid"])
with
{
AllowedSigningAlgorithms = new HashSet<SigningAlgorithm> { SigningAlgorithm.ES256 },
};
builder.AddInMemoryClients(clients => clients.Add(customClient));
ClientRegistration is a record, so with expressions work to override any property that was not set by the factory method.
AllowedSigningAlgorithmsmust be a subset of what the server advertises, and the server advertises only the algorithms its configured signing keys use. TheES256above therefore requires an ES256 signing key to be configured; without one, startup fails withclient.signing_algorithms.not_subset.
Multiple AddInMemoryClients calls
Multiple calls to AddInMemoryClients accumulate registrations — they do not replace earlier registrations. This is useful for separating concerns (for example, test clients from production clients, or clients from different configuration sources):
builder.AddInMemoryClients(clients =>
clients.AddPublic("spa", ["https://app.example.com/cb"], [], ["openid"]));
// Called later in a different extension method or configuration source:
builder.AddInMemoryClients(clients =>
clients.AddConfidential("api-gateway", secretValue, ["https://api.example.com/cb"], [], ["openid"]));
Both clients will be present in the repository.
Hasher selection when multiple hashers are registered
When more than one IClientSecretHasher is registered, the framework must know which one to use as the default — that is, which hasher creates new secrets and generates the timing-pad dummy credential at startup. The isDefault parameter on AddClientSecretHasher<T>() controls this. The full selection matrix is:
| Hashers registered | Explicit defaults (isDefault: true) | Outcome |
|---|---|---|
| 1 | 0 | That hasher is the default (auto-selected) |
| 2 or more | 0 | Startup failure — ambiguous, cannot select a default |
| 2 or more | 1 | The flagged hasher is the default |
| 2 or more | 2 or more | Startup failure — multiple defaults conflict |
The built-in PBKDF2 hasher is always registered and is the default, so it creates every new secret. To keep verifying secrets hashed with another algorithm, register that hasher alongside it:
auth.AddClientSecretHasher<BcryptClientSecretHasher>(); // verifies old bcrypt secrets
A host cannot yet make its own hasher the default in place of PBKDF2.
For the full isDefault rules and startup validation behaviour, see Client secrets reference. To implement a custom hasher, see Implement a custom extension point.
Startup validation
All clients are validated when the IClientRepository singleton is first resolved. Validation errors are aggregated into a single ZeeKayDaConfigurationException so you see all problems at once rather than one at a time.
Common validation failures:
| Code | Cause |
|---|---|
client.redirect_uri.fragment | Redirect URI contains a # fragment |
client.redirect_uri.scheme_http_non_loopback | http:// URI for a non-loopback host |
client.is_public.trinity_violation | IsPublic, Credentials, and AllowedTokenEndpointAuthMethods are inconsistent |
client.token_endpoint_auth_methods.not_subset | Client auth method not in server’s AuthMethodsSupported |
client.client_id.duplicate | Two clients with the same ClientId |
See also
- Implement a custom client repository — store clients in a database or other persistent store.
- Implement a custom extension point — implement other custom extension points.