TryAuthenticateAndSignIn(
+ HttpContext httpContext,
+ OpenIddictRequest request,
+ IAuthorityIdentityProviderRegistry registry,
+ IAuthorityClientStore clientStore,
+ TimeProvider timeProvider,
+ string username,
+ string password)
+ {
+ // Find a password-capable provider.
+ var providerMeta = registry.Providers.FirstOrDefault(
+ static p => p.Capabilities.SupportsPassword);
+
+ if (providerMeta is null)
+ {
+ return Results.Content(
+ BuildLoginHtml(request, "No identity provider is configured."),
+ "text/html", Encoding.UTF8);
+ }
+
+ await using var handle = await registry.AcquireAsync(
+ providerMeta.Name, httpContext.RequestAborted).ConfigureAwait(false);
+ var provider = handle.Provider;
+
+ var verification = await provider.Credentials.VerifyPasswordAsync(
+ username, password, httpContext.RequestAborted).ConfigureAwait(false);
+
+ if (!verification.Succeeded || verification.User is null)
+ {
+ return Results.Content(
+ BuildLoginHtml(request, verification.Message ?? "Invalid username or password.", username),
+ "text/html", Encoding.UTF8);
+ }
+
+ // Build ClaimsPrincipal (mirrors HandlePasswordGrantHandler pattern).
+ var identity = new ClaimsIdentity(
+ OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
+ OpenIddictConstants.Claims.Name,
+ OpenIddictConstants.Claims.Role);
+
+ identity.AddClaim(new Claim(OpenIddictConstants.Claims.Subject, verification.User.SubjectId));
+ identity.AddClaim(new Claim(OpenIddictConstants.Claims.PreferredUsername, verification.User.Username));
+
+ if (!string.IsNullOrWhiteSpace(verification.User.DisplayName))
+ {
+ identity.AddClaim(new Claim(OpenIddictConstants.Claims.Name, verification.User.DisplayName!));
+ }
+
+ foreach (var role in verification.User.Roles)
+ {
+ identity.AddClaim(new Claim(OpenIddictConstants.Claims.Role, role));
+ }
+
+ // Resolve tenant from the client document.
+ var clientId = request.ClientId;
+ if (!string.IsNullOrWhiteSpace(clientId))
+ {
+ var client = await clientStore.FindByClientIdAsync(
+ clientId!, httpContext.RequestAborted).ConfigureAwait(false);
+
+ if (client?.Properties.TryGetValue(AuthorityClientMetadataKeys.Tenant, out var tenant) == true
+ && !string.IsNullOrWhiteSpace(tenant))
+ {
+ identity.SetClaim(StellaOpsClaimTypes.Tenant, tenant.Trim().ToLowerInvariant());
+ }
+ }
+
+ var issuedAt = timeProvider.GetUtcNow();
+ identity.AddClaim(new Claim(
+ OpenIddictConstants.Claims.AuthenticationTime,
+ EpochTime.GetIntDate(issuedAt.UtcDateTime).ToString(CultureInfo.InvariantCulture),
+ ClaimValueTypes.Integer64));
+
+ identity.SetDestinations(static claim => claim.Type switch
+ {
+ OpenIddictConstants.Claims.Subject => new[]
+ {
+ OpenIddictConstants.Destinations.AccessToken,
+ OpenIddictConstants.Destinations.IdentityToken
+ },
+ OpenIddictConstants.Claims.Name => new[]
+ {
+ OpenIddictConstants.Destinations.AccessToken,
+ OpenIddictConstants.Destinations.IdentityToken
+ },
+ OpenIddictConstants.Claims.PreferredUsername => new[]
+ {
+ OpenIddictConstants.Destinations.AccessToken
+ },
+ OpenIddictConstants.Claims.Role => new[]
+ {
+ OpenIddictConstants.Destinations.AccessToken
+ },
+ _ => new[] { OpenIddictConstants.Destinations.AccessToken }
+ });
+
+ var principal = new ClaimsPrincipal(identity);
+ principal.SetScopes(request.GetScopes());
+
+ // Enrich claims via the identity provider plugin.
+ var enrichmentContext = new AuthorityClaimsEnrichmentContext(
+ provider.Context, verification.User, null);
+ await provider.ClaimsEnricher.EnrichAsync(
+ identity, enrichmentContext, httpContext.RequestAborted).ConfigureAwait(false);
+
+ // SignIn via OpenIddict — generates the authorization code and
+ // redirects the browser back to the client's redirect_uri.
+ return Results.SignIn(
+ principal,
+ properties: null,
+ OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+
+ private static string BuildErrorRedirect(
+ string redirectUri, string error, string description, string? state)
+ {
+ var separator = redirectUri.Contains('?') ? '&' : '?';
+ var sb = new StringBuilder(redirectUri);
+ sb.Append(separator);
+ sb.Append("error=").Append(Uri.EscapeDataString(error));
+ sb.Append("&error_description=").Append(Uri.EscapeDataString(description));
+ if (!string.IsNullOrWhiteSpace(state))
+ {
+ sb.Append("&state=").Append(Uri.EscapeDataString(state));
+ }
+ return sb.ToString();
+ }
+
+ private static string BuildLoginHtml(
+ OpenIddictRequest request, string? error = null, string? username = null)
+ {
+ var enc = HtmlEncoder.Default;
+
+ var sb = new StringBuilder(8192);
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("Sign In — StellaOps");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("");
+ sb.AppendLine("");
+
+ return sb.ToString();
+ }
+
+ private static void AppendHidden(StringBuilder sb, string name, string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return;
+ }
+
+ var enc = HtmlEncoder.Default;
+ sb.Append("");
+ }
+}
diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/Admin/ConsoleBrandingEndpointExtensions.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/Admin/ConsoleBrandingEndpointExtensions.cs
index 1657576f0..51f1cc41d 100644
--- a/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/Admin/ConsoleBrandingEndpointExtensions.cs
+++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/Admin/ConsoleBrandingEndpointExtensions.cs
@@ -79,15 +79,24 @@ internal static class ConsoleBrandingEndpointExtensions
// Placeholder: load from storage
var branding = GetDefaultBranding(tenantId);
- await WriteAuditAsync(
- httpContext,
- auditSink,
- timeProvider,
- "authority.console.branding.read",
- AuthEventOutcome.Success,
- null,
- BuildProperties(("tenant.id", tenantId)),
- cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await WriteAuditAsync(
+ httpContext,
+ auditSink,
+ timeProvider,
+ "authority.console.branding.read",
+ AuthEventOutcome.Success,
+ null,
+ BuildProperties(("tenant.id", tenantId)),
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ // Best-effort audit for public branding endpoint.
+ // Do not fail the request if the audit sink is unavailable
+ // (e.g. DB schema not yet initialized).
+ }
return Results.Ok(branding);
}
diff --git a/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/TenantHeaderFilter.cs b/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/TenantHeaderFilter.cs
index 040c95036..0df9cec26 100644
--- a/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/TenantHeaderFilter.cs
+++ b/src/Authority/StellaOps.Authority/StellaOps.Authority/Console/TenantHeaderFilter.cs
@@ -24,30 +24,40 @@ internal sealed class TenantHeaderFilter : IEndpointFilter
}
var tenantHeader = httpContext.Request.Headers[AuthorityHttpHeaders.Tenant];
- if (IsMissing(tenantHeader))
- {
- return ValueTask.FromResult