Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/LdapOpcUaUserAuthenticator.cs
T
Joseph Doherty 21eac21409 feat(opcua,host): F13c LDAP-bound UserName validator
Adds IOpcUaUserAuthenticator seam in OpcUaServer.Security with a deny-all
NullOpcUaUserAuthenticator default. OpcUaApplicationHost subscribes to
SessionManager.ImpersonateUser after _application.Start so UserName tokens
flow through the authenticator and either attach a UserIdentity to the
session (Allow) or set IdentityValidationError = BadIdentityTokenRejected
(Deny / authenticator exception). Anonymous + X509 tokens fall through to
SDK defaults.

LdapOpcUaUserAuthenticator (Host project) bridges to the same
ILdapAuthService that AddOtOpcUaAuth uses for Admin cookies / JWT, so a
single LDAP source-of-truth governs both Admin control plane and OPC UA
data plane. Program.cs registers LdapOptions + LdapAuthService +
IOpcUaUserAuthenticator on driver-role hosts; admin-only nodes are
unchanged.

OtOpcUaServerHostedService threads the resolved authenticator into
OpcUaApplicationHost so the seam respects Host DI.

10 new tests: 6 in OpcUaServer.Tests cover the pure HandleImpersonation
static method (success / denial / anonymous fallthrough / authenticator-
throw / null-username / Null authenticator); 4 in Host.IntegrationTests
cover the LdapOpcUaUserAuthenticator adapter (LDAP allow → Allow with
roles, LDAP deny → Deny, exception → backend-error denial, display-name
fallback). OpcUaServer suite is 40 / 40 green.

Closes #104. Unblocks Task 60 (dual-endpoint + ServiceLevel tests) once
#81 residual lands.
2026-05-26 10:21:37 -04:00

37 lines
1.5 KiB
C#

using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa;
/// <summary>
/// Production <see cref="IOpcUaUserAuthenticator"/> adapter that bridges OPC UA UserName
/// tokens to the same <see cref="ILdapAuthService"/> the Admin UI cookie/JWT flows use, so a
/// single LDAP source-of-truth governs both control-plane (Admin) and data-plane (OPC UA)
/// session identities. Roles flow through unchanged — the data-plane ACL evaluator reads
/// them off <c>OperationContext.UserIdentity</c> downstream.
/// </summary>
public sealed class LdapOpcUaUserAuthenticator(
ILdapAuthService ldap,
ILogger<LdapOpcUaUserAuthenticator> logger)
: IOpcUaUserAuthenticator
{
public async Task<OpcUaUserAuthResult> AuthenticateUserNameAsync(string username, string password, CancellationToken ct)
{
try
{
var result = await ldap.AuthenticateAsync(username, password, ct).ConfigureAwait(false);
if (!result.Success)
{
return OpcUaUserAuthResult.Deny(result.Error ?? "Invalid credentials");
}
return OpcUaUserAuthResult.Allow(result.DisplayName ?? username, result.Roles);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogWarning(ex, "LDAP authentication threw for OPC UA user {User}", username);
return OpcUaUserAuthResult.Deny("Authentication backend error");
}
}
}