diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs
index 240b143ed..23ffebf91 100644
--- a/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs
+++ b/Stack/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs
@@ -1427,7 +1427,37 @@ private static EndpointDescriptionCollection MatchEndpoints(
// no matches (security parameters may have changed).
if (matches.Count == 0)
{
- matches = collection;
+ // if specific security parameters were requested, throw appropriate error
+ bool hasSpecificPolicy = !string.IsNullOrEmpty(securityPolicyUri);
+ bool hasSpecificMode = securityMode != MessageSecurityMode.Invalid;
+
+ if (hasSpecificPolicy && hasSpecificMode)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityPolicyRejected,
+ "Server does not support the requested security policy '{0}' and security mode '{1}'.",
+ securityPolicyUri,
+ securityMode);
+ }
+ else if (hasSpecificPolicy)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityPolicyRejected,
+ "Server does not support the requested security policy '{0}'.",
+ securityPolicyUri);
+ }
+ else if (hasSpecificMode)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityModeRejected,
+ "Server does not support the requested security mode '{0}'.",
+ securityMode);
+ }
+ else
+ {
+ // no specific security parameters were requested, fall back to any endpoint
+ matches = collection;
+ }
}
// check if list has to be narrowed down further.
diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs
new file mode 100644
index 000000000..4c7bda9f0
--- /dev/null
+++ b/Tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs
@@ -0,0 +1,271 @@
+/* ========================================================================
+ * 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.Reflection;
+using NUnit.Framework;
+using Opc.Ua.Tests;
+using Assert = NUnit.Framework.Legacy.ClassicAssert;
+
+namespace Opc.Ua.Core.Tests.Stack.Client
+{
+ ///
+ /// Tests for ConfiguredEndpoint matching.
+ ///
+ [TestFixture]
+ [Category("Client")]
+ [SetCulture("en-us")]
+ [SetUICulture("en-us")]
+ [Parallelizable]
+ public class ConfiguredEndpointTests
+ {
+ ///
+ /// Test that when a requested security policy is not supported by the server,
+ /// BadSecurityPolicyRejected is thrown instead of BadUserAccessDenied.
+ ///
+ [Test]
+ public void MatchEndpoints_ThrowsBadSecurityPolicyRejected_WhenPolicyNotSupported()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+
+ // Create server endpoints with only None and Basic256Sha256
+ var serverEndpoints = new EndpointDescriptionCollection
+ {
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.None,
+ SecurityPolicyUri = SecurityPolicies.None
+ },
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.SignAndEncrypt,
+ SecurityPolicyUri = SecurityPolicies.Basic256Sha256
+ }
+ };
+
+ // Try to match with a security policy that doesn't exist (Aes256_Sha256_RsaPss)
+ var ex = Assert.Throws(() =>
+ {
+ InvokeMatchEndpoints(
+ serverEndpoints,
+ new Uri("opc.tcp://localhost:4840"),
+ MessageSecurityMode.SignAndEncrypt,
+ SecurityPolicies.Aes256_Sha256_RsaPss
+ );
+ });
+
+ Assert.IsInstanceOf(ex.InnerException);
+ var serviceException = (ServiceResultException)ex.InnerException;
+ Assert.AreEqual(StatusCodes.BadSecurityPolicyRejected, serviceException.StatusCode);
+ Assert.That(serviceException.Message, Does.Contain(SecurityPolicies.Aes256_Sha256_RsaPss));
+ }
+
+ ///
+ /// Test that when a requested security mode is not supported by the server,
+ /// BadSecurityModeRejected is thrown.
+ ///
+ [Test]
+ public void MatchEndpoints_ThrowsBadSecurityModeRejected_WhenModeNotSupported()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+
+ // Create server endpoints with only None security mode
+ var serverEndpoints = new EndpointDescriptionCollection
+ {
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.None,
+ SecurityPolicyUri = SecurityPolicies.None
+ }
+ };
+
+ // Try to match with SignAndEncrypt mode that doesn't exist
+ var ex = Assert.Throws(() =>
+ {
+ InvokeMatchEndpoints(
+ serverEndpoints,
+ new Uri("opc.tcp://localhost:4840"),
+ MessageSecurityMode.SignAndEncrypt,
+ null // no specific policy requested, only mode
+ );
+ });
+
+ Assert.IsInstanceOf(ex.InnerException);
+ var serviceException = (ServiceResultException)ex.InnerException;
+ Assert.AreEqual(StatusCodes.BadSecurityModeRejected, serviceException.StatusCode);
+ Assert.That(serviceException.Message, Does.Contain("SignAndEncrypt"));
+ }
+
+ ///
+ /// Test that when both security policy and mode are not supported,
+ /// BadSecurityPolicyRejected is thrown with information about both.
+ ///
+ [Test]
+ public void MatchEndpoints_ThrowsBadSecurityPolicyRejected_WhenBothPolicyAndModeNotSupported()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+
+ // Create server endpoints with only None
+ var serverEndpoints = new EndpointDescriptionCollection
+ {
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.None,
+ SecurityPolicyUri = SecurityPolicies.None
+ }
+ };
+
+ // Try to match with both policy and mode that don't exist
+ var ex = Assert.Throws(() =>
+ {
+ InvokeMatchEndpoints(
+ serverEndpoints,
+ new Uri("opc.tcp://localhost:4840"),
+ MessageSecurityMode.SignAndEncrypt,
+ SecurityPolicies.Basic256Sha256
+ );
+ });
+
+ Assert.IsInstanceOf(ex.InnerException);
+ var serviceException = (ServiceResultException)ex.InnerException;
+ Assert.AreEqual(StatusCodes.BadSecurityPolicyRejected, serviceException.StatusCode);
+ Assert.That(serviceException.Message, Does.Contain(SecurityPolicies.Basic256Sha256));
+ Assert.That(serviceException.Message, Does.Contain("SignAndEncrypt"));
+ }
+
+ ///
+ /// Test that when no specific security parameters are requested,
+ /// the method returns available endpoints without throwing.
+ ///
+ [Test]
+ public void MatchEndpoints_ReturnsEndpoints_WhenNoSecurityParametersSpecified()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+
+ // Create server endpoints
+ var serverEndpoints = new EndpointDescriptionCollection
+ {
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.None,
+ SecurityPolicyUri = SecurityPolicies.None
+ },
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.SignAndEncrypt,
+ SecurityPolicyUri = SecurityPolicies.Basic256Sha256
+ }
+ };
+
+ // Match without specifying security parameters
+ var matches = InvokeMatchEndpoints(
+ serverEndpoints,
+ new Uri("opc.tcp://localhost:4840"),
+ MessageSecurityMode.Invalid,
+ null
+ );
+
+ // Should return available endpoints
+ Assert.IsNotNull(matches);
+ Assert.Greater(matches.Count, 0);
+ }
+
+ ///
+ /// Test that matching works correctly when the requested security parameters exist.
+ ///
+ [Test]
+ public void MatchEndpoints_ReturnsMatchingEndpoint_WhenSecurityParametersMatch()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+
+ // Create server endpoints
+ var serverEndpoints = new EndpointDescriptionCollection
+ {
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.None,
+ SecurityPolicyUri = SecurityPolicies.None
+ },
+ new EndpointDescription
+ {
+ EndpointUrl = "opc.tcp://localhost:4840",
+ SecurityMode = MessageSecurityMode.SignAndEncrypt,
+ SecurityPolicyUri = SecurityPolicies.Basic256Sha256
+ }
+ };
+
+ // Match with existing security parameters
+ var matches = InvokeMatchEndpoints(
+ serverEndpoints,
+ new Uri("opc.tcp://localhost:4840"),
+ MessageSecurityMode.SignAndEncrypt,
+ SecurityPolicies.Basic256Sha256
+ );
+
+ // Should return the matching endpoint
+ Assert.IsNotNull(matches);
+ Assert.AreEqual(1, matches.Count);
+ Assert.AreEqual(SecurityPolicies.Basic256Sha256, matches[0].SecurityPolicyUri);
+ Assert.AreEqual(MessageSecurityMode.SignAndEncrypt, matches[0].SecurityMode);
+ }
+
+ ///
+ /// Helper method to invoke the private MatchEndpoints method via reflection.
+ ///
+ private EndpointDescriptionCollection InvokeMatchEndpoints(
+ EndpointDescriptionCollection collection,
+ Uri endpointUrl,
+ MessageSecurityMode securityMode,
+ string securityPolicyUri)
+ {
+ var configuredEndpointType = typeof(ConfiguredEndpoint);
+ var matchEndpointsMethod = configuredEndpointType.GetMethod(
+ "MatchEndpoints",
+ BindingFlags.NonPublic | BindingFlags.Static,
+ null,
+ new[] { typeof(EndpointDescriptionCollection), typeof(Uri), typeof(MessageSecurityMode), typeof(string) },
+ null
+ );
+
+ Assert.IsNotNull(matchEndpointsMethod, "MatchEndpoints method not found");
+
+ return (EndpointDescriptionCollection)matchEndpointsMethod.Invoke(
+ null,
+ new object[] { collection, endpointUrl, securityMode, securityPolicyUri }
+ );
+ }
+ }
+}