Initial commit: scadaproj umbrella — sister-project index, auth component normalization (design + GAPS), and the built ZB.MOM.WW.Auth shared library (0.1.0, flattened in).

This commit is contained in:
dohertj2
2026-06-01 03:59:23 -04:00
commit 37e23cf9f2
73 changed files with 6836 additions and 0 deletions
@@ -0,0 +1,53 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.Auth.Ldap;
namespace ZB.MOM.WW.Auth.AspNetCore;
/// <summary>
/// Dependency-injection helpers that wire up the ZB.MOM.WW LDAP authentication provider
/// from configuration. Composes the concrete implementation living in the
/// <c>ZB.MOM.WW.Auth.Ldap</c> package so consuming apps register a provider with a single call.
/// </summary>
/// <remarks>
/// API-key DI wiring lives in <c>ZB.MOM.WW.Auth.ApiKeys</c>
/// (<c>ZB.MOM.WW.Auth.ApiKeys.DependencyInjection.ApiKeyServiceCollectionExtensions.AddZbApiKeyAuth</c>)
/// so that an LDAP-only consumer can reference this package without pulling in SQLite.
/// </remarks>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers LDAP authentication: binds and validates <see cref="LdapOptions"/> from the
/// configuration section at <paramref name="sectionPath"/>, and registers
/// <see cref="ILdapAuthService"/>.
/// </summary>
/// <param name="services">The service collection to add to.</param>
/// <param name="config">The application configuration.</param>
/// <param name="sectionPath">Path of the configuration section holding the LDAP options.</param>
/// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
public static IServiceCollection AddZbLdapAuth(
this IServiceCollection services,
IConfiguration config,
string sectionPath)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(config);
ArgumentException.ThrowIfNullOrWhiteSpace(sectionPath);
services.Configure<LdapOptions>(config.GetSection(sectionPath));
// Fail fast at startup on a misconfigured directory rather than on first login.
services.AddSingleton<IValidateOptions<LdapOptions>, LdapOptionsValidator>();
// LdapAuthService is stateless: it holds only a snapshot of LdapOptions and a stateless
// connection factory, and opens/disposes a connection per call. It is not IDisposable.
// Singleton is correct; TryAdd mirrors the pattern in AddZbApiKeyAuth (idempotency).
services.TryAddSingleton<ILdapAuthService>(sp =>
new LdapAuthService(sp.GetRequiredService<IOptions<LdapOptions>>().Value));
return services;
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<IsPackable>true</IsPackable>
<PackageId>ZB.MOM.WW.Auth.AspNetCore</PackageId>
<Authors>ZB.MOM.WW</Authors>
<Description>ASP.NET Core DI helpers, cookie defaults, and claim mappings for the ZB.MOM.WW SCADA family.</Description>
<PackageProjectUrl>https://gitea.dohertylan.com/dohertj2/zb-mom-ww-auth</PackageProjectUrl>
<RepositoryUrl>https://gitea.dohertylan.com/dohertj2/zb-mom-ww-auth</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ZB.MOM.WW.Auth.Abstractions\ZB.MOM.WW.Auth.Abstractions.csproj" />
<ProjectReference Include="..\ZB.MOM.WW.Auth.Ldap\ZB.MOM.WW.Auth.Ldap.csproj" />
</ItemGroup>
<ItemGroup>
<!--
Microsoft.AspNetCore.App is a shared framework, not a NuGet package. It brings in
cookie authentication (Microsoft.AspNetCore.Authentication.Cookies), authorization,
and the Microsoft.Extensions.* surface (Configuration.Abstractions, Options,
DependencyInjection.Abstractions) used by the DI helpers below. There is no net10
standalone NuGet package for cookie auth, so referencing the shared framework is the
supported path.
-->
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
@@ -0,0 +1,40 @@
using System.Security.Claims;
namespace ZB.MOM.WW.Auth.AspNetCore;
/// <summary>
/// Canonical claim-type constants used across ZB.MOM.WW authentication. Centralising the
/// strings here keeps claim issuance (LDAP/API-key sign-in) and claim consumption
/// (authorization policies, role checks) in agreement on exactly one spelling per concept.
/// </summary>
/// <remarks>
/// <see cref="Name"/> and <see cref="Role"/> deliberately alias the framework's
/// <see cref="ClaimTypes.Name"/> and <see cref="ClaimTypes.Role"/> URIs so that ASP.NET
/// Core's built-in <see cref="ClaimsPrincipal.Identity"/> name resolution and
/// <c>[Authorize(Roles = ...)]</c> / <see cref="ClaimsPrincipal.IsInRole(string)"/> checks
/// work without bespoke configuration. The remaining claim types are app-specific and use
/// stable, short <c>zb:</c>-prefixed names that will not collide with the framework URIs.
/// </remarks>
public static class ZbClaimTypes
{
/// <summary>
/// The principal's name claim. Aliases <see cref="ClaimTypes.Name"/> so the framework
/// populates <see cref="System.Security.Principal.IIdentity.Name"/> from it.
/// </summary>
public const string Name = ClaimTypes.Name;
/// <summary>
/// A role claim. Aliases <see cref="ClaimTypes.Role"/> so <c>[Authorize(Roles = ...)]</c>
/// and <see cref="ClaimsPrincipal.IsInRole(string)"/> resolve against it by default.
/// </summary>
public const string Role = ClaimTypes.Role;
/// <summary>Human-friendly display name (distinct from the login <see cref="Name"/>).</summary>
public const string DisplayName = "zb:displayname";
/// <summary>The directory/login username the principal authenticated as.</summary>
public const string Username = "zb:username";
/// <summary>The identifier of the scope (site/area) the principal's roles apply within.</summary>
public const string ScopeId = "zb:scopeid";
}
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
namespace ZB.MOM.WW.Auth.AspNetCore;
/// <summary>
/// Applies the hardened cookie-authentication defaults shared by ZB.MOM.WW apps:
/// HTTP-only, <see cref="SameSiteMode.Strict"/>, sliding expiration, a caller-supplied idle
/// timeout, and a configurable HTTPS requirement.
/// </summary>
/// <remarks>
/// The cookie <em>name</em> is intentionally left untouched: each app owns its own cookie name
/// (so two apps on the same host do not clobber each other's session), and the caller sets it
/// when configuring the cookie scheme.
/// </remarks>
public static class ZbCookieDefaults
{
/// <summary>
/// Default idle timeout used when a caller does not supply one. After this much inactivity
/// the (sliding) session cookie expires and the principal must re-authenticate.
/// </summary>
public static readonly TimeSpan DefaultIdleTimeout = TimeSpan.FromMinutes(30);
/// <summary>
/// Applies the hardened defaults to <paramref name="options"/>.
/// </summary>
/// <param name="options">The cookie-authentication options to mutate.</param>
/// <param name="requireHttps">
/// When <see langword="true"/> (the default), the cookie is only ever sent over HTTPS
/// (<see cref="CookieSecurePolicy.Always"/>). Set to <see langword="false"/> only for local
/// development over plain HTTP (<see cref="CookieSecurePolicy.SameAsRequest"/>: Secure is
/// still set when the current request is HTTPS, which is safer than <c>None</c>).
/// </param>
/// <param name="idleTimeout">
/// The sliding idle timeout. Defaults to <see cref="DefaultIdleTimeout"/> when not specified.
/// </param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
public static void Apply(
CookieAuthenticationOptions options,
bool requireHttps = true,
TimeSpan? idleTimeout = null)
{
ArgumentNullException.ThrowIfNull(options);
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = requireHttps
? CookieSecurePolicy.Always
: CookieSecurePolicy.SameAsRequest;
options.SlidingExpiration = true;
options.ExpireTimeSpan = idleTimeout ?? DefaultIdleTimeout;
}
}