Initial commit (history squashed)
This commit is contained in:
		@@ -0,0 +1,12 @@
 | 
			
		||||
namespace StellaOps.Authority.Plugins.Abstractions;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Well-known metadata keys persisted with Authority client registrations.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class AuthorityClientMetadataKeys
 | 
			
		||||
{
 | 
			
		||||
    public const string AllowedGrantTypes = "allowedGrantTypes";
 | 
			
		||||
    public const string AllowedScopes = "allowedScopes";
 | 
			
		||||
    public const string RedirectUris = "redirectUris";
 | 
			
		||||
    public const string PostLogoutRedirectUris = "postLogoutRedirectUris";
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,139 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Authority.Plugins.Abstractions;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Well-known Authority plugin capability identifiers.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class AuthorityPluginCapabilities
 | 
			
		||||
{
 | 
			
		||||
    public const string Password = "password";
 | 
			
		||||
    public const string Bootstrap = "bootstrap";
 | 
			
		||||
    public const string Mfa = "mfa";
 | 
			
		||||
    public const string ClientProvisioning = "clientProvisioning";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Immutable description of an Authority plugin loaded from configuration.
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <param name="Name">Logical name derived from configuration key.</param>
 | 
			
		||||
/// <param name="Type">Plugin type identifier (used for capability routing).</param>
 | 
			
		||||
/// <param name="Enabled">Whether the plugin is enabled.</param>
 | 
			
		||||
/// <param name="AssemblyName">Assembly name without extension.</param>
 | 
			
		||||
/// <param name="AssemblyPath">Explicit assembly path override.</param>
 | 
			
		||||
/// <param name="Capabilities">Capability hints exposed by the plugin.</param>
 | 
			
		||||
/// <param name="Metadata">Additional metadata forwarded to plugin implementations.</param>
 | 
			
		||||
/// <param name="ConfigPath">Absolute path to the plugin configuration manifest.</param>
 | 
			
		||||
public sealed record AuthorityPluginManifest(
 | 
			
		||||
    string Name,
 | 
			
		||||
    string Type,
 | 
			
		||||
    bool Enabled,
 | 
			
		||||
    string? AssemblyName,
 | 
			
		||||
    string? AssemblyPath,
 | 
			
		||||
    IReadOnlyList<string> Capabilities,
 | 
			
		||||
    IReadOnlyDictionary<string, string?> Metadata,
 | 
			
		||||
    string ConfigPath)
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Determines whether the manifest declares the specified capability.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="capability">Capability identifier to check.</param>
 | 
			
		||||
    public bool HasCapability(string capability)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(capability))
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var entry in Capabilities)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.Equals(entry, capability, StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
            {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Runtime context combining plugin manifest metadata and its bound configuration.
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <param name="Manifest">Manifest describing the plugin.</param>
 | 
			
		||||
/// <param name="Configuration">Root configuration built from the plugin YAML manifest.</param>
 | 
			
		||||
public sealed record AuthorityPluginContext(
 | 
			
		||||
    AuthorityPluginManifest Manifest,
 | 
			
		||||
    IConfiguration Configuration);
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Registry exposing the set of Authority plugins loaded at runtime.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IAuthorityPluginRegistry
 | 
			
		||||
{
 | 
			
		||||
    IReadOnlyCollection<AuthorityPluginContext> Plugins { get; }
 | 
			
		||||
 | 
			
		||||
    bool TryGet(string name, [NotNullWhen(true)] out AuthorityPluginContext? context);
 | 
			
		||||
 | 
			
		||||
    AuthorityPluginContext GetRequired(string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (TryGet(name, out var context))
 | 
			
		||||
        {
 | 
			
		||||
            return context;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new KeyNotFoundException($"Authority plugin '{name}' is not registered.");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Registry exposing loaded identity provider plugins and their capabilities.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IAuthorityIdentityProviderRegistry
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets all registered identity provider plugins keyed by logical name.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IReadOnlyCollection<IIdentityProviderPlugin> Providers { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets identity providers that advertise password support.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IReadOnlyCollection<IIdentityProviderPlugin> PasswordProviders { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets identity providers that advertise multi-factor authentication support.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IReadOnlyCollection<IIdentityProviderPlugin> MfaProviders { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets identity providers that advertise client provisioning support.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IReadOnlyCollection<IIdentityProviderPlugin> ClientProvisioningProviders { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Aggregate capability flags across all registered providers.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    AuthorityIdentityProviderCapabilities AggregateCapabilities { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Attempts to resolve an identity provider by name.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    bool TryGet(string name, [NotNullWhen(true)] out IIdentityProviderPlugin? provider);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Resolves an identity provider by name or throws when not found.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IIdentityProviderPlugin GetRequired(string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (TryGet(name, out var provider))
 | 
			
		||||
        {
 | 
			
		||||
            return provider;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new KeyNotFoundException($"Identity provider plugin '{name}' is not registered.");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.Extensions.Configuration;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Authority.Plugins.Abstractions;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Provides shared services and metadata to Authority plugin registrars during DI setup.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed class AuthorityPluginRegistrationContext
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initialises a new registration context.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="services">Service collection used to register plugin services.</param>
 | 
			
		||||
    /// <param name="plugin">Plugin context describing the manifest and configuration.</param>
 | 
			
		||||
    /// <param name="hostConfiguration">Root host configuration available during registration.</param>
 | 
			
		||||
    /// <exception cref="ArgumentNullException">Thrown when any argument is null.</exception>
 | 
			
		||||
    public AuthorityPluginRegistrationContext(
 | 
			
		||||
        IServiceCollection services,
 | 
			
		||||
        AuthorityPluginContext plugin,
 | 
			
		||||
        IConfiguration hostConfiguration)
 | 
			
		||||
    {
 | 
			
		||||
        Services = services ?? throw new ArgumentNullException(nameof(services));
 | 
			
		||||
        Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
 | 
			
		||||
        HostConfiguration = hostConfiguration ?? throw new ArgumentNullException(nameof(hostConfiguration));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the service collection used to register plugin dependencies.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IServiceCollection Services { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the plugin context containing manifest metadata and configuration.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityPluginContext Plugin { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the root configuration associated with the Authority host.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IConfiguration HostConfiguration { get; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Registers Authority plugin services for a specific plugin type.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IAuthorityPluginRegistrar
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Logical plugin type identifier supported by this registrar (e.g. <c>standard</c>, <c>ldap</c>).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    string PluginType { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Registers services for the supplied plugin context.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="context">Registration context containing services and metadata.</param>
 | 
			
		||||
    void Register(AuthorityPluginRegistrationContext context);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
using System.Security.Cryptography;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Authority.Plugins.Abstractions;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Deterministic hashing utilities for secrets managed by Authority plugins.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public static class AuthoritySecretHasher
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Computes a stable SHA-256 hash for the provided secret.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static string ComputeHash(string secret)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrEmpty(secret))
 | 
			
		||||
        {
 | 
			
		||||
            return string.Empty;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        using var sha256 = SHA256.Create();
 | 
			
		||||
        var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(secret));
 | 
			
		||||
        return Convert.ToBase64String(bytes);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,785 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace StellaOps.Authority.Plugins.Abstractions;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Describes feature support advertised by an identity provider plugin.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityIdentityProviderCapabilities(
 | 
			
		||||
    bool SupportsPassword,
 | 
			
		||||
    bool SupportsMfa,
 | 
			
		||||
    bool SupportsClientProvisioning)
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Builds capabilities metadata from a list of capability identifiers.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityIdentityProviderCapabilities FromCapabilities(IEnumerable<string> capabilities)
 | 
			
		||||
    {
 | 
			
		||||
        if (capabilities is null)
 | 
			
		||||
        {
 | 
			
		||||
            return new AuthorityIdentityProviderCapabilities(false, false, false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
        foreach (var entry in capabilities)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(entry))
 | 
			
		||||
            {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            seen.Add(entry.Trim());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new AuthorityIdentityProviderCapabilities(
 | 
			
		||||
            SupportsPassword: seen.Contains(AuthorityPluginCapabilities.Password),
 | 
			
		||||
            SupportsMfa: seen.Contains(AuthorityPluginCapabilities.Mfa),
 | 
			
		||||
            SupportsClientProvisioning: seen.Contains(AuthorityPluginCapabilities.ClientProvisioning));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Represents a loaded Authority identity provider plugin instance.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IIdentityProviderPlugin
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the logical name of the plugin instance (matches the manifest key).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    string Name { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the plugin type identifier (e.g. <c>standard</c>, <c>ldap</c>).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    string Type { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the plugin context comprising the manifest and bound configuration.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    AuthorityPluginContext Context { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the credential store responsible for authenticator validation and user provisioning.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IUserCredentialStore Credentials { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the claims enricher applied to issued principals.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IClaimsEnricher ClaimsEnricher { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the optional client provisioning store exposed by the plugin.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    IClientProvisioningStore? ClientProvisioning { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the capability metadata advertised by the plugin.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    AuthorityIdentityProviderCapabilities Capabilities { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Evaluates the health of the plugin and backing data stores.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="cancellationToken">Token used to cancel the operation.</param>
 | 
			
		||||
    /// <returns>Health result describing the plugin status.</returns>
 | 
			
		||||
    ValueTask<AuthorityPluginHealthResult> CheckHealthAsync(CancellationToken cancellationToken);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Supplies operations for validating credentials and managing user records.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IUserCredentialStore
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Verifies the supplied username/password combination.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(
 | 
			
		||||
        string username,
 | 
			
		||||
        string password,
 | 
			
		||||
        CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates or updates a user record based on the supplied registration data.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(
 | 
			
		||||
        AuthorityUserRegistration registration,
 | 
			
		||||
        CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Attempts to resolve a user descriptor by its canonical subject identifier.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ValueTask<AuthorityUserDescriptor?> FindBySubjectAsync(
 | 
			
		||||
        string subjectId,
 | 
			
		||||
        CancellationToken cancellationToken);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Enriches issued principals with additional claims based on plugin-specific rules.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IClaimsEnricher
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Adds or adjusts claims on the provided identity.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ValueTask EnrichAsync(
 | 
			
		||||
        ClaimsIdentity identity,
 | 
			
		||||
        AuthorityClaimsEnrichmentContext context,
 | 
			
		||||
        CancellationToken cancellationToken);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Manages client (machine-to-machine) provisioning for Authority.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IClientProvisioningStore
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates or updates a client registration.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ValueTask<AuthorityPluginOperationResult<AuthorityClientDescriptor>> CreateOrUpdateAsync(
 | 
			
		||||
        AuthorityClientRegistration registration,
 | 
			
		||||
        CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Attempts to resolve a client descriptor by its identifier.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ValueTask<AuthorityClientDescriptor?> FindByClientIdAsync(
 | 
			
		||||
        string clientId,
 | 
			
		||||
        CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Removes a client registration.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    ValueTask<AuthorityPluginOperationResult> DeleteAsync(
 | 
			
		||||
        string clientId,
 | 
			
		||||
        CancellationToken cancellationToken);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Represents the health state of a plugin or backing store.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public enum AuthorityPluginHealthStatus
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Plugin is healthy and operational.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Healthy,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Plugin is degraded but still usable (e.g. transient connectivity issues).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Degraded,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Plugin is unavailable and cannot service requests.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Unavailable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Result of a plugin health probe.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityPluginHealthResult
 | 
			
		||||
{
 | 
			
		||||
    private AuthorityPluginHealthResult(
 | 
			
		||||
        AuthorityPluginHealthStatus status,
 | 
			
		||||
        string? message,
 | 
			
		||||
        IReadOnlyDictionary<string, string?> details)
 | 
			
		||||
    {
 | 
			
		||||
        Status = status;
 | 
			
		||||
        Message = message;
 | 
			
		||||
        Details = details;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the overall status of the plugin.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityPluginHealthStatus Status { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets an optional human-readable status description.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Message { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets optional structured details for diagnostics.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyDictionary<string, string?> Details { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates a healthy result.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginHealthResult Healthy(
 | 
			
		||||
        string? message = null,
 | 
			
		||||
        IReadOnlyDictionary<string, string?>? details = null)
 | 
			
		||||
        => new(AuthorityPluginHealthStatus.Healthy, message, details ?? EmptyDetails);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates a degraded result.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginHealthResult Degraded(
 | 
			
		||||
        string? message = null,
 | 
			
		||||
        IReadOnlyDictionary<string, string?>? details = null)
 | 
			
		||||
        => new(AuthorityPluginHealthStatus.Degraded, message, details ?? EmptyDetails);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates an unavailable result.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginHealthResult Unavailable(
 | 
			
		||||
        string? message = null,
 | 
			
		||||
        IReadOnlyDictionary<string, string?>? details = null)
 | 
			
		||||
        => new(AuthorityPluginHealthStatus.Unavailable, message, details ?? EmptyDetails);
 | 
			
		||||
 | 
			
		||||
    private static readonly IReadOnlyDictionary<string, string?> EmptyDetails =
 | 
			
		||||
        new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Describes a canonical Authority user surfaced by a plugin.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityUserDescriptor
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initialises a new user descriptor.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityUserDescriptor(
 | 
			
		||||
        string subjectId,
 | 
			
		||||
        string username,
 | 
			
		||||
        string? displayName,
 | 
			
		||||
        bool requiresPasswordReset,
 | 
			
		||||
        IReadOnlyCollection<string>? roles = null,
 | 
			
		||||
        IReadOnlyDictionary<string, string?>? attributes = null)
 | 
			
		||||
    {
 | 
			
		||||
        SubjectId = ValidateRequired(subjectId, nameof(subjectId));
 | 
			
		||||
        Username = ValidateRequired(username, nameof(username));
 | 
			
		||||
        DisplayName = displayName;
 | 
			
		||||
        RequiresPasswordReset = requiresPasswordReset;
 | 
			
		||||
        Roles = roles is null ? Array.Empty<string>() : roles.ToArray();
 | 
			
		||||
        Attributes = attributes is null
 | 
			
		||||
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
 | 
			
		||||
            : new Dictionary<string, string?>(attributes, StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Stable subject identifier for token issuance.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string SubjectId { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Canonical username (case-normalised).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Username { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional human-friendly display name.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? DisplayName { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Indicates whether the user must reset their password.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool RequiresPasswordReset { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Collection of role identifiers associated with the user.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<string> Roles { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Arbitrary plugin-defined attributes (used by claims enricher).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyDictionary<string, string?> Attributes { get; }
 | 
			
		||||
 | 
			
		||||
    private static string ValidateRequired(string value, string paramName)
 | 
			
		||||
        => string.IsNullOrWhiteSpace(value)
 | 
			
		||||
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
 | 
			
		||||
            : value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Outcome of a credential verification attempt.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityCredentialVerificationResult
 | 
			
		||||
{
 | 
			
		||||
    private AuthorityCredentialVerificationResult(
 | 
			
		||||
        bool succeeded,
 | 
			
		||||
        AuthorityUserDescriptor? user,
 | 
			
		||||
        AuthorityCredentialFailureCode? failureCode,
 | 
			
		||||
        string? message,
 | 
			
		||||
        TimeSpan? retryAfter)
 | 
			
		||||
    {
 | 
			
		||||
        Succeeded = succeeded;
 | 
			
		||||
        User = user;
 | 
			
		||||
        FailureCode = failureCode;
 | 
			
		||||
        Message = message;
 | 
			
		||||
        RetryAfter = retryAfter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Indicates whether the verification succeeded.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Succeeded { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Resolved user descriptor when successful.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityUserDescriptor? User { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Failure classification when unsuccessful.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityCredentialFailureCode? FailureCode { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional message describing the outcome.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Message { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional suggested retry interval (e.g. for lockouts).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public TimeSpan? RetryAfter { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Builds a successful verification result.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityCredentialVerificationResult Success(
 | 
			
		||||
        AuthorityUserDescriptor user,
 | 
			
		||||
        string? message = null)
 | 
			
		||||
        => new(true, user ?? throw new ArgumentNullException(nameof(user)), null, message, null);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Builds a failed verification result.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityCredentialVerificationResult Failure(
 | 
			
		||||
        AuthorityCredentialFailureCode failureCode,
 | 
			
		||||
        string? message = null,
 | 
			
		||||
        TimeSpan? retryAfter = null)
 | 
			
		||||
        => new(false, null, failureCode, message, retryAfter);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Classifies credential verification failures.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public enum AuthorityCredentialFailureCode
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Username/password combination is invalid.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    InvalidCredentials,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Account is locked out (retry after a specified duration).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    LockedOut,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Password has expired and must be reset.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    PasswordExpired,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// User must reset password before proceeding.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    RequiresPasswordReset,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Additional multi-factor authentication is required.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    RequiresMfa,
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Unexpected failure occurred (see message for details).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    UnknownError
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Represents a user provisioning request.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityUserRegistration
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initialises a new registration.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityUserRegistration(
 | 
			
		||||
        string username,
 | 
			
		||||
        string? password,
 | 
			
		||||
        string? displayName,
 | 
			
		||||
        string? email,
 | 
			
		||||
        bool requirePasswordReset,
 | 
			
		||||
        IReadOnlyCollection<string>? roles = null,
 | 
			
		||||
        IReadOnlyDictionary<string, string?>? attributes = null)
 | 
			
		||||
    {
 | 
			
		||||
        Username = ValidateRequired(username, nameof(username));
 | 
			
		||||
        Password = password;
 | 
			
		||||
        DisplayName = displayName;
 | 
			
		||||
        Email = email;
 | 
			
		||||
        RequirePasswordReset = requirePasswordReset;
 | 
			
		||||
        Roles = roles is null ? Array.Empty<string>() : roles.ToArray();
 | 
			
		||||
        Attributes = attributes is null
 | 
			
		||||
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
 | 
			
		||||
            : new Dictionary<string, string?>(attributes, StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Canonical username (unique).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string Username { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional raw password (hashed by plugin).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Password { get; init; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional human-friendly display name.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? DisplayName { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional contact email.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Email { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Indicates whether the user must reset their password at next login.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool RequirePasswordReset { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Associated roles.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<string> Roles { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Plugin-defined attributes.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyDictionary<string, string?> Attributes { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates a copy with the provided password while preserving other fields.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityUserRegistration WithPassword(string? password)
 | 
			
		||||
        => new(Username, password, DisplayName, Email, RequirePasswordReset, Roles, Attributes);
 | 
			
		||||
 | 
			
		||||
    private static string ValidateRequired(string value, string paramName)
 | 
			
		||||
        => string.IsNullOrWhiteSpace(value)
 | 
			
		||||
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
 | 
			
		||||
            : value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Generic operation result utilised by plugins.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityPluginOperationResult
 | 
			
		||||
{
 | 
			
		||||
    private AuthorityPluginOperationResult(bool succeeded, string? errorCode, string? message)
 | 
			
		||||
    {
 | 
			
		||||
        Succeeded = succeeded;
 | 
			
		||||
        ErrorCode = errorCode;
 | 
			
		||||
        Message = message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Indicates whether the operation succeeded.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Succeeded { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Machine-readable error code (populated on failure).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? ErrorCode { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional human-readable message.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Message { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns a successful result.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginOperationResult Success(string? message = null)
 | 
			
		||||
        => new(true, null, message);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns a failed result with the supplied error code.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginOperationResult Failure(string errorCode, string? message = null)
 | 
			
		||||
        => new(false, ValidateErrorCode(errorCode), message);
 | 
			
		||||
 | 
			
		||||
    internal static string ValidateErrorCode(string errorCode)
 | 
			
		||||
        => string.IsNullOrWhiteSpace(errorCode)
 | 
			
		||||
            ? throw new ArgumentException("Error code is required for failures.", nameof(errorCode))
 | 
			
		||||
            : errorCode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Generic operation result that returns a value.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityPluginOperationResult<TValue>
 | 
			
		||||
{
 | 
			
		||||
    private AuthorityPluginOperationResult(
 | 
			
		||||
        bool succeeded,
 | 
			
		||||
        TValue? value,
 | 
			
		||||
        string? errorCode,
 | 
			
		||||
        string? message)
 | 
			
		||||
    {
 | 
			
		||||
        Succeeded = succeeded;
 | 
			
		||||
        Value = value;
 | 
			
		||||
        ErrorCode = errorCode;
 | 
			
		||||
        Message = message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Indicates whether the operation succeeded.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Succeeded { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returned value when successful.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public TValue? Value { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Machine-readable error code (on failure).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? ErrorCode { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional human-readable message.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? Message { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns a successful result with the provided value.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginOperationResult<TValue> Success(TValue value, string? message = null)
 | 
			
		||||
        => new(true, value, null, message);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns a successful result without a value (defaults to <c>default</c>).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginOperationResult<TValue> Success(string? message = null)
 | 
			
		||||
        => new(true, default, null, message);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Returns a failed result with the supplied error code.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static AuthorityPluginOperationResult<TValue> Failure(string errorCode, string? message = null)
 | 
			
		||||
        => new(false, default, AuthorityPluginOperationResult.ValidateErrorCode(errorCode), message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Context supplied to claims enrichment routines.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed class AuthorityClaimsEnrichmentContext
 | 
			
		||||
{
 | 
			
		||||
    private readonly Dictionary<string, object?> items;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initialises a new context instance.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityClaimsEnrichmentContext(
 | 
			
		||||
        AuthorityPluginContext plugin,
 | 
			
		||||
        AuthorityUserDescriptor? user,
 | 
			
		||||
        AuthorityClientDescriptor? client)
 | 
			
		||||
    {
 | 
			
		||||
        Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
 | 
			
		||||
        User = user;
 | 
			
		||||
        Client = client;
 | 
			
		||||
        items = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the plugin context associated with the principal.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityPluginContext Plugin { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the user descriptor when available.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityUserDescriptor? User { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets the client descriptor when available.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityClientDescriptor? Client { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Extensible bag for plugin-specific data passed between enrichment stages.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IDictionary<string, object?> Items => items;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Represents a registered OAuth/OpenID client.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityClientDescriptor
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initialises a new client descriptor.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityClientDescriptor(
 | 
			
		||||
        string clientId,
 | 
			
		||||
        string? displayName,
 | 
			
		||||
        bool confidential,
 | 
			
		||||
        IReadOnlyCollection<string>? allowedGrantTypes = null,
 | 
			
		||||
        IReadOnlyCollection<string>? allowedScopes = null,
 | 
			
		||||
        IReadOnlyCollection<Uri>? redirectUris = null,
 | 
			
		||||
        IReadOnlyCollection<Uri>? postLogoutRedirectUris = null,
 | 
			
		||||
        IReadOnlyDictionary<string, string?>? properties = null)
 | 
			
		||||
    {
 | 
			
		||||
        ClientId = ValidateRequired(clientId, nameof(clientId));
 | 
			
		||||
        DisplayName = displayName;
 | 
			
		||||
        Confidential = confidential;
 | 
			
		||||
        AllowedGrantTypes = allowedGrantTypes is null ? Array.Empty<string>() : allowedGrantTypes.ToArray();
 | 
			
		||||
        AllowedScopes = allowedScopes is null ? Array.Empty<string>() : allowedScopes.ToArray();
 | 
			
		||||
        RedirectUris = redirectUris is null ? Array.Empty<Uri>() : redirectUris.ToArray();
 | 
			
		||||
        PostLogoutRedirectUris = postLogoutRedirectUris is null ? Array.Empty<Uri>() : postLogoutRedirectUris.ToArray();
 | 
			
		||||
        Properties = properties is null
 | 
			
		||||
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
 | 
			
		||||
            : new Dictionary<string, string?>(properties, StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Unique client identifier.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ClientId { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional display name.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? DisplayName { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Indicates whether the client is confidential (requires secret).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Confidential { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Permitted OAuth grant types.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<string> AllowedGrantTypes { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Permitted scopes.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<string> AllowedScopes { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Registered redirect URIs.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<Uri> RedirectUris { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Registered post-logout redirect URIs.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<Uri> PostLogoutRedirectUris { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Additional plugin-defined metadata.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyDictionary<string, string?> Properties { get; }
 | 
			
		||||
 | 
			
		||||
    private static string ValidateRequired(string value, string paramName)
 | 
			
		||||
        => string.IsNullOrWhiteSpace(value)
 | 
			
		||||
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
 | 
			
		||||
            : value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Client registration payload used when provisioning clients through plugins.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public sealed record AuthorityClientRegistration
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Initialises a new registration.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityClientRegistration(
 | 
			
		||||
        string clientId,
 | 
			
		||||
        bool confidential,
 | 
			
		||||
        string? displayName,
 | 
			
		||||
        string? clientSecret,
 | 
			
		||||
        IReadOnlyCollection<string>? allowedGrantTypes = null,
 | 
			
		||||
        IReadOnlyCollection<string>? allowedScopes = null,
 | 
			
		||||
        IReadOnlyCollection<Uri>? redirectUris = null,
 | 
			
		||||
        IReadOnlyCollection<Uri>? postLogoutRedirectUris = null,
 | 
			
		||||
        IReadOnlyDictionary<string, string?>? properties = null)
 | 
			
		||||
    {
 | 
			
		||||
        ClientId = ValidateRequired(clientId, nameof(clientId));
 | 
			
		||||
        Confidential = confidential;
 | 
			
		||||
        DisplayName = displayName;
 | 
			
		||||
        ClientSecret = confidential
 | 
			
		||||
            ? ValidateRequired(clientSecret ?? string.Empty, nameof(clientSecret))
 | 
			
		||||
            : clientSecret;
 | 
			
		||||
        AllowedGrantTypes = allowedGrantTypes is null ? Array.Empty<string>() : allowedGrantTypes.ToArray();
 | 
			
		||||
        AllowedScopes = allowedScopes is null ? Array.Empty<string>() : allowedScopes.ToArray();
 | 
			
		||||
        RedirectUris = redirectUris is null ? Array.Empty<Uri>() : redirectUris.ToArray();
 | 
			
		||||
        PostLogoutRedirectUris = postLogoutRedirectUris is null ? Array.Empty<Uri>() : postLogoutRedirectUris.ToArray();
 | 
			
		||||
        Properties = properties is null
 | 
			
		||||
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
 | 
			
		||||
            : new Dictionary<string, string?>(properties, StringComparer.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Unique client identifier.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string ClientId { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Indicates whether the client is confidential (requires secret handling).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool Confidential { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional display name.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? DisplayName { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Optional raw client secret (hashed by the plugin for storage).
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? ClientSecret { get; init; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Grant types to enable.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<string> AllowedGrantTypes { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Scopes assigned to the client.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<string> AllowedScopes { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Redirect URIs permitted for the client.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<Uri> RedirectUris { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Post-logout redirect URIs.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyCollection<Uri> PostLogoutRedirectUris { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Additional metadata for the plugin.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public IReadOnlyDictionary<string, string?> Properties { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates a copy of the registration with the provided client secret.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public AuthorityClientRegistration WithClientSecret(string? clientSecret)
 | 
			
		||||
        => new(ClientId, Confidential, DisplayName, clientSecret, AllowedGrantTypes, AllowedScopes, RedirectUris, PostLogoutRedirectUris, Properties);
 | 
			
		||||
 | 
			
		||||
    private static string ValidateRequired(string value, string paramName)
 | 
			
		||||
        => string.IsNullOrWhiteSpace(value)
 | 
			
		||||
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
 | 
			
		||||
            : value;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>net10.0</TargetFramework>
 | 
			
		||||
    <LangVersion>preview</LangVersion>
 | 
			
		||||
    <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
    <Nullable>enable</Nullable>
 | 
			
		||||
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
 | 
			
		||||
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
		Reference in New Issue
	
	Block a user