/* ========================================================================
* 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Client;
using Opc.Ua.Client.TestFramework;
namespace Opc.Ua.Core.Security.Tests
{
///
/// compliance tests for Security – user token handling,
/// identity changes, nonce uniqueness, session timeout, and
/// per-SecurityPolicy connectivity.
///
[TestFixture]
[Category("Conformance")]
[Category("SecurityUserToken")]
public class SecurityUserTokenTests : TestFixture
{
[Test]
public async Task ConnectSysadminOnSignEndpointAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
EndpointDescription signEp =
FindEndpoint(endpoints, MessageSecurityMode.Sign);
if (signEp == null)
{
Assert.Fail("No Sign endpoint available.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, signEp!.SecurityPolicyUri!,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ConnectSysadminOnSignAndEncryptEndpointAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
EndpointDescription encryptEp =
FindEndpoint(endpoints, MessageSecurityMode.SignAndEncrypt);
if (encryptEp == null)
{
Assert.Fail("No SignAndEncrypt endpoint available.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, encryptEp!.SecurityPolicyUri!,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ConnectAppuserVerifyLimitedAccessAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Ignore("No Username token advertised.");
}
ISession session;
try
{
// Use the no-retry helper: 'appuser' may not exist on the
// configured server, and the 25-retry wrapper would lock out
// the account for 5 minutes (see SetUp ClearAuthenticationLockouts).
session = await OpenAuxSessionAsync(
userIdentity: new UserIdentity("appuser", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Ignore($"Rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.Connected, Is.True);
// Try to write as appuser; may be denied
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse wr = await session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From(99))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(wr.Results.Count, Is.EqualTo(1));
// Accept Good (if allowed) or BadUserAccessDenied
Assert.That(
wr.Results[0].Code == StatusCodes.Good ||
wr.Results[0].Code == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected Good or BadUserAccessDenied, got {wr.Results[0]}");
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task AnonymousCanReadNodesAsync()
{
// Shared session is anonymous
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse rr = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(rr.Results.Count, Is.EqualTo(1));
Assert.That(StatusCode.IsGood(rr.Results[0].StatusCode), Is.True);
}
[Test]
public async Task SysadminCanReadNodeAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse rr = await session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(rr.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(rr.Results[0].StatusCode), Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task SysadminCanWriteNodeAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse wr = await session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From(77))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(wr.Results.Count, Is.EqualTo(1));
Assert.That(
wr.Results[0].Code, Is.EqualTo(StatusCodes.Good));
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task AppuserWriteDeniedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Ignore("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("appuser", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Ignore($"Rejected: {sre.StatusCode}");
return;
}
try
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse wr = await session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From(88))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(wr.Results.Count, Is.EqualTo(1));
Assert.That(
wr.Results[0].Code == StatusCodes.Good ||
wr.Results[0].Code == StatusCodes.BadUserAccessDenied,
Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ConnectWithEmptyPasswordRejectedAsync()
{
ArrayOf endpoints;
try
{
endpoints = await GetEndpointsAsync().ConfigureAwait(false);
}
catch (ServiceResultException ex) when (
ex.StatusCode == StatusCodes.BadConnectionClosed ||
ex.StatusCode == StatusCodes.BadNotConnected)
{
Assert.Ignore("Discovery endpoint not available: " + ex.StatusCode);
return;
}
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Ignore("No Username token advertised.");
}
// Use the non-retrying ConnectAsync overload to avoid repeated
// failed login attempts that could lock out the sysadmin account.
ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(
ServerUrl, SecurityPolicies.None, endpoints).ConfigureAwait(false);
try
{
ISession session = await ClientFixture.ConnectAsync(
endpoint,
new UserIdentity("sysadmin", ""u8))
.ConfigureAwait(false);
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
Assert.Ignore("Server accepted empty password (demo server behavior).");
}
catch (ServiceResultException)
{
// Any rejection is acceptable — empty password should be rejected
Assert.Pass("Server correctly rejected empty password.");
}
catch (Exception)
{
Assert.Ignore("Connection failed with unexpected error.");
}
}
[Test]
public async Task VerifySecurityLevelOrderingAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
Assert.That(endpoints.Count, Is.GreaterThan(0));
byte maxNoneLevel = 0;
byte minSecureLevel = byte.MaxValue;
bool foundNone = false;
bool foundSecure = false;
foreach (EndpointDescription ep in endpoints)
{
if (ep.SecurityMode == MessageSecurityMode.None)
{
if (ep.SecurityLevel > maxNoneLevel)
{
maxNoneLevel = ep.SecurityLevel;
}
foundNone = true;
}
else
{
if (ep.SecurityLevel < minSecureLevel)
{
minSecureLevel = ep.SecurityLevel;
}
foundSecure = true;
}
}
if (foundNone && foundSecure)
{
Assert.That(minSecureLevel,
Is.GreaterThanOrEqualTo(maxNoneLevel),
"Secure endpoints should have higher SecurityLevel.");
}
}
[Test]
public async Task ConnectWithEachSecurityPolicyAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
var policies = new HashSet();
foreach (EndpointDescription ep in endpoints)
{
if (ep.SecurityMode != MessageSecurityMode.None)
{
policies.Add(ep.SecurityPolicyUri!);
}
}
if (policies.Count == 0)
{
Assert.Ignore("No secure endpoints available.");
}
foreach (string policy in policies)
{
try
{
ISession session = await ClientFixture
.ConnectAsync(ServerUrl, policy)
.ConfigureAwait(false);
try
{
Assert.That(session.Connected, Is.True,
$"Failed to connect with policy {policy}");
}
finally
{
await session.CloseAsync(5000, true)
.ConfigureAwait(false);
session.Dispose();
}
}
catch (ServiceResultException sre)
when (sre.StatusCode == StatusCodes.BadConnectionClosed ||
sre.StatusCode == StatusCodes.BadNotConnected)
{
Assert.Ignore("Secure connection with policy " +
policy +
" failed: " +
sre.StatusCode.ToString());
}
}
}
[Test]
public async Task EachSessionHasUniqueSessionIdAsync()
{
var sessionIds = new List();
for (int i = 0; i < 3; i++)
{
ISession session;
try
{
session = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Connection rejected: {sre.StatusCode}");
return;
}
try
{
sessionIds.Add(session.SessionId);
}
finally
{
await session.CloseAsync(5000, true)
.ConfigureAwait(false);
session.Dispose();
}
}
Assert.That(sessionIds, Has.Count.EqualTo(3));
Assert.That(sessionIds.Distinct().Count(), Is.EqualTo(3),
"Each session should have a unique SessionId.");
}
[Test]
public async Task SessionTimeoutIsPositiveAsync()
{
ISession session;
try
{
session = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Connection rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.SessionTimeout,
Is.GreaterThan(0),
"Revised session timeout should be positive.");
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ActivateWithEmptyUsernameIsRejectedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
// Use the non-retrying ConnectAsync overload to avoid repeated
// failed login attempts that could lock out accounts.
ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(
ServerUrl, SecurityPolicies.None, endpoints).ConfigureAwait(false);
try
{
ISession session = await ClientFixture.ConnectAsync(
endpoint,
new UserIdentity(string.Empty, "demo"u8))
.ConfigureAwait(false);
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
Assert.Fail("Expected rejection for empty username.");
}
catch (ServiceResultException sre)
{
Assert.That(
sre.StatusCode == StatusCodes.BadIdentityTokenInvalid ||
sre.StatusCode == StatusCodes.BadIdentityTokenRejected ||
sre.StatusCode == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected rejection, got {sre.StatusCode}");
}
}
[Test]
public async Task ActivateWithVeryLongUsernameIsRejectedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(
ServerUrl, SecurityPolicies.None, endpoints).ConfigureAwait(false);
string longUsername = new('x', 1000);
try
{
ISession session = await ClientFixture.ConnectAsync(
endpoint,
new UserIdentity(longUsername, "demo"u8))
.ConfigureAwait(false);
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
Assert.Fail("Expected rejection for very long username.");
}
catch (ServiceResultException sre)
{
Assert.That(
sre.StatusCode == StatusCodes.BadIdentityTokenRejected ||
sre.StatusCode == StatusCodes.BadIdentityTokenInvalid ||
sre.StatusCode == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected rejection, got {sre.StatusCode}");
}
}
[Test]
public async Task ActivateWithVeryLongPasswordIsRejectedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(
ServerUrl, SecurityPolicies.None, endpoints).ConfigureAwait(false);
byte[] longPassword = new byte[1000];
for (int i = 0; i < longPassword.Length; i++)
{
longPassword[i] = (byte)'p';
}
try
{
ISession session = await ClientFixture.ConnectAsync(
endpoint,
new UserIdentity("sysadmin", longPassword))
.ConfigureAwait(false);
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
Assert.Fail("Expected rejection for very long password.");
}
catch (ServiceResultException sre)
{
Assert.That(
sre.StatusCode == StatusCodes.BadIdentityTokenRejected ||
sre.StatusCode == StatusCodes.BadIdentityTokenInvalid ||
sre.StatusCode == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected rejection, got {sre.StatusCode}");
}
}
[Test]
public async Task ActivateWithSpecialCharsInUsernameIsRejectedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(
ServerUrl, SecurityPolicies.None, endpoints).ConfigureAwait(false);
try
{
ISession session = await ClientFixture.ConnectAsync(
endpoint,
new UserIdentity(
"user<>&\"'!@#$%^", "demo"u8))
.ConfigureAwait(false);
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
Assert.Fail("Expected rejection for unknown user.");
}
catch (ServiceResultException sre)
{
Assert.That(
sre.StatusCode == StatusCodes.BadIdentityTokenRejected ||
sre.StatusCode == StatusCodes.BadIdentityTokenInvalid ||
sre.StatusCode == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected rejection, got {sre.StatusCode}");
}
}
[Test]
public async Task ActivateWithUnicodePasswordIsRejectedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ConfiguredEndpoint endpoint = await ClientFixture.GetEndpointAsync(
ServerUrl, SecurityPolicies.None, endpoints).ConfigureAwait(false);
try
{
ISession session = await ClientFixture.ConnectAsync(
endpoint,
new UserIdentity(
"sysadmin", "\u00e9\u00e8\u00ea\u00eb\u4e16\u754c"u8))
.ConfigureAwait(false);
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
Assert.Fail("Expected rejection for wrong password.");
}
catch (ServiceResultException sre)
{
Assert.That(
sre.StatusCode == StatusCodes.BadIdentityTokenRejected ||
sre.StatusCode == StatusCodes.BadIdentityTokenInvalid ||
sre.StatusCode == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected rejection, got {sre.StatusCode}");
}
}
[Test]
public async Task SwitchFromAnonymousToUserNameMidSessionAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture
.ConnectAsync(ServerUrl, SecurityPolicies.None)
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Anonymous connect rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.Connected, Is.True);
await session.UpdateSessionAsync(
new UserIdentity("sysadmin", "demo"u8),
session.PreferredLocales)
.ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Switch rejected: {sre.StatusCode}");
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task SwitchFromUserNameToAnonymousMidSessionAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
await session.UpdateSessionAsync(
new UserIdentity(),
session.PreferredLocales).ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Switch rejected: {sre.StatusCode}");
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task SwitchFromOneUserToAnotherMidSessionAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Ignore("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Ignore($"Rejected: {sre.StatusCode}");
return;
}
try
{
await session.UpdateSessionAsync(
new UserIdentity("appuser", "demo"u8),
session.PreferredLocales)
.ConfigureAwait(false);
Assert.That(session.Connected, Is.True);
}
catch (ServiceResultException sre)
{
Assert.Ignore($"Switch rejected: {sre.StatusCode}");
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ActivateCorrectCredentialsOnNoneAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ActivateCorrectCredentialsOnSignAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
EndpointDescription signEp = FindEndpoint(
endpoints, MessageSecurityMode.Sign);
if (signEp == null)
{
Assert.Fail("No Sign endpoint available.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, signEp!.SecurityPolicyUri!,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task ActivateCorrectCredentialsOnSignAndEncryptAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
EndpointDescription encryptEp = FindEndpoint(
endpoints, MessageSecurityMode.SignAndEncrypt);
if (encryptEp == null)
{
Assert.Fail("No SignAndEncrypt endpoint available.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, encryptEp!.SecurityPolicyUri!,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
Assert.That(session.Connected, Is.True);
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task UserNameTokenPolicyIdMatchesAdvertisedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
foreach (EndpointDescription ep in endpoints)
{
if (ep.UserIdentityTokens == default!)
{
continue;
}
foreach (UserTokenPolicy tokenPolicy in ep.UserIdentityTokens)
{
if (tokenPolicy.TokenType == UserTokenType.UserName)
{
Assert.That(tokenPolicy.PolicyId,
Is.Not.Null.And.Not.Empty,
"UserName token policy must have a PolicyId.");
}
}
}
}
[Test]
public async Task SysadminCanReadAdminRestrictedNodeAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Fail("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("sysadmin", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Fail($"Rejected: {sre.StatusCode}");
return;
}
try
{
// Read server status as admin
ReadResponse rr = await session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus_State,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(rr.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(rr.Results[0].StatusCode), Is.True,
"Sysadmin should be able to read ServerStatus.State.");
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
[Test]
public async Task AppuserWriteToAdminNodeDeniedAsync()
{
ArrayOf endpoints = await GetEndpointsAsync().ConfigureAwait(false);
if (!EndpointsHaveUsernameToken(endpoints))
{
Assert.Ignore("No Username token advertised.");
}
ISession session;
try
{
session = await ClientFixture.ConnectAsync(
ServerUrl, SecurityPolicies.None,
userIdentity: new UserIdentity("appuser", "demo"u8))
.ConfigureAwait(false);
}
catch (ServiceResultException sre)
{
Assert.Ignore($"Rejected: {sre.StatusCode}");
return;
}
try
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
WriteResponse wr = await session.WriteAsync(
null,
new WriteValue[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.Value,
Value = new DataValue(Variant.From(999))
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(wr.Results.Count, Is.EqualTo(1));
Assert.That(
wr.Results[0].Code == StatusCodes.Good ||
wr.Results[0].Code == StatusCodes.BadUserAccessDenied,
Is.True,
$"Expected Good or BadUserAccessDenied, got {wr.Results[0]}");
}
finally
{
await session.CloseAsync(5000, true).ConfigureAwait(false);
session.Dispose();
}
}
private async Task> GetEndpointsAsync()
{
var endpointConfiguration =
EndpointConfiguration.Create(ClientFixture.Config);
using DiscoveryClient client = await DiscoveryClient.CreateAsync(
ServerUrl,
endpointConfiguration,
Telemetry,
ct: CancellationToken.None).ConfigureAwait(false);
return await client.GetEndpointsAsync(
default, CancellationToken.None).ConfigureAwait(false);
}
private bool EndpointsHaveUsernameToken(
ArrayOf endpoints)
{
foreach (EndpointDescription ep in endpoints)
{
if (ep.UserIdentityTokens != default!)
{
foreach (UserTokenPolicy t in ep.UserIdentityTokens)
{
if (t.TokenType == UserTokenType.UserName)
{
return true;
}
}
}
}
return false;
}
private EndpointDescription FindEndpoint(
ArrayOf endpoints,
MessageSecurityMode mode)
{
foreach (EndpointDescription ep in endpoints)
{
if (ep.SecurityMode == mode)
{
return ep;
}
}
return null!;
}
}
}