diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/AuditActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/AuditActor.cs
new file mode 100644
index 00000000..00781b8f
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/AuditActor.cs
@@ -0,0 +1,50 @@
+namespace ZB.MOM.WW.OtOpcUa.Security.Audit;
+
+///
+/// Default-resolution helpers for the Actor field of a canonical
+/// ZB.MOM.WW.Audit.AuditEvent.
+///
+///
+///
+/// Usage pattern — call when constructing an AuditEvent:
+///
+/// new AuditEvent
+/// {
+/// Actor = AuditActor.Resolve(auditActorAccessor),
+/// ...
+/// }
+///
+///
+///
+/// Note: OtOpcUa has no live structured AuditEvent emit sites as of Phase 3
+/// (all production audit flows through the bespoke stored-procedure path). This helper is
+/// forward-looking — it is tested and ready so that future emit sites pick up the correct
+/// Actor automatically.
+///
+///
+public static class AuditActor
+{
+ /// The fallback actor string used when no authenticated principal is available.
+ public const string SystemFallback = "system";
+
+ ///
+ /// Returns the current principal's actor string from , or
+ /// when the accessor returns
+ /// (no HTTP context, unauthenticated, or in a background/non-HTTP execution context).
+ ///
+ /// The audit-actor accessor. May be
+ /// (e.g. in a background context where DI did not wire the accessor).
+ /// The actor string — never .
+ public static string Resolve(IAuditActorAccessor? accessor) =>
+ Resolve(accessor, SystemFallback);
+
+ ///
+ /// Returns the current principal's actor string from , or
+ /// when the accessor returns .
+ ///
+ /// The audit-actor accessor. May be .
+ /// The explicit fallback value.
+ /// The actor string — never .
+ public static string Resolve(IAuditActorAccessor? accessor, string fallback) =>
+ accessor?.CurrentActor ?? fallback;
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/HttpAuditActorAccessor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/HttpAuditActorAccessor.cs
new file mode 100644
index 00000000..9a86339f
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/HttpAuditActorAccessor.cs
@@ -0,0 +1,53 @@
+using Microsoft.AspNetCore.Http;
+using ZB.MOM.WW.Auth.AspNetCore;
+
+namespace ZB.MOM.WW.OtOpcUa.Security.Audit;
+
+///
+/// HTTP-context–backed for the OtOpcUa control-plane.
+///
+///
+/// Reads the authenticated principal from :
+///
+/// - If there is no current HttpContext or the user is not authenticated,
+/// returns .
+/// - Otherwise, returns the claim value (the
+/// canonical directory login name set at sign-in by AuthEndpoints).
+/// - Falls back to the claim, then to
+/// , in that order.
+///
+///
+/// Registered as scoped in
+/// so that it correctly follows the request scope used by Blazor Server interactive components
+/// and minimal-API endpoints. IHttpContextAccessor is registered by
+/// AddOtOpcUaAuth via services.AddHttpContextAccessor().
+///
+///
+public sealed class HttpAuditActorAccessor : IAuditActorAccessor
+{
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ /// Initializes the accessor with the ASP.NET Core HTTP context accessor.
+ /// The HTTP context accessor.
+ public HttpAuditActorAccessor(IHttpContextAccessor httpContextAccessor)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ ///
+ public string? CurrentActor
+ {
+ get
+ {
+ var user = _httpContextAccessor.HttpContext?.User;
+ if (user?.Identity?.IsAuthenticated != true)
+ return null;
+
+ // Prefer the canonical login-name claim; fall back to the Name claim or
+ // Identity.Name (both of which map to ClaimTypes.Name / ZbClaimTypes.Name).
+ return user.FindFirst(ZbClaimTypes.Username)?.Value
+ ?? user.FindFirst(ZbClaimTypes.Name)?.Value
+ ?? user.Identity.Name;
+ }
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/IAuditActorAccessor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/IAuditActorAccessor.cs
new file mode 100644
index 00000000..ad9f081c
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Audit/IAuditActorAccessor.cs
@@ -0,0 +1,30 @@
+namespace ZB.MOM.WW.OtOpcUa.Security.Audit;
+
+///
+/// Resolves the current HTTP principal's actor string for inclusion in a canonical
+/// ZB.MOM.WW.Audit.AuditEvent as the Actor field.
+///
+///
+/// The seam abstracts the identity source so that:
+///
+/// - production code uses (reads the
+/// authenticated Blazor cookie principal from IHttpContextAccessor); and
+/// - unit tests or non-HTTP contexts can substitute a stub or return
+/// (which triggers the "system" fallback in
+/// ).
+///
+///
+/// Note: OtOpcUa has no live structured AuditEvent emit sites as of Phase 3
+/// (all production audit flows through the bespoke stored-procedure path). This seam is
+/// forward-looking — wired and tested so that future emit sites can call
+/// and get the Auth principal automatically.
+///
+///
+public interface IAuditActorAccessor
+{
+ ///
+ /// Returns the authenticated principal's actor string, or when
+ /// there is no current HTTP context or the user is not authenticated.
+ ///
+ string? CurrentActor { get; }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs
index 953a286b..f62b7e5f 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.OtOpcUa.Configuration;
+using ZB.MOM.WW.OtOpcUa.Security.Audit;
using ZB.MOM.WW.OtOpcUa.Security.Jwt;
using ZB.MOM.WW.OtOpcUa.Security.Ldap;
@@ -37,6 +38,19 @@ public static class ServiceCollectionExtensions
services.AddOptions().Bind(configuration.GetSection(LdapOptions.SectionName));
services.AddSingleton();
+
+ // IHttpContextAccessor is not registered by default — call AddHttpContextAccessor()
+ // so HttpAuditActorAccessor and any Blazor/minimal-API component that reads the current
+ // HTTP context by injection can resolve it. AddHttpContextAccessor is idempotent (internal
+ // TryAdd), so calling it here is safe even if the host also calls it elsewhere.
+ services.AddHttpContextAccessor();
+
+ // IAuditActorAccessor — resolves the authenticated HTTP principal's actor string for use
+ // as the Actor field when constructing a canonical ZB.MOM.WW.Audit.AuditEvent. Registered
+ // Scoped so it correctly follows the request scope used by Blazor Server and minimal-API
+ // endpoints.
+ services.TryAddScoped();
+
// Singleton — OtOpcUaLdapAuthService is stateless (the shared-library directory client it
// wraps opens/disposes an LdapConnection per call) and must be consumable by the Singleton
// LdapOpcUaUserAuthenticator on driver-role nodes. This is the app's ILdapAuthService: it
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/Audit/AuditActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/Audit/AuditActorTests.cs
new file mode 100644
index 00000000..19f8a09a
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/Audit/AuditActorTests.cs
@@ -0,0 +1,81 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Security.Audit;
+
+namespace ZB.MOM.WW.OtOpcUa.Security.Tests.Audit;
+
+///
+/// Unit tests for — the static resolution helper that sources the
+/// Actor field of a canonical ZB.MOM.WW.Audit.AuditEvent from the current
+/// HTTP principal and falls back to a configurable value when no principal is available.
+///
+public sealed class AuditActorTests
+{
+ ///
+ /// returns the accessor's value
+ /// when the accessor returns a non-null string.
+ ///
+ [Fact]
+ public void Resolve_returns_accessor_value_when_present()
+ {
+ var accessor = new StubAccessor("alice");
+
+ AuditActor.Resolve(accessor).ShouldBe("alice");
+ }
+
+ ///
+ /// returns
+ /// when the accessor returns null
+ /// (unauthenticated / no HTTP context).
+ ///
+ [Fact]
+ public void Resolve_returns_system_fallback_when_accessor_returns_null()
+ {
+ var accessor = new StubAccessor(null);
+
+ AuditActor.Resolve(accessor).ShouldBe(AuditActor.SystemFallback);
+ }
+
+ ///
+ /// returns
+ /// when the accessor reference itself is null
+ /// (e.g. in a background/non-HTTP context where DI did not inject the accessor).
+ ///
+ [Fact]
+ public void Resolve_returns_system_fallback_when_accessor_is_null()
+ {
+ AuditActor.Resolve(null).ShouldBe(AuditActor.SystemFallback);
+ }
+
+ ///
+ /// uses the explicit
+ /// fallback string rather than when the accessor
+ /// returns null.
+ ///
+ [Fact]
+ public void Resolve_uses_explicit_fallback_when_accessor_returns_null()
+ {
+ var accessor = new StubAccessor(null);
+
+ AuditActor.Resolve(accessor, "scheduler").ShouldBe("scheduler");
+ }
+
+ ///
+ /// prefers the accessor's
+ /// value over the explicit fallback when the accessor returns a non-null string.
+ ///
+ [Fact]
+ public void Resolve_prefers_accessor_value_over_explicit_fallback()
+ {
+ var accessor = new StubAccessor("bob");
+
+ AuditActor.Resolve(accessor, "scheduler").ShouldBe("bob");
+ }
+
+ // ── stub ──────────────────────────────────────────────────────────────────────
+
+ private sealed class StubAccessor(string? value) : IAuditActorAccessor
+ {
+ public string? CurrentActor { get; } = value;
+ }
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/Audit/HttpAuditActorAccessorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/Audit/HttpAuditActorAccessorTests.cs
new file mode 100644
index 00000000..ce082d5f
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests/Audit/HttpAuditActorAccessorTests.cs
@@ -0,0 +1,115 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Http;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.Auth.AspNetCore;
+using ZB.MOM.WW.OtOpcUa.Security.Audit;
+
+namespace ZB.MOM.WW.OtOpcUa.Security.Tests.Audit;
+
+///
+/// Unit tests for .
+///
+/// Covers the three cases:
+///
+/// - Authenticated principal with a claim →
+/// returns the username claim value.
+/// - Authenticated principal with only a / no
+/// username claim → falls back to the Name claim.
+/// - No HTTP context (null) or unauthenticated principal → returns
+/// .
+///
+///
+///
+public sealed class HttpAuditActorAccessorTests
+{
+ // ── helpers ──────────────────────────────────────────────────────────────────
+
+ private static IHttpContextAccessor ContextWith(ClaimsPrincipal principal)
+ {
+ var context = new DefaultHttpContext { User = principal };
+ return new HttpContextAccessorStub(context);
+ }
+
+ private static IHttpContextAccessor NoContext() =>
+ new HttpContextAccessorStub(null);
+
+ private static ClaimsPrincipal AuthenticatedWith(params Claim[] claims)
+ {
+ var identity = new ClaimsIdentity(
+ claims,
+ authenticationType: "TestScheme", // non-null authenticationType → IsAuthenticated = true
+ nameType: ZbClaimTypes.Name,
+ roleType: ZbClaimTypes.Role);
+ return new ClaimsPrincipal(identity);
+ }
+
+ private static ClaimsPrincipal Unauthenticated() =>
+ new(new ClaimsIdentity()); // no authenticationType → IsAuthenticated = false
+
+ // ── tests ─────────────────────────────────────────────────────────────────────
+
+ ///
+ /// An authenticated principal that carries
+ /// returns exactly that claim value — it is the canonical actor string.
+ ///
+ [Fact]
+ public void Returns_username_claim_for_authenticated_principal()
+ {
+ var principal = AuthenticatedWith(
+ new Claim(ZbClaimTypes.Username, "alice"),
+ new Claim(ZbClaimTypes.Name, "alice-name"),
+ new Claim(ZbClaimTypes.DisplayName, "Alice User"));
+
+ var sut = new HttpAuditActorAccessor(ContextWith(principal));
+
+ sut.CurrentActor.ShouldBe("alice");
+ }
+
+ ///
+ /// When the principal has no claim but does have
+ /// a claim, the Name claim value is returned as the
+ /// fallback actor.
+ ///
+ [Fact]
+ public void Falls_back_to_Name_claim_when_Username_claim_is_absent()
+ {
+ var principal = AuthenticatedWith(
+ new Claim(ZbClaimTypes.Name, "bob"));
+
+ var sut = new HttpAuditActorAccessor(ContextWith(principal));
+
+ sut.CurrentActor.ShouldBe("bob");
+ }
+
+ ///
+ /// An unauthenticated principal (Identity.IsAuthenticated == false) returns null —
+ /// the caller's fallback (typically ) is used.
+ ///
+ [Fact]
+ public void Returns_null_for_unauthenticated_principal()
+ {
+ var sut = new HttpAuditActorAccessor(ContextWith(Unauthenticated()));
+
+ sut.CurrentActor.ShouldBeNull();
+ }
+
+ ///
+ /// When there is no current HttpContext (e.g. background task, actor mailbox
+ /// worker), returns null.
+ ///
+ [Fact]
+ public void Returns_null_when_no_HttpContext()
+ {
+ var sut = new HttpAuditActorAccessor(NoContext());
+
+ sut.CurrentActor.ShouldBeNull();
+ }
+
+ // ── stub ──────────────────────────────────────────────────────────────────────
+
+ private sealed class HttpContextAccessorStub(HttpContext? context) : IHttpContextAccessor
+ {
+ public HttpContext? HttpContext { get; set; } = context;
+ }
+}