/* ========================================================================
* 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.Security.Cryptography.X509Certificates;
using System.Threading;
using Microsoft.Extensions.Logging;
namespace Opc.Ua.Server
{
///
/// A generic session manager object for a server.
///
public class Session : ISession
{
///
/// Initializes a new instance of the class.
///
/// The context.
/// The Server object.
/// The server certificate.
/// The unique private identifier assigned to the Session.
/// The client nonce.
/// The server nonce.
/// The name assigned to the Session.
/// Application description for the client application.
/// The endpoint URL.
/// The client certificate.
/// The client certifiate chain
/// The session timeout.
/// The maximum number of browse continuation points.
/// The maximum number of history continuation points.
public Session(
OperationContext context,
IServerInternal server,
X509Certificate2 serverCertificate,
NodeId authenticationToken,
byte[] clientNonce,
Nonce serverNonce,
string sessionName,
ApplicationDescription clientDescription,
string endpointUrl,
X509Certificate2 clientCertificate,
X509Certificate2Collection clientCertificateChain,
double sessionTimeout,
int maxBrowseContinuationPoints,
int maxHistoryContinuationPoints)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// verify that a secure channel was specified.
if (context.ChannelContext == null)
{
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
}
m_server = server ?? throw new ArgumentNullException(nameof(server));
m_logger = server.Telemetry.CreateLogger();
ClientNonce = clientNonce;
m_serverNonce = serverNonce;
m_sessionName = sessionName;
m_serverCertificate = serverCertificate;
ClientCertificate = clientCertificate;
m_clientIssuerCertificates = clientCertificateChain;
SecureChannelId = context.ChannelContext.SecureChannelId;
MaxBrowseContinuationPoints = maxBrowseContinuationPoints;
m_maxHistoryContinuationPoints = maxHistoryContinuationPoints;
EndpointDescription = context.ChannelContext.EndpointDescription;
// use anonymous the default identity.
Identity = new UserIdentity();
// initialize diagnostics.
DateTime now = DateTime.UtcNow;
SessionDiagnostics = new SessionDiagnosticsDataType
{
SessionId = default,
SessionName = sessionName,
ClientDescription = clientDescription,
ServerUri = null,
EndpointUrl = endpointUrl,
ActualSessionTimeout = sessionTimeout,
ClientConnectionTime = now,
ClientLastContactTime = now
};
// initialize security diagnostics.
m_securityDiagnostics = new SessionSecurityDiagnosticsDataType
{
SessionId = Id,
ClientUserIdOfSession = Identity.DisplayName,
AuthenticationMechanism = Identity.TokenType.ToString(),
Encoding = context.ChannelContext.MessageEncoding.ToString()
};
m_securityDiagnostics.ClientUserIdHistory.Add(Identity.DisplayName);
EndpointDescription description = context.ChannelContext.EndpointDescription;
if (description != null)
{
m_securityDiagnostics.TransportProtocol = new Uri(description.EndpointUrl).Scheme;
m_securityDiagnostics.SecurityMode = EndpointDescription.SecurityMode;
m_securityDiagnostics.SecurityPolicyUri = EndpointDescription.SecurityPolicyUri;
}
if (clientCertificate != null)
{
m_securityDiagnostics.ClientCertificate = clientCertificate.RawData;
}
ServerSystemContext systemContext = m_server.DefaultSystemContext.Copy(context);
// create diagnostics object.
Id = server.DiagnosticsNodeManager.CreateSessionDiagnostics(
systemContext,
SessionDiagnostics,
OnUpdateDiagnostics,
m_securityDiagnostics,
OnUpdateSecurityDiagnostics);
TraceState("CREATED");
}
///
/// Frees any unmanaged resources.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// An overrideable version of the Dispose.
///
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
List browseCPs = null;
lock (m_lock)
{
browseCPs = m_browseContinuationPoints;
m_browseContinuationPoints = null;
}
if (browseCPs != null)
{
for (int ii = 0; ii < browseCPs.Count; ii++)
{
Utils.SilentDispose(browseCPs[ii]);
}
}
List historyCPs = null;
lock (m_lock)
{
historyCPs = m_historyContinuationPoints;
m_historyContinuationPoints = null;
}
if (historyCPs != null)
{
for (int ii = 0; ii < historyCPs.Count; ii++)
{
Utils.SilentDispose(historyCPs[ii].Value);
}
}
}
}
///
/// Gets the identifier assigned to the session when it was created.
///
public NodeId Id { get; }
///
/// The user identity provided by the client.
///
public IUserIdentity Identity { get; private set; }
///
/// The application defined mapping for user identity provided by the client.
///
public IUserIdentity EffectiveIdentity { get; private set; }
///
/// The user identity token provided by the client.
///
public UserIdentityToken IdentityToken { get; private set; }
///
/// A lock which must be acquired before accessing the diagnostics.
///
public object DiagnosticsLock => SessionDiagnostics;
///
/// The diagnostics associated with the session.
///
public SessionDiagnosticsDataType SessionDiagnostics { get; }
///
/// The client Nonce associated with the session.
///
public byte[] ClientNonce { get; }
///
/// The application instance certificate associated with the client.
///
public X509Certificate2 ClientCertificate { get; }
///
/// The locales requested when the session was created.
///
public string[] PreferredLocales { get; private set; }
///
/// Whether the session timeout has elapsed since the last communication from the client.
///
public bool HasExpired
{
get
{
lock (DiagnosticsLock)
{
return SessionDiagnostics.ClientLastContactTime.AddMilliseconds(
SessionDiagnostics.ActualSessionTimeout
) < DateTime.UtcNow;
}
}
}
///
/// The last time the session was contacted by the client.
///
public DateTime ClientLastContactTime
{
get
{
lock (DiagnosticsLock)
{
return SessionDiagnostics.ClientLastContactTime;
}
}
}
///
/// Whether the session has been activated.
///
public bool Activated { get; private set; }
///
/// Set the ECC security policy URI
///
public virtual void SetEccUserTokenSecurityPolicy(string securityPolicyUri)
{
lock (m_lock)
{
m_eccUserTokenSecurityPolicyUri = securityPolicyUri;
m_eccUserTokenNonce = null;
}
}
///
/// Create new ECC ephemeral key
///
/// A new ephemeral key
public virtual EphemeralKeyType GetNewEccKey()
{
lock (m_lock)
{
if (m_eccUserTokenSecurityPolicyUri == null)
{
return null;
}
m_eccUserTokenNonce = Nonce.CreateNonce(m_eccUserTokenSecurityPolicyUri);
var key = new EphemeralKeyType { PublicKey = m_eccUserTokenNonce.Data };
key.Signature = EccUtils.Sign(
new ArraySegment(key.PublicKey),
m_serverCertificate,
m_eccUserTokenSecurityPolicyUri);
return key;
}
}
///
/// Returns the session's endpoint
///
public EndpointDescription EndpointDescription { get; }
///
/// Returns the session's SecureChannelId
///
public string SecureChannelId { get; private set; }
///
/// allow derived classes access
///
protected int MaxBrowseContinuationPoints { get; set; }
///
/// Validates the request.
///
/// is null.
///
public virtual void ValidateRequest(RequestHeader requestHeader, SecureChannelContext secureChannelContext, RequestType requestType)
{
if (requestHeader == null)
{
throw new ArgumentNullException(nameof(requestHeader));
}
lock (m_lock)
{
if (secureChannelContext == null || !IsSecureChannelValid(secureChannelContext.SecureChannelId))
{
UpdateDiagnosticCounters(requestType, true, true);
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
}
// verify that session has been activated.
if (!Activated && requestType != RequestType.CloseSession)
{
UpdateDiagnosticCounters(requestType, true, true);
throw new ServiceResultException(StatusCodes.BadSessionNotActivated);
}
// request accepted.
UpdateDiagnosticCounters(requestType, false, false);
}
}
///
/// Validate the diagnostic info.
///
public virtual void ValidateDiagnosticInfo(RequestHeader requestHeader)
{
const uint additionalInfoDiagnosticsMask = (uint)(
DiagnosticsMasks.ServiceAdditionalInfo | DiagnosticsMasks.OperationAdditionalInfo);
if ((requestHeader.ReturnDiagnostics & additionalInfoDiagnosticsMask) != 0)
{
NodeIdCollection currentRoleIds = EffectiveIdentity?.GrantedRoleIds;
if ((currentRoleIds?.Contains(ObjectIds.WellKnownRole_SecurityAdmin)) == true ||
(currentRoleIds?.Contains(ObjectIds.WellKnownRole_ConfigureAdmin)) == true)
{
requestHeader.ReturnDiagnostics
|= (uint)DiagnosticsMasks.UserPermissionAdditionalInfo;
}
}
}
///
/// Checks if the secure channel is currently valid.
///
public virtual bool IsSecureChannelValid(string secureChannelId)
{
lock (m_lock)
{
return SecureChannelId == secureChannelId;
}
}
///
/// Updates the requested locale ids.
///
/// true if the new locale ids are different from the old locale ids.
/// is null.
public bool UpdateLocaleIds(StringCollection localeIds)
{
if (localeIds == null)
{
throw new ArgumentNullException(nameof(localeIds));
}
lock (m_lock)
{
string[] ids = [.. localeIds];
if (!Utils.IsEqual(ids, PreferredLocales))
{
PreferredLocales = ids;
// update diagnostics.
lock (DiagnosticsLock)
{
SessionDiagnostics.LocaleIds = [.. localeIds];
}
return true;
}
return false;
}
}
///
/// Activates the session and binds it to the current secure channel.
///
///
public void ValidateBeforeActivate(
OperationContext context,
SignatureData clientSignature,
ExtensionObject userIdentityToken,
SignatureData userTokenSignature,
out UserIdentityToken identityToken,
out UserTokenPolicy userTokenPolicy)
{
lock (m_lock)
{
// verify that a secure channel was specified.
if (context.ChannelContext == null)
{
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
}
// verify that the same security policy has been used.
EndpointDescription endpoint = context.ChannelContext.EndpointDescription;
if (endpoint.SecurityPolicyUri != EndpointDescription.SecurityPolicyUri ||
endpoint.SecurityMode != EndpointDescription.SecurityMode)
{
throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
}
// verify the client signature.
if (ClientCertificate != null)
{
if (EndpointDescription.SecurityPolicyUri != SecurityPolicies.None &&
clientSignature != null &&
clientSignature.Signature == null)
{
throw new ServiceResultException(
StatusCodes.BadApplicationSignatureInvalid);
}
byte[] dataToSign = Utils.Append(
m_serverCertificate.RawData,
m_serverNonce.Data);
if (!SecurityPolicies.Verify(
ClientCertificate,
EndpointDescription.SecurityPolicyUri,
dataToSign,
clientSignature))
{
// verify for certificate chain in endpoint.
// validate the signature with complete chain if the check with leaf certificate failed.
X509Certificate2Collection serverCertificateChain =
Utils.ParseCertificateChainBlob(
EndpointDescription.ServerCertificate,
m_server.Telemetry);
if (serverCertificateChain.Count > 1)
{
var serverCertificateChainList = new List();
for (int i = 0; i < serverCertificateChain.Count; i++)
{
serverCertificateChainList.AddRange(
serverCertificateChain[i].RawData);
}
byte[] serverCertificateChainData = [.. serverCertificateChainList];
dataToSign = Utils.Append(
serverCertificateChainData,
m_serverNonce.Data);
if (!SecurityPolicies.Verify(
ClientCertificate,
EndpointDescription.SecurityPolicyUri,
dataToSign,
clientSignature))
{
throw new ServiceResultException(
StatusCodes.BadApplicationSignatureInvalid);
}
}
else
{
throw new ServiceResultException(
StatusCodes.BadApplicationSignatureInvalid);
}
}
}
if (!Activated)
{
// must active the session on the channel that was used to create it.
if (SecureChannelId != context.ChannelContext.SecureChannelId)
{
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
}
}
// validate the user identity token.
identityToken = ValidateUserIdentityToken(
userIdentityToken,
userTokenSignature,
out userTokenPolicy);
TraceState("VALIDATED");
}
}
///
/// Activates the session and binds it to the current secure channel.
///
public bool Activate(
OperationContext context,
UserIdentityToken identityToken,
IUserIdentity identity,
IUserIdentity effectiveIdentity,
StringCollection localeIds,
Nonce serverNonce)
{
lock (m_lock)
{
// update user identity.
bool changed = false;
if (identityToken != null &&
UpdateUserIdentity(identityToken, identity, effectiveIdentity))
{
changed = true;
}
// update local ids.
if (UpdateLocaleIds(localeIds))
{
changed = true;
}
if (!Activated)
{
// toggle the activated flag.
Activated = true;
TraceState("FIRST ACTIVATION");
}
else
{
// bind to the new secure channel.
SecureChannelId = context.ChannelContext.SecureChannelId;
TraceState("RE-ACTIVATION");
}
// update server nonce.
m_serverNonce = serverNonce;
// update the contact time.
lock (DiagnosticsLock)
{
SessionDiagnostics.ClientLastContactTime = DateTime.UtcNow;
}
// indicate whether the user context has changed.
return changed;
}
}
///
/// Closes a session and removes itself from the address space.
///
public void Close()
{
TraceState("CLOSED");
m_server.DiagnosticsNodeManager
.DeleteSessionDiagnostics(m_server.DefaultSystemContext, Id);
}
///
/// Saves a continuation point for a session.
///
///
/// If the session has too many continuation points the oldest one is dropped.
///
/// is null.
public void SaveContinuationPoint(ContinuationPoint continuationPoint)
{
if (continuationPoint == null)
{
throw new ArgumentNullException(nameof(continuationPoint));
}
lock (m_lock)
{
m_browseContinuationPoints ??= [];
// remove the first continuation point if too many points.
while (m_browseContinuationPoints.Count >= MaxBrowseContinuationPoints)
{
ContinuationPoint cp = m_browseContinuationPoints[0];
m_browseContinuationPoints.RemoveAt(0);
Utils.SilentDispose(cp);
}
// add to end of list.
m_browseContinuationPoints.Add(continuationPoint);
}
}
///
/// Restores a continuation point for a session.
///
///
/// The caller is responsible for disposing the continuation point returned.
///
public ContinuationPoint RestoreContinuationPoint(byte[] continuationPoint)
{
lock (m_lock)
{
if (m_browseContinuationPoints == null)
{
return null;
}
if (continuationPoint == null || continuationPoint.Length != 16)
{
return null;
}
var id = new Guid(continuationPoint);
for (int ii = 0; ii < m_browseContinuationPoints.Count; ii++)
{
if (m_browseContinuationPoints[ii].Id == id)
{
ContinuationPoint cp = m_browseContinuationPoints[ii];
m_browseContinuationPoints.RemoveAt(ii);
return cp;
}
}
return null;
}
}
///
/// Saves a continuation point used for historical reads.
///
/// The identifier for the continuation point.
/// The continuation point.
///
/// If the continuationPoint implements IDisposable it will be disposed when
/// the Session is closed or discarded.
///
/// is null.
public void SaveHistoryContinuationPoint(Guid id, object continuationPoint)
{
if (continuationPoint == null)
{
throw new ArgumentNullException(nameof(continuationPoint));
}
lock (m_lock)
{
m_historyContinuationPoints ??= [];
// remove existing continuation point if space needed.
while (m_historyContinuationPoints.Count >= m_maxHistoryContinuationPoints)
{
HistoryContinuationPoint oldCP = m_historyContinuationPoints[0];
m_historyContinuationPoints.RemoveAt(0);
Utils.SilentDispose(oldCP.Value);
}
// create the cp.
var cp = new HistoryContinuationPoint
{
Id = id,
Value = continuationPoint,
Timestamp = DateTime.UtcNow
};
m_historyContinuationPoints.Add(cp);
}
}
///
/// Saves a continuation point used for historical reads unless the request
/// that creates it already holds all continuation points of the session.
///
///
/// OPC 10000-4 7.9: "A Server shall process the operations until it uses the
/// maximum number of continuation points in this response. Once that happens
/// the Server shall return a Bad_NoContinuationPoints error for any remaining
/// operations." and "A Server shall automatically free ContinuationPoints from
/// prior requests from a Session if they are needed to process a new request
/// from this Session."
/// When the session holds MaxHistoryContinuationPoints points, the oldest point
/// of a prior request is freed. If all stored points belong to the request
/// identified by , nothing is freed or saved and
/// the method returns false; the caller then reports
/// Bad_NoContinuationPoints for the operation. A of
/// 0 is not associated with a request and behaves like
/// .
/// If the continuationPoint implements IDisposable it will be disposed when
/// it is freed or when the Session is closed or discarded.
///
/// The identifier for the continuation point.
/// The continuation point.
/// The of the request.
/// true if the continuation point was saved.
/// is null.
public bool TrySaveHistoryContinuationPoint(Guid id, object continuationPoint, uint requestId)
{
if (continuationPoint == null)
{
throw new ArgumentNullException(nameof(continuationPoint));
}
lock (m_lock)
{
m_historyContinuationPoints ??= [];
while (m_maxHistoryContinuationPoints > 0 &&
m_historyContinuationPoints.Count >= m_maxHistoryContinuationPoints)
{
int index = m_historyContinuationPoints.FindIndex(
cp => requestId == 0 || cp.RequestId != requestId);
if (index < 0)
{
return false;
}
HistoryContinuationPoint oldCP = m_historyContinuationPoints[index];
m_historyContinuationPoints.RemoveAt(index);
Utils.SilentDispose(oldCP.Value);
}
m_historyContinuationPoints.Add(new HistoryContinuationPoint
{
Id = id,
Value = continuationPoint,
Timestamp = DateTime.UtcNow,
RequestId = requestId
});
return true;
}
}
///
/// Restores a previously saves history continuation point.
///
/// The identifier for the continuation point.
/// The save continuation point. null if not found.
public object RestoreHistoryContinuationPoint(byte[] continuationPoint)
{
lock (m_lock)
{
if (m_historyContinuationPoints == null)
{
return null;
}
if (continuationPoint == null || continuationPoint.Length != 16)
{
return null;
}
var id = new Guid(continuationPoint);
for (int ii = 0; ii < m_historyContinuationPoints.Count; ii++)
{
HistoryContinuationPoint cp = m_historyContinuationPoints[ii];
if (cp.Id == id)
{
m_historyContinuationPoints.RemoveAt(ii);
return cp.Value;
}
}
return null;
}
}
///
/// Stores a continuation point used for historial reads.
///
private class HistoryContinuationPoint
{
public Guid Id;
public object Value;
public DateTime Timestamp;
public uint RequestId;
}
///
/// Dumps the current state of the session queue.
///
internal void TraceState(string context)
{
// Legacy event source logging
ServerUtils.EventLog.SessionState(
context,
Id.ToString(),
m_sessionName,
SecureChannelId,
Identity?.DisplayName ?? "(none)");
m_logger.LogInformation(
"Session {Context}, Id={SessionId}, Name={Name}, ChannelId={ChannelId}, User={User}",
context,
Id.ToString(),
m_sessionName,
SecureChannelId,
Identity?.DisplayName ?? "(none)");
}
///
/// Returns a copy of the current diagnostics.
///
private ServiceResult OnUpdateDiagnostics(
ISystemContext context,
NodeState node,
ref object value)
{
lock (DiagnosticsLock)
{
value = Utils.Clone(SessionDiagnostics);
}
return ServiceResult.Good;
}
///
/// Returns a copy of the current security diagnostics.
///
private ServiceResult OnUpdateSecurityDiagnostics(
ISystemContext context,
NodeState node,
ref object value)
{
lock (DiagnosticsLock)
{
value = Utils.Clone(m_securityDiagnostics);
}
return ServiceResult.Good;
}
///
/// Validates the identity token supplied by the client.
///
///
private UserIdentityToken ValidateUserIdentityToken(
ExtensionObject identityToken,
SignatureData userTokenSignature,
out UserTokenPolicy policy)
{
policy = null;
// check for empty token.
if (identityToken == null ||
identityToken.Body == null ||
identityToken.Body.GetType() == typeof(AnonymousIdentityToken))
{
// check if an anonymous login is permitted.
if (EndpointDescription.UserIdentityTokens != null &&
EndpointDescription.UserIdentityTokens.Count > 0)
{
bool found = false;
for (int ii = 0; ii < EndpointDescription.UserIdentityTokens.Count; ii++)
{
if (EndpointDescription.UserIdentityTokens[ii]
.TokenType == UserTokenType.Anonymous)
{
found = true;
policy = EndpointDescription.UserIdentityTokens[ii];
break;
}
}
if (!found)
{
throw ServiceResultException.Create(
StatusCodes.BadIdentityTokenRejected,
"Anonymous user token policy not supported.");
}
}
// create an anonymous token to use for subsequent validation.
return new AnonymousIdentityToken { PolicyId = policy.PolicyId };
}
UserIdentityToken token;
// check for unrecognized token.
if (!typeof(UserIdentityToken).IsInstanceOfType(identityToken.Body))
{
//handle the use case when the UserIdentityToken is binary encoded over xml message encoding
if (identityToken.Encoding == ExtensionObjectEncoding.Binary &&
typeof(byte[]).IsInstanceOfType(identityToken.Body))
{
if (BaseVariableState.DecodeExtensionObject(
null,
typeof(UserIdentityToken),
identityToken,
false)
is not UserIdentityToken newToken)
{
throw ServiceResultException.Create(
StatusCodes.BadUserAccessDenied,
"Invalid user identity token provided.");
}
policy = EndpointDescription.FindUserTokenPolicy(
newToken.PolicyId,
EndpointDescription.SecurityPolicyUri);
if (policy == null)
{
throw ServiceResultException.Create(
StatusCodes.BadUserAccessDenied,
"User token policy not supported.",
"Opc.Ua.Server.Session.ValidateUserIdentityToken");
}
switch (policy.TokenType)
{
case UserTokenType.Anonymous:
token =
BaseVariableState.DecodeExtensionObject(
null,
typeof(AnonymousIdentityToken),
identityToken,
true
) as AnonymousIdentityToken;
break;
case UserTokenType.UserName:
token =
BaseVariableState.DecodeExtensionObject(
null,
typeof(UserNameIdentityToken),
identityToken,
true
) as UserNameIdentityToken;
break;
case UserTokenType.Certificate:
token =
BaseVariableState.DecodeExtensionObject(
null,
typeof(X509IdentityToken),
identityToken,
true
) as X509IdentityToken;
break;
case UserTokenType.IssuedToken:
token =
BaseVariableState.DecodeExtensionObject(
null,
typeof(IssuedIdentityToken),
identityToken,
true
) as IssuedIdentityToken;
break;
default:
throw ServiceResultException.Create(
StatusCodes.BadUserAccessDenied,
"Invalid user identity token provided.");
}
}
else
{
throw ServiceResultException.Create(
StatusCodes.BadUserAccessDenied,
"Invalid user identity token provided.");
}
}
else
{
// get the token.
token = (UserIdentityToken)identityToken.Body;
}
// find the user token policy.
policy = EndpointDescription.FindUserTokenPolicy(
token.PolicyId,
EndpointDescription.SecurityPolicyUri);
if (policy == null)
{
throw ServiceResultException.Create(
StatusCodes.BadIdentityTokenInvalid,
"User token policy not supported.");
}
if (token is IssuedIdentityToken issuedToken &&
policy.IssuedTokenType == Profiles.JwtUserToken)
{
issuedToken.IssuedTokenType = IssuedTokenType.JWT;
}
// determine the security policy uri.
string securityPolicyUri = policy.SecurityPolicyUri;
if (string.IsNullOrEmpty(securityPolicyUri))
{
securityPolicyUri = EndpointDescription.SecurityPolicyUri;
}
if (ServerBase.RequireEncryption(EndpointDescription))
{
// decrypt the token.
if (m_serverCertificate == null)
{
m_serverCertificate = X509CertificateLoader.LoadCertificate(
EndpointDescription.ServerCertificate);
// check for valid certificate.
if (m_serverCertificate == null)
{
throw ServiceResultException.Create(
StatusCodes.BadConfigurationError,
"ApplicationCertificate cannot be found.");
}
}
try
{
token.Decrypt(
m_serverCertificate,
m_serverNonce,
securityPolicyUri,
m_server.MessageContext,
m_eccUserTokenNonce,
ClientCertificate,
m_clientIssuerCertificates);
}
catch (Exception e)
{
// Every decryption, padding, length or nonce error must look the
// same to the client (OPC 10000-4 7.40.2.1). The cause is logged
// here only: returning it as the status or the inner result would
// let a client tell the failures apart and use the server key as
// a decryption oracle.
m_logger.LogWarning(
e,
"Could not decrypt the user identity token of session {SessionId} (SecurityPolicyUri={SecurityPolicyUri}).",
Id,
securityPolicyUri);
throw ServiceResultException.Create(
StatusCodes.BadIdentityTokenInvalid,
"Could not decrypt identity token.");
}
// verify the signature.
if (securityPolicyUri != SecurityPolicies.None)
{
byte[] dataToSign = Utils.Append(
m_serverCertificate.RawData,
m_serverNonce.Data);
if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri, m_server.Telemetry))
{
// verify for certificate chain in endpoint.
// validate the signature with complete chain if the check with leaf certificate failed.
X509Certificate2Collection serverCertificateChain =
Utils.ParseCertificateChainBlob(
EndpointDescription.ServerCertificate,
m_server.Telemetry);
if (serverCertificateChain.Count > 1)
{
var serverCertificateChainList = new List();
for (int i = 0; i < serverCertificateChain.Count; i++)
{
serverCertificateChainList.AddRange(
serverCertificateChain[i].RawData);
}
byte[] serverCertificateChainData = [.. serverCertificateChainList];
dataToSign = Utils.Append(
serverCertificateChainData,
m_serverNonce.Data);
if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri, m_server.Telemetry))
{
throw new ServiceResultException(
StatusCodes.BadIdentityTokenRejected,
"Invalid user signature!");
}
}
else
{
throw new ServiceResultException(
StatusCodes.BadIdentityTokenRejected,
"Invalid user signature!");
}
}
}
}
// validate user identity token.
return token;
}
///
/// Updates the user identity.
///
/// true if the new identity is different from the old identity.
/// is null.
private bool UpdateUserIdentity(
UserIdentityToken identityToken,
IUserIdentity identity,
IUserIdentity effectiveIdentity)
{
if (identityToken == null)
{
throw new ArgumentNullException(nameof(identityToken));
}
lock (m_lock)
{
bool changed = EffectiveIdentity == null && effectiveIdentity != null;
if (EffectiveIdentity != null)
{
changed = !EffectiveIdentity.Equals(effectiveIdentity);
}
// always save the new identity since it may have additional information that does not affect equality.
IdentityToken = identityToken;
Identity = identity;
EffectiveIdentity = effectiveIdentity;
// update diagnostics.
lock (DiagnosticsLock)
{
m_securityDiagnostics.ClientUserIdOfSession = identity.DisplayName;
m_securityDiagnostics.AuthenticationMechanism = identity.TokenType.ToString();
m_securityDiagnostics.ClientUserIdHistory.Add(identity.DisplayName);
}
return changed;
}
}
///
/// Updates the diagnostic counters associated with the request.
///
///
private void UpdateDiagnosticCounters(
RequestType requestType,
bool error,
bool authorizationError)
{
ServiceCounterDataType counter = null;
lock (DiagnosticsLock)
{
if (!error)
{
SessionDiagnostics.ClientLastContactTime = DateTime.UtcNow;
}
SessionDiagnostics.TotalRequestCount.TotalCount++;
if (error)
{
SessionDiagnostics.TotalRequestCount.ErrorCount++;
if (authorizationError)
{
SessionDiagnostics.UnauthorizedRequestCount++;
}
}
switch (requestType)
{
case RequestType.Read:
counter = SessionDiagnostics.ReadCount;
break;
case RequestType.HistoryRead:
counter = SessionDiagnostics.HistoryReadCount;
break;
case RequestType.Write:
counter = SessionDiagnostics.WriteCount;
break;
case RequestType.HistoryUpdate:
counter = SessionDiagnostics.HistoryUpdateCount;
break;
case RequestType.Call:
counter = SessionDiagnostics.CallCount;
break;
case RequestType.CreateMonitoredItems:
counter = SessionDiagnostics.CreateMonitoredItemsCount;
break;
case RequestType.ModifyMonitoredItems:
counter = SessionDiagnostics.ModifyMonitoredItemsCount;
break;
case RequestType.SetMonitoringMode:
counter = SessionDiagnostics.SetMonitoringModeCount;
break;
case RequestType.SetTriggering:
counter = SessionDiagnostics.SetTriggeringCount;
break;
case RequestType.DeleteMonitoredItems:
counter = SessionDiagnostics.DeleteMonitoredItemsCount;
break;
case RequestType.CreateSubscription:
counter = SessionDiagnostics.CreateSubscriptionCount;
break;
case RequestType.ModifySubscription:
counter = SessionDiagnostics.ModifySubscriptionCount;
break;
case RequestType.SetPublishingMode:
counter = SessionDiagnostics.SetPublishingModeCount;
break;
case RequestType.Publish:
counter = SessionDiagnostics.PublishCount;
break;
case RequestType.Republish:
counter = SessionDiagnostics.RepublishCount;
break;
case RequestType.TransferSubscriptions:
counter = SessionDiagnostics.TransferSubscriptionsCount;
break;
case RequestType.DeleteSubscriptions:
counter = SessionDiagnostics.DeleteSubscriptionsCount;
break;
case RequestType.AddNodes:
counter = SessionDiagnostics.AddNodesCount;
break;
case RequestType.AddReferences:
counter = SessionDiagnostics.AddReferencesCount;
break;
case RequestType.DeleteNodes:
counter = SessionDiagnostics.DeleteNodesCount;
break;
case RequestType.DeleteReferences:
counter = SessionDiagnostics.DeleteReferencesCount;
break;
case RequestType.Browse:
counter = SessionDiagnostics.BrowseCount;
break;
case RequestType.BrowseNext:
counter = SessionDiagnostics.BrowseNextCount;
break;
case RequestType.TranslateBrowsePathsToNodeIds:
counter = SessionDiagnostics.TranslateBrowsePathsToNodeIdsCount;
break;
case RequestType.QueryFirst:
counter = SessionDiagnostics.QueryFirstCount;
break;
case RequestType.QueryNext:
counter = SessionDiagnostics.QueryNextCount;
break;
case RequestType.RegisterNodes:
counter = SessionDiagnostics.RegisterNodesCount;
break;
case RequestType.UnregisterNodes:
counter = SessionDiagnostics.UnregisterNodesCount;
break;
case RequestType.Unknown:
case RequestType.FindServers:
case RequestType.GetEndpoints:
case RequestType.CreateSession:
case RequestType.ActivateSession:
case RequestType.CloseSession:
case RequestType.Cancel:
break;
default:
throw ServiceResultException.Unexpected(
$"Unexpected RequestType {requestType}");
}
if (counter != null)
{
counter.TotalCount++;
if (error)
{
counter.ErrorCount++;
}
}
}
if (counter != null)
{
m_server.SessionManager.RaiseSessionDiagnosticsChangedEvent(this);
}
}
private readonly Lock m_lock = new();
private readonly ILogger m_logger;
private readonly IServerInternal m_server;
private readonly string m_sessionName;
private X509Certificate2 m_serverCertificate;
private Nonce m_serverNonce;
private string m_eccUserTokenSecurityPolicyUri;
private Nonce m_eccUserTokenNonce;
private readonly X509Certificate2Collection m_clientIssuerCertificates;
private readonly int m_maxHistoryContinuationPoints;
private readonly SessionSecurityDiagnosticsDataType m_securityDiagnostics;
private List m_browseContinuationPoints;
private List m_historyContinuationPoints;
}
}