diff --git a/src/Opc.Ua.Bindings.Https/Https/HttpsServiceHost.cs b/src/Opc.Ua.Bindings.Https/Https/HttpsServiceHost.cs index 81fbb1f20e..b6d78d9458 100644 --- a/src/Opc.Ua.Bindings.Https/Https/HttpsServiceHost.cs +++ b/src/Opc.Ua.Bindings.Https/Https/HttpsServiceHost.cs @@ -226,8 +226,11 @@ public async ValueTask> CreateServiceHostAsync( description); description.TransportProfileUri = TransportProfileUri; - // if no mutual TLS authentication is used, anonymous user tokens are not allowed - if (!httpsMutualTls) + // Keep the HTTPS authentication policy that requires an + // authenticated user when mutual TLS is disabled. WSS carries + // normal UASC sessions, so it uses the server's configured OPC UA + // user-token policies just like UA TCP, including Anonymous. + if (!httpsMutualTls && !Profiles.IsWssBinary(TransportProfileUri)) { description.UserIdentityTokens = description.UserIdentityTokens .Filter(token => token.TokenType != UserTokenType.Anonymous); diff --git a/src/Opc.Ua.Client/Session/ChannelManagerSessionFactory.cs b/src/Opc.Ua.Client/Session/ChannelManagerSessionFactory.cs index 3ae9bdfbe4..1a8fa04170 100644 --- a/src/Opc.Ua.Client/Session/ChannelManagerSessionFactory.cs +++ b/src/Opc.Ua.Client/Session/ChannelManagerSessionFactory.cs @@ -401,7 +401,9 @@ private async Task PrepareEndpointAndManagerAsync( if (updateBeforeConnect && connection == null) { - await endpoint.UpdateFromServerAsync(messageContext.Telemetry, ct).ConfigureAwait(false); + await endpoint + .UpdateFromServerAsync(configuration, messageContext.Telemetry, ct) + .ConfigureAwait(false); } if (checkDomain && endpoint.Description.ServerCertificate.Length > 0) diff --git a/src/Opc.Ua.Client/Session/DefaultSessionFactory.cs b/src/Opc.Ua.Client/Session/DefaultSessionFactory.cs index 98e46d7143..b182f6d394 100644 --- a/src/Opc.Ua.Client/Session/DefaultSessionFactory.cs +++ b/src/Opc.Ua.Client/Session/DefaultSessionFactory.cs @@ -266,7 +266,9 @@ public virtual async Task CreateChannelAsync( // update endpoint description using the discovery endpoint. if (endpoint.UpdateBeforeConnect && connection == null) { - await endpoint.UpdateFromServerAsync(messageContext.Telemetry, ct).ConfigureAwait(false); + await endpoint + .UpdateFromServerAsync(configuration, messageContext.Telemetry, ct) + .ConfigureAwait(false); endpointDescription = endpoint.Description; // UpdateFromServerAsync re-reads Configuration from the discovery response; // it is set whenever the description was updated successfully. diff --git a/src/Opc.Ua.Client/Session/Session.ChannelManager.cs b/src/Opc.Ua.Client/Session/Session.ChannelManager.cs index 172200c453..9bc99e3f35 100644 --- a/src/Opc.Ua.Client/Session/Session.ChannelManager.cs +++ b/src/Opc.Ua.Client/Session/Session.ChannelManager.cs @@ -310,7 +310,7 @@ public static async Task CreateAsync( if (updateBeforeConnect) { await endpoint - .UpdateFromServerAsync(probeContext.Telemetry, ct) + .UpdateFromServerAsync(configuration, probeContext.Telemetry, ct) .ConfigureAwait(false); } diff --git a/src/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs b/src/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs index 3368f24c68..6f85c5416e 100644 --- a/src/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs +++ b/src/Opc.Ua.Core/Stack/Configuration/ConfiguredEndpoints.cs @@ -1173,13 +1173,59 @@ public Task UpdateFromServerAsync( /// /// Updates an endpoint with information from the server's discovery endpoint. /// - public async Task UpdateFromServerAsync( + /// + /// The application configuration supplies the certificate validator required by + /// secure discovery transports such as WSS. + /// + public Task UpdateFromServerAsync( + ApplicationConfiguration applicationConfiguration, + ITelemetryContext? telemetry, + CancellationToken ct = default) + { + if (applicationConfiguration == null) + { + throw new ArgumentNullException(nameof(applicationConfiguration)); + } + + return UpdateFromServerCoreAsync( + applicationConfiguration, + EndpointUrl!, + connection: null, + m_description.SecurityMode, + m_description.SecurityPolicyUri!, + telemetry, + ct); + } + + /// + /// Updates an endpoint with information from the server's discovery endpoint. + /// + public Task UpdateFromServerAsync( Uri endpointUrl, ITransportWaitingConnection? connection, MessageSecurityMode securityMode, string securityPolicyUri, ITelemetryContext? telemetry, CancellationToken ct = default) + { + return UpdateFromServerCoreAsync( + applicationConfiguration: null, + endpointUrl, + connection, + securityMode, + securityPolicyUri, + telemetry, + ct); + } + + private async Task UpdateFromServerCoreAsync( + ApplicationConfiguration? applicationConfiguration, + Uri endpointUrl, + ITransportWaitingConnection? connection, + MessageSecurityMode securityMode, + string securityPolicyUri, + ITelemetryContext? telemetry, + CancellationToken ct) { // get the a discovery url. Uri? discoveryUrl = GetDiscoveryUrl(endpointUrl); @@ -1188,7 +1234,15 @@ public async Task UpdateFromServerAsync( DiscoveryClient? client = null; try { - if (connection != null) + if (applicationConfiguration != null) + { + client = await DiscoveryClient.CreateAsync( + applicationConfiguration, + discoveryUrl, + m_configuration, + ct: ct).ConfigureAwait(false); + } + else if (connection != null) { client = await DiscoveryClient.CreateAsync( connection, diff --git a/tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs b/tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs index b3457a3460..2da61e3687 100644 --- a/tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs +++ b/tests/Opc.Ua.Core.Tests/Stack/Client/ConfiguredEndpointTests.cs @@ -566,6 +566,23 @@ public void NeedUpdateFromServerReturnsFalseWithNoneSecurity() Assert.That(endpoint.NeedUpdateFromServer(), Is.False); } + [Test] + public void UpdateFromServerWithNullApplicationConfigurationThrowsArgumentNullException() + { + var endpoint = new ConfiguredEndpoint(null, new EndpointDescription + { + EndpointUrl = "opc.wss://localhost:4840", + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + }); + + ArgumentNullException ex = Assert.Throws(() => + endpoint.UpdateFromServerAsync( + (ApplicationConfiguration)null!, + telemetry: null)); + Assert.That(ex.ParamName, Is.EqualTo("applicationConfiguration")); + } + [Test] public void GetDiscoveryUrlWithHttpScheme() { diff --git a/tests/Opc.Ua.Sessions.Tests/WssJsonTransportIntegrationTests.cs b/tests/Opc.Ua.Sessions.Tests/WssJsonTransportIntegrationTests.cs index 7a13bcc5ce..9cca00ee82 100644 --- a/tests/Opc.Ua.Sessions.Tests/WssJsonTransportIntegrationTests.cs +++ b/tests/Opc.Ua.Sessions.Tests/WssJsonTransportIntegrationTests.cs @@ -134,6 +134,25 @@ public void ServerExposesWssEndpointWithSecurityNone() "Reference server did not advertise an unsecured WSS endpoint - JSON sub-protocol requires SM None."); } + [Test] + public async Task AnonymousSessionOverWssBinaryOpensWithoutMutualTlsAsync() + { + EndpointDescription wss = m_server.GetEndpoints() + .ToArray() + .First(ep => + string.Equals(ep.TransportProfileUri, Profiles.UaWssTransport, StringComparison.Ordinal) && + ep.SecurityMode == MessageSecurityMode.None); + Assert.That( + wss.UserIdentityTokens.ToArray(), + Has.Some.Matches(token => token.TokenType == UserTokenType.Anonymous)); + + using var session = await m_clientFixture + .ConnectAsync(m_endpointUrl.ToString()) + .ConfigureAwait(false); + Assert.That(session.Connected, Is.True); + await session.CloseAsync().ConfigureAwait(false); + } + [Test] public async Task GetEndpointsOverWssJsonReturnsServerEndpointsAsync() { diff --git a/tests/Opc.Ua.Sessions.Tests/WssTransportIntegrationTests.cs b/tests/Opc.Ua.Sessions.Tests/WssTransportIntegrationTests.cs index a954635a96..5fc587a69e 100644 --- a/tests/Opc.Ua.Sessions.Tests/WssTransportIntegrationTests.cs +++ b/tests/Opc.Ua.Sessions.Tests/WssTransportIntegrationTests.cs @@ -179,6 +179,29 @@ public async Task ConnectAndBrowseServerNodeAsync() await session.CloseAsync().ConfigureAwait(false); } + + [Test] + public async Task UpdateBeforeConnectUsesApplicationCertificateValidationAsync() + { + ConfiguredEndpoint endpoint = await m_clientFixture + .GetEndpointAsync(m_endpointUrl, SecurityPolicies.Basic256Sha256) + .ConfigureAwait(false); + + using ISession session = await m_clientFixture.SessionFactory + .CreateAsync( + m_clientFixture.Config, + endpoint, + updateBeforeConnect: true, + checkDomain: false, + nameof(UpdateBeforeConnectUsesApplicationCertificateValidationAsync), + m_clientFixture.SessionTimeout, + identity: null, + preferredLocales: default) + .ConfigureAwait(false); + + Assert.That(session.Connected, Is.True); + await session.CloseAsync().ConfigureAwait(false); + } } }