/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua; using Opc.Ua.Gds.Server; using Opc.Ua.Security.Certificates; using Opc.Ua.Server; using Opc.Ua.Server.UserDatabase; using Opc.Ua.Server.UserManagement; using Quickstarts.Servers; namespace Quickstarts.ReferenceServer { /// /// Implements the Quickstart Reference Server. /// /// /// /// Each server instance must have one instance of a StandardServer object which is /// responsible for reading the configuration file, creating the endpoints and dispatching /// incoming requests to the appropriate handler. /// /// /// This sub-class specifies non-configurable metadata such as Product Name and initializes /// the EmptyNodeManager which provides access to the data exposed by the Server. /// /// public class ReferenceServer : ReverseConnectServer { /// /// Create reference server /// public ReferenceServer(ITelemetryContext telemetry) : base(telemetry) { m_telemetry = telemetry; m_userDatabase = new LinqUserDatabase(); m_userDatabase.CreateUser("sysadmin", "demo"u8, [Role.SecurityAdmin, Role.AuthenticatedUser]); m_userDatabase.CreateUser("user1", "password"u8, [Role.AuthenticatedUser]); m_userDatabase.CreateUser("user2", "password1"u8, [Role.AuthenticatedUser]); m_userDatabase.CreateUser( "SystemAdmin", Encoding.UTF8.GetBytes("demo"), [GdsRole.CertificateAuthorityAdmin, GdsRole.DiscoveryAdmin, Role.SecurityAdmin, Role.ConfigureAdmin, Role.AuthenticatedUser]); m_userDatabase.CreateUser( "AppAdmin", Encoding.UTF8.GetBytes("demo"), [Role.AuthenticatedUser, GdsRole.CertificateAuthorityAdmin, GdsRole.DiscoveryAdmin, Role.AuthenticatedUser]); m_userDatabase.CreateUser( "DiscoveryAdmin", Encoding.UTF8.GetBytes("demo"), [Role.AuthenticatedUser, GdsRole.DiscoveryAdmin, Role.AuthenticatedUser]); m_userDatabase.CreateUser( "CertificateAuthorityAdmin", Encoding.UTF8.GetBytes("demo"), [Role.AuthenticatedUser, GdsRole.CertificateAuthorityAdmin, Role.AuthenticatedUser]); m_userManagement = new UserManagement(m_userDatabase, new Opc.Ua.Range(256, 1)); } /// /// Token validator /// public ITokenValidator? TokenValidator { get; set; } /// /// If true the ReferenceNodeManager is set to work with a sampling group mechanism /// for managing monitored items instead of a Monitored Node mechanism /// public bool UseSamplingGroupsInReferenceNodeManager { get; set; } /// /// If true, the server starts in provisioning mode with limited namespace /// and requires authenticated user access for certificate provisioning /// public bool ProvisioningMode { get; set; } /// /// The user database used for credential verification and user management. /// public IUserDatabase UserDatabase => m_userDatabase; /// /// If true, the server creates the FileSystem node manager that /// exposes the configured under /// the standard Server.FileSystem object (i=16314). /// This materially grows the address space — only enable it in /// tests / hosts that exercise FileSystem (Part 20). Default is /// false so the standard test fixtures keep a small, /// browse-friendly address space. /// public bool EnableFileSystemNodeManager { get; set; } /// /// Provider that backs the FileSystem node manager when /// is true. /// When null, the server falls back to a /// /// rooted at %TEMP%/OpcUaReferenceServerFs. /// public Opc.Ua.Server.FileSystem.IFileSystemProvider? FileSystemProvider { get; set; } /// /// Creates the node managers for the server. /// /// /// This method allows the sub-class create any additional node managers which it uses. The SDK /// always creates a CoreNodeManager which handles the built-in nodes defined by the specification. /// Any additional NodeManagers are expected to handle application specific nodes. /// protected override IMasterNodeManager CreateMasterNodeManager( IServerInternal server, ApplicationConfiguration configuration) { m_logger.CreatingReferenceServerNodeManager(); var nodeManagers = new List(); var asyncNodeManagers = new List(); if (ProvisioningMode) { m_logger.ProvisioningModeEnabled(); nodeManagers = []; } else { ReferenceNodeManager? referenceNodeManager = null; try { // CA2000: ownership-transfer pattern — nulled after handoff to asyncNodeManagers. #pragma warning disable CA2000 referenceNodeManager = new ReferenceNodeManager( server, configuration, UseSamplingGroupsInReferenceNodeManager); #pragma warning restore CA2000 asyncNodeManagers = [referenceNodeManager]; referenceNodeManager = null; } finally { // CA1508: only non-null on the exceptional path (try block clears it on success). #pragma warning disable CA1508 referenceNodeManager?.Dispose(); #pragma warning restore CA1508 } foreach (INodeManagerFactory nodeManagerFactory in NodeManagerFactories) { nodeManagers.Add(nodeManagerFactory.Create(server, configuration)); } foreach (IAsyncNodeManagerFactory nodeManagerFactory in AsyncNodeManagerFactories) { asyncNodeManagers.Add(nodeManagerFactory.CreateAsync(server, configuration).AsTask().GetAwaiter().GetResult()); } } if (EnableFileSystemNodeManager) { // FileSystem node manager — exposes the configured // provider (defaults to a temp folder) under the standard // Server.FileSystem object (i=16314). The reference server is a // test server, so anonymous clients may modify the mount too. Opc.Ua.Server.FileSystem.IFileSystemProvider provider = FileSystemProvider ?? CreateDefaultFileSystemProvider(); asyncNodeManagers.Add(new Opc.Ua.Server.FileSystem.FileSystemNodeManager( server, configuration, provider) { AllowAnonymousWrite = true }); } // OPC UA Part 17 — AliasName provider for the reference server. // // Registers a Part 17 InMemoryAliasNameStore directly with the // server-wide IAliasNameStoreRegistry so the standard // well-known FindAlias methods on TagVariables (i=23485) and // Topics (i=23494) — wired by DiagnosticsNodeManager — return // the sample tag/topic aliases below. // // No standalone AliasNameNodeManager is needed for read-only // service of standard categories; apps wanting their own // categories with full Add/Delete support add an // AliasNameNodeManager configured with their own namespace. ConfigureAliasNameStore(server); return new MasterNodeManager(server, configuration, null, asyncNodeManagers, nodeManagers); } /// protected override Opc.Ua.Server.UserManagement.IUserManagement CreateUserManagement( IServerInternal server, ApplicationConfiguration configuration) { return m_userManagement; } private static void ConfigureAliasNameStore(IServerInternal server) { if (server is not Opc.Ua.Server.AliasNames.IAliasNameStoreRegistryProvider provider) { return; } // A nested category under TagVariables: Part 17 §6.3.1 allows // AliasNameCategoryType instances to be organized under one // another, and FindAlias on the parent searches the whole // sub-tree while FindAlias on the child is restricted to it. // Its NodeId lives in the diagnostics namespace because that is // where DiagnosticsNodeManager materializes the node — which // means the namespace is registered here, ahead of the node // manager that would otherwise be the first to add it. // BrowseName and NodeId share the diagnostics namespace: // namespace 0 is reserved for OPC-Foundation-defined names, so // a server-defined category (and the aliases that inherit its // BrowseName namespace via the store) must not publish there. ushort diagnosticsNamespaceIndex = server.NamespaceUris.GetIndexOrAppend(DiagnosticsNamespaceUri); var devices = new Opc.Ua.Server.AliasNames.AliasNameCategoryDescriptor( new NodeId(DevicesCategoryName, diagnosticsNamespaceIndex), new QualifiedName(DevicesCategoryName, diagnosticsNamespaceIndex), Opc.Ua.Server.AliasNames.AliasNameCapabilities.All); // The descriptor BrowseName's namespace is what the store // stamps onto every alias QualifiedName it reports, and the // materializer re-homes ns=0 alias BrowseNames into the // diagnostics namespace — declaring the diagnostics namespace // here keeps FindAlias results and the browsable nodes' // BrowseNames identical, so TranslateBrowsePaths with a // returned alias name resolves. (The predefined TagVariables/ // Topics nodes keep their own NodeSet BrowseNames regardless.) var tagVariables = new Opc.Ua.Server.AliasNames.AliasNameCategoryDescriptor( Opc.Ua.ObjectIds.TagVariables, new QualifiedName(Opc.Ua.BrowseNames.TagVariables, diagnosticsNamespaceIndex), Opc.Ua.Server.AliasNames.AliasNameCapabilities.All, subCategories: [devices]); var topics = new Opc.Ua.Server.AliasNames.AliasNameCategoryDescriptor( Opc.Ua.ObjectIds.Topics, new QualifiedName(Opc.Ua.BrowseNames.Topics, diagnosticsNamespaceIndex), Opc.Ua.Server.AliasNames.AliasNameCapabilities.All); // Root the Aliases (i=23470) object too so FindAlias / // FindAliasVerbose / LastChange dispatched against it // aggregate the TagVariables / Topics sub-categories // (per Part 17 §6.3.2 recursive matching semantics). // // Every category declares the full capability set: the standard // NodeSet instantiates none of the optional Part 17 children on // the well-known objects, so DiagnosticsNodeManager adds them // during materialization. // AddAliasesToCategory / DeleteAliasesFromCategory require a // SecurityAdmin caller on a SignAndEncrypt channel. var aliases = new Opc.Ua.Server.AliasNames.AliasNameCategoryDescriptor( Opc.Ua.ObjectIds.Aliases, new QualifiedName(Opc.Ua.BrowseNames.Aliases, diagnosticsNamespaceIndex), Opc.Ua.Server.AliasNames.AliasNameCapabilities.All, subCategories: [tagVariables, topics]); // CA2000: ownership transferred to the registry which disposes // the store along with itself. #pragma warning disable CA2000 var store = new Opc.Ua.Server.AliasNames.InMemoryAliasNameStore( [aliases]); #pragma warning restore CA2000 int refServerNsIndex = server.NamespaceUris.GetIndex( Namespaces.ReferenceServer); ushort refServerNs = refServerNsIndex >= 0 ? (ushort)refServerNsIndex : ushort.MaxValue; if (refServerNs != ushort.MaxValue) { SeedTag(store, Opc.Ua.ObjectIds.TagVariables, "TIC101_Setpoint", new ExpandedNodeId("Scalar_Static_Double", refServerNs)); SeedTag(store, Opc.Ua.ObjectIds.TagVariables, "TIC101_PV", new ExpandedNodeId("Scalar_Static_Float", refServerNs)); SeedTag(store, Opc.Ua.ObjectIds.TagVariables, "FIC202_Flow", new ExpandedNodeId("Scalar_Simulation_Double", refServerNs)); SeedTag(store, Opc.Ua.ObjectIds.TagVariables, "MultiRefAlias", new ExpandedNodeId("Scalar_Static_Double", refServerNs)); SeedTag(store, Opc.Ua.ObjectIds.TagVariables, "MultiRefAlias", new ExpandedNodeId("Scalar_Static_Int32", refServerNs)); // The device tags live in the nested category, so FindAlias // on Devices returns exactly these two while FindAlias on // TagVariables still returns all six. SeedTag(store, devices.NodeId, "Pump1_Status", new ExpandedNodeId("Scalar_Static_Boolean", refServerNs)); SeedTag(store, devices.NodeId, "Heater_Power", new ExpandedNodeId("Scalar_Static_Int32", refServerNs)); } // The Topics aliases are NOT seeded here: Part 17 §9 constrains // that hierarchy to aliases for PublishedDataSetType instances, // and those dataset nodes are created later by // ReferenceServerConfigurationNodeManager.AddConformanceDataSetsAsync // — which seeds the aliases in the same place, so an alias can // never point at a dataset that was not created. provider.AliasNameStoreRegistry.Register(store); static void SeedTag( Opc.Ua.Server.AliasNames.InMemoryAliasNameStore store, NodeId categoryId, string name, ExpandedNodeId target) { store.Seed(categoryId, name, target, serverUri: null, referenceTypeId: ReferenceTypeIds.AliasFor); } } /// /// BrowseName of the AliasNameCategory nested under TagVariables. /// private const string DevicesCategoryName = "Devices"; /// /// Namespace that DiagnosticsNodeManager owns and in which /// the materialized alias nodes and the conformance datasets live. /// Shared with /// so the dataset NodeIds and the alias seeds that target them can /// never drift apart. /// internal const string DiagnosticsNamespaceUri = Opc.Ua.Namespaces.OpcUa + "Diagnostics"; /// /// Overrides the SDK default factory to plug in a /// reference-server-specific /// that applies a few CTT-only address-space tweaks (Server-node /// RolePermissions, HasAddIn instance, optional EngineeringUnits /// on AnalogItemType). Keeping these out of the SDK avoids /// polluting the standard nodeset for non-CTT hosts. /// protected override IMainNodeManagerFactory CreateMainNodeManagerFactory( IServerInternal server, ApplicationConfiguration configuration) { // In provisioning mode the server is in the application setup // state (OPC 10000-12 Annex G.2): expose InApplicationSetup=TRUE. return new ReferenceServerMainNodeManagerFactory( configuration, server, ProvisioningMode ? new ServerConfigurationOptions { InApplicationSetup = true } : null); } /// /// /// OPC 10000-12 Annex G.2: while the server is in the application /// setup state (provisioning mode) its ServerState is NoConfiguration. /// protected override ServerState StartupServerState => ProvisioningMode ? ServerState.NoConfiguration : base.StartupServerState; /// /// Returns a default /// rooted at a per-process temp folder. Override /// to mount a different backend. /// private static Opc.Ua.Server.FileSystem.PhysicalFileSystemProvider CreateDefaultFileSystemProvider() { string root = System.IO.Path.Combine( System.IO.Path.GetTempPath(), "OpcUaReferenceServerFs"); return new Opc.Ua.Server.FileSystem.PhysicalFileSystemProvider( root, mountName: "Temp"); } protected override IMonitoredItemQueueFactory CreateMonitoredItemQueueFactory( IServerInternal server, ApplicationConfiguration configuration) { if (configuration?.ServerConfiguration?.DurableSubscriptionsEnabled == true) { return new Servers.DurableMonitoredItemQueueFactory(server.Telemetry, server.MessageContext); } return new MonitoredItemQueueFactory(server.Telemetry); } /// /// Creates the subscriptionStore for the server. /// /// The server. /// The configuration. /// Returns a subscriptionStore for a server, the return type is . protected override ISubscriptionStore? CreateSubscriptionStore( IServerInternal server, ApplicationConfiguration configuration) { if (configuration?.ServerConfiguration?.DurableSubscriptionsEnabled == true) { return new Servers.SubscriptionStore(server); } return null; } /// /// Loads the non-configurable properties for the application. /// /// /// These properties are exposed by the server but cannot be changed by administrators. /// protected override ServerProperties LoadServerProperties() { return new ServerProperties { ManufacturerName = "OPC Foundation", ProductName = "Quickstart Reference Server", ProductUri = "http://opcfoundation.org/Quickstart/ReferenceServer/v1.04", SoftwareVersion = Opc.Ua.Utils.GetAssemblySoftwareVersion(), BuildNumber = Opc.Ua.Utils.GetAssemblyBuildNumber(), BuildDate = Opc.Ua.Utils.GetAssemblyTimestamp() }; } /// /// Creates the resource manager for the server. /// protected override ResourceManager CreateResourceManager( IServerInternal server, ApplicationConfiguration configuration) { var resourceManager = new ResourceManager(configuration); foreach (StatusCode id in StatusCode.InternedStatusCodes) { if (id.SymbolicId is { } symbolicId) { resourceManager.Add(symbolicId, "en-US", symbolicId); // Advertise German ("de-DE") as an additional supported locale. // The server's ServerCapabilities.LocaleIdArray is derived from // the locales that have translation tables, so registering the // status-code symbols under de-DE makes the locale available. resourceManager.Add(symbolicId, "de-DE", symbolicId); } } // Offer the application name in the advertised locales, so FindServers and // GetEndpoints return a de-DE ApplicationName when the client asks for German. string applicationName = configuration.ApplicationName ?? string.Empty; resourceManager.Add(ApplicationNameKey, "en-US", applicationName); resourceManager.Add( ApplicationNameKey, "de-DE", applicationName.Replace("Reference Server", "Referenzserver", StringComparison.Ordinal)); return resourceManager; } /// /// Initializes the server before it starts up. /// /// /// This method is called before any startup processing occurs. The sub-class may update the /// configuration object or do any other application specific startup tasks. /// protected override void OnServerStarting(ApplicationConfiguration configuration) { base.OnServerStarting(configuration); m_logger.ServerStarting(); // it is up to the application to decide how to validate user identity tokens. // this function creates validator for X509 identity tokens. CreateUserIdentityValidators(configuration); } /// /// Called after the server has been started. /// protected override void OnServerStarted(IServerInternal server) { base.OnServerStarted(server); RegisterIdentityAuthenticators(server); // Key the ApplicationName of the endpoints, so the resource manager translates it. if (ServerDescription != null) { ServerDescription.ApplicationName = new LocalizedText( ApplicationNameKey, "en-US", ServerDescription.ApplicationName.Text ?? string.Empty); } try { ServerInternal.UpdateServerStatus( status => // allow a faster sampling interval for CurrentTime node. status.Variable!.CurrentTime!.MinimumSamplingInterval = 250); } catch { } } /// /// Override some of the default user token policies for some endpoints. /// /// /// Sample to show how to override default user token policies. /// public override ArrayOf GetUserTokenPolicies( ApplicationConfiguration configuration, EndpointDescription description) { ArrayOf policies = base.GetUserTokenPolicies( configuration, description); // In provisioning mode, remove anonymous authentication if (ProvisioningMode) { return policies.Filter(u => u.TokenType != UserTokenType.Anonymous); } // sample how to modify default user token policies if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss && description.SecurityMode == MessageSecurityMode.SignAndEncrypt) { return policies.Filter(u => u.TokenType != UserTokenType.Certificate); } else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && description.SecurityMode == MessageSecurityMode.Sign) { return policies.Filter(u => u.TokenType != UserTokenType.Anonymous); } else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && description.SecurityMode == MessageSecurityMode.SignAndEncrypt) { return policies.Filter(u => u.TokenType != UserTokenType.UserName); } return policies; } /// /// Creates the objects used to validate the user identity tokens supported by the server. /// private void CreateUserIdentityValidators(ApplicationConfiguration configuration) { ServerConfiguration serverConfiguration = configuration.ServerConfiguration!; for (int ii = 0; ii < serverConfiguration.UserTokenPolicies.Count; ii++) { UserTokenPolicy policy = serverConfiguration.UserTokenPolicies[ii]; // create a validator for a certificate token policy. if (policy.TokenType == UserTokenType.Certificate) { // check if user certificate trust lists are specified in configuration. if (configuration.SecurityConfiguration.TrustedUserCertificates != null && configuration.SecurityConfiguration.UserIssuerCertificates != null) { // The server's CertificateManager already maps // TrustedUserCertificates / UserIssuerCertificates to the // Users trust list during MapFromSecurityConfiguration(). // Use the CertificateManager directly and validate against // the Users trust list per call. m_userCertificateValidator = CertificateManager; // Configure a dedicated review store for rejected X509 user // certificates so an operator can inspect a rejected user // certificate and, if legitimate, move it into the // TrustedUserCertificates store (the shared rejected store // mixes application and user certificates, which is awkward // when provisioning user identities for conformance testing). ConfigureRejectedUserCertificateStore(configuration.SecurityConfiguration); } } } } /// /// Resolves the directory used to persist rejected X509 user /// certificates for later review. It is created as a sibling of the /// configured TrustedUserCertificates store (e.g. /// pki/rejectedUser next to pki/trustedUser). Failures are /// swallowed: an unavailable review store must never affect user /// authentication. /// private void ConfigureRejectedUserCertificateStore(SecurityConfiguration security) { try { string? trustedUserPath = Opc.Ua.Utils.ReplaceSpecialFolderNames( security.TrustedUserCertificates?.StorePath); if (string.IsNullOrEmpty(trustedUserPath)) { return; } string? parent = Path.GetDirectoryName( trustedUserPath!.TrimEnd( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); if (!string.IsNullOrEmpty(parent)) { m_rejectedUserStorePath = Path.Combine(parent!, "rejectedUser"); } } catch (Exception e) { m_logger.FailedToConfigureRejectedUserStore(e); } } /// /// Validates the password for a username token. /// /// private RoleBasedIdentity VerifyPassword(UserNameIdentityTokenHandler userTokenHandler) { string userName = userTokenHandler.UserName; byte[]? password = userTokenHandler.DecryptedPassword; if (string.IsNullOrEmpty(userName)) { // an empty username is not accepted. throw ServiceResultException.Create( StatusCodes.BadIdentityTokenInvalid, "Security token is not a valid username token. An empty username is not accepted."); } if (Opc.Ua.Utils.Utf8IsNullOrEmpty(password)) { // an empty password is not accepted. throw ServiceResultException.Create( StatusCodes.BadIdentityTokenRejected, "Security token is not a valid username token. An empty password is not accepted."); } if (!m_userDatabase.CheckCredentials(userName, password)) { // construct translation object with default text. var info = new TranslationInfo( "InvalidPassword", "en-US", "Invalid username or password.", userName); // create an exception with a vendor defined sub-code. throw new ServiceResultException( new ServiceResult( LoadServerProperties().ProductUri, new StatusCode(StatusCodes.BadUserAccessDenied.Code, "InvalidPassword"), new LocalizedText(info))); } ICollection roles = m_userDatabase.GetUserRoles(userName); var identity = new UserIdentity(userTokenHandler); List effectiveRoles = roles is { Count: > 0 } ? [.. roles] : [Role.AuthenticatedUser]; if (effectiveRoles.Contains(Role.SecurityAdmin) && !effectiveRoles.Contains(Role.ConfigureAdmin)) { effectiveRoles.Add(Role.ConfigureAdmin); } return new RoleBasedIdentity( identity, effectiveRoles, ServerInternal.MessageContext.NamespaceUris); } /// /// Verifies that a certificate user token is trusted. /// /// private async ValueTask VerifyX509IdentityTokenAsync( X509IdentityTokenHandler x509TokenHandler, CancellationToken ct) { var wireToken = (X509IdentityToken)x509TokenHandler.Token; using Certificate? userCertificate = wireToken.CertificateData.IsEmpty ? null : Certificate.FromRawData(wireToken.CertificateData); try { Opc.Ua.CertificateValidationResult userCertResult = await (m_userCertificateValidator ?? CertificateManager!) .ValidateAsync( userCertificate!, TrustListIdentifier.Users, ct) .ConfigureAwait(false); if (!userCertResult.IsValid) { throw new ServiceResultException(userCertResult.StatusCode); } } catch (Exception e) { // Persist the rejected user certificate to a dedicated review // store so an operator can inspect it and, if legitimate, move it // into the TrustedUserCertificates store. Best-effort: a review // store write must never change the activation result. await PersistRejectedUserCertificateAsync(userCertificate, ct) .ConfigureAwait(false); TranslationInfo info; StatusCode result = StatusCodes.BadIdentityTokenRejected; if (e is ServiceResultException se && se.StatusCode == StatusCodes.BadCertificateUseNotAllowed) { info = new TranslationInfo( "InvalidCertificate", "en-US", "'{0}' is an invalid user certificate.", userCertificate?.Subject ?? string.Empty); result = StatusCodes.BadIdentityTokenInvalid; } else { // construct translation object with default text. info = new TranslationInfo( "UntrustedCertificate", "en-US", "'{0}' is not a trusted user certificate.", userCertificate?.Subject ?? string.Empty); } // create an exception with a vendor defined sub-code. throw new ServiceResultException( new ServiceResult( LoadServerProperties().ProductUri, new StatusCode(result.Code, info.Key), new LocalizedText(info))); } } /// /// Best-effort persistence of a rejected X509 user certificate to the /// dedicated review store configured by /// . Never throws: /// any failure is logged and swallowed so it cannot influence the /// authentication outcome. /// private async ValueTask PersistRejectedUserCertificateAsync( Certificate? userCertificate, CancellationToken ct) { if (userCertificate == null || string.IsNullOrEmpty(m_rejectedUserStorePath)) { return; } try { await userCertificate.AddToStoreAsync( CertificateStoreType.Directory, m_rejectedUserStorePath!, password: null, m_telemetry, ct).ConfigureAwait(false); } catch (Exception e) { m_logger.FailedToPersistRejectedUserCertificate(e, m_rejectedUserStorePath); } } private IUserIdentity? VerifyIssuedToken(IssuedIdentityTokenHandler issuedTokenHandler) { if (TokenValidator == null) { m_logger.NoTokenValidatorSpecified(); return null; } try { if (issuedTokenHandler.IssuedTokenType == IssuedTokenType.JWT) { m_logger.VerifyIssuedTokenValidateToken(); return TokenValidator.ValidateToken(issuedTokenHandler); } return null; } catch (Exception e) { TranslationInfo info; StatusCode result = StatusCodes.BadIdentityTokenRejected; if (e is ServiceResultException se && se.StatusCode == StatusCodes.BadIdentityTokenInvalid) { info = new TranslationInfo( "IssuedTokenInvalid", "en-US", "token is an invalid issued token."); result = StatusCodes.BadIdentityTokenInvalid; } else // Rejected { // construct translation object with default text. info = new TranslationInfo( "IssuedTokenRejected", "en-US", "token is rejected."); } m_logger.VerifyIssuedTokenThrowServiceResultException(result); throw new ServiceResultException( new ServiceResult( LoadServerProperties().ProductUri, new StatusCode(result.Code, info.Key), new LocalizedText(info))); } } private void RegisterIdentityAuthenticators(IServerInternal server) { server.IdentityRegistry.RegisterDefaultAuthenticators( VerifyUserNameIdentityAsync, VerifyX509IdentityAsync, VerifyJwtIdentityAsync); } private ValueTask VerifyUserNameIdentityAsync( UserNameIdentityTokenHandler userNameToken, CancellationToken ct) { ct.ThrowIfCancellationRequested(); RoleBasedIdentity identity = VerifyPassword(userNameToken); m_logger.UsernameTokenAccepted(identity.DisplayName); return new ValueTask(identity); } private async ValueTask VerifyX509IdentityAsync( X509IdentityTokenHandler x509Token, CancellationToken ct) { ct.ThrowIfCancellationRequested(); await VerifyX509IdentityTokenAsync(x509Token, ct).ConfigureAwait(false); var identity = new RoleBasedIdentity( new UserIdentity(x509Token), [Role.AuthenticatedUser], ServerInternal.MessageContext.NamespaceUris); m_logger.X509TokenAccepted(identity.DisplayName); return identity; } private ValueTask VerifyJwtIdentityAsync( IssuedIdentityTokenHandler issuedToken, CancellationToken ct) { ct.ThrowIfCancellationRequested(); IUserIdentity? identity = VerifyIssuedToken(issuedToken); return new ValueTask(identity == null ? null : new RoleBasedIdentity( identity, [Role.AuthenticatedUser], ServerInternal.MessageContext.NamespaceUris)); } protected override void Dispose(bool disposing) { if (disposing) { m_userManagement.Dispose(); } base.Dispose(disposing); } private const string ApplicationNameKey = "ApplicationName"; private CertificateManager? m_userCertificateValidator; private string? m_rejectedUserStorePath; private readonly ITelemetryContext m_telemetry; private readonly LinqUserDatabase m_userDatabase; private readonly UserManagement m_userManagement; } internal static partial class ReferenceServerLog { [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 0, Level = LogLevel.Information, Message = "Creating the Reference Server Node Manager.")] public static partial void CreatingReferenceServerNodeManager(this ILogger logger); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 1, Level = LogLevel.Information, Message = "Server is in provisioning mode - limited namespace enabled.")] public static partial void ProvisioningModeEnabled(this ILogger logger); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 2, Level = LogLevel.Information, Message = "The server is starting.")] public static partial void ServerStarting(this ILogger logger); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 3, Level = LogLevel.Warning, Message = "No TokenValidator is specified.")] public static partial void NoTokenValidatorSpecified(this ILogger logger); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 4, Level = LogLevel.Debug, Message = "VerifyIssuedToken: ValidateToken")] public static partial void VerifyIssuedTokenValidateToken(this ILogger logger); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 5, Level = LogLevel.Warning, Message = "VerifyIssuedToken: Throw ServiceResultException 0x{Result:x}")] public static partial void VerifyIssuedTokenThrowServiceResultException( this ILogger logger, StatusCode result); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 6, Level = LogLevel.Information, Message = "Username Token Accepted: {Identity}")] public static partial void UsernameTokenAccepted(this ILogger logger, string identity); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 7, Level = LogLevel.Information, Message = "X509 Token Accepted: {Identity}")] public static partial void X509TokenAccepted(this ILogger logger, string identity); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 8, Level = LogLevel.Warning, Message = "Failed to configure the rejected user certificate review store.")] public static partial void FailedToConfigureRejectedUserStore(this ILogger logger, Exception exception); [LoggerMessage( EventId = QuickstartsServersEventIds.ReferenceServer + 9, Level = LogLevel.Warning, Message = "Failed to persist rejected user certificate to the review store '{Path}'.")] public static partial void FailedToPersistRejectedUserCertificate( this ILogger logger, Exception exception, string? path); } }