/* ======================================================================== * 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.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Opc.Ua; using Opc.Ua.Identity; using Opc.Ua.Security.Certificates; using Opc.Ua.Server; using Opc.Ua.Server.Hosting; using Opc.Ua.Server.UserDatabase; using Opc.Ua.Server.UserManagement; namespace Microsoft.Extensions.DependencyInjection { /// /// extensions provided by /// Opc.Ua.Server: register an OPC UA server hosted as an /// so the .NET Generic Host owns its /// lifetime, logging pipeline and Ctrl+C / SIGTERM handling. /// public static class OpcUaServerBuilderExtensions { /// /// Default section name used by the /// overload. /// public const string DefaultConfigurationSection = "OpcUa:Server"; /// /// Registers an OPC UA server hosted as an /// and returns an for chaining /// node-manager registrations. /// /// /// /// The hosted service builds an /// from the supplied , ensures the /// application instance certificate is present, attaches every /// resolved from /// DI, then starts a . Stop is signalled /// by the host. /// /// /// Calling this method twice on the same /// throws /// : at most one regular /// server may be registered per service collection. A normal /// server can coexist with a GDS server / LDS server / WoT /// Connectivity server registered via their own /// .AddGdsServer() / .AddLdsServer() / /// .AddWotConServer() methods. /// /// /// The OPC UA builder. /// Required callback used to populate /// . /// An for chaining. /// /// or is null. /// An OPC UA server /// is already registered. public static IOpcUaServerBuilder AddServer( this IOpcUaBuilder builder, Action configure) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (configure is null) { throw new ArgumentNullException(nameof(configure)); } EnsureFirstRegistration(builder.Services); builder.Services.AddOptions().Configure(configure); RegisterCommonServices(builder.Services, enableConfiguredIdentityAuthenticators: false); return new OpcUaServerBuilder(builder.Services); } /// /// Registers an OPC UA server hosted as an /// with options bound from the supplied /// section (OpcUa:Server). /// /// /// AOT-safe: bound by the .NET 8+ configuration binding source /// generator (EnableConfigurationBindingGenerator). /// /// The OPC UA builder. /// Configuration root containing /// the OpcUa:Server section. /// /// or is null. /// An OPC UA server /// is already registered. public static IOpcUaServerBuilder AddServer( this IOpcUaBuilder builder, IConfiguration configuration) { if (configuration is null) { throw new ArgumentNullException(nameof(configuration)); } return builder.AddServer(configuration.GetSection(DefaultConfigurationSection)); } /// /// Registers an OPC UA server hosted as an /// with options bound from the supplied . /// /// /// AOT-safe: bound by the .NET 8+ configuration binding source /// generator (EnableConfigurationBindingGenerator). /// /// The OPC UA builder. /// Configuration section to bind. /// /// or is null. /// An OPC UA server /// is already registered. public static IOpcUaServerBuilder AddServer( this IOpcUaBuilder builder, IConfigurationSection section) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (section is null) { throw new ArgumentNullException(nameof(section)); } EnsureFirstRegistration(builder.Services); builder.Services.AddOptions() .Configure(options => BindOpcUaServerOptions(options, section)); IConfigurationSection rolesSection = section.GetSection("Roles"); if (rolesSection.Exists()) { builder.Services.AddOptions().Bind(rolesSection); } IConfigurationSection identitySection = section.GetSection("Identity"); if (identitySection.Exists()) { foreach (IConfigurationSection issuerSection in identitySection.GetSection("Issuers").GetChildren()) { RegisterJwtIssuer(builder.Services, BindJwtIssuerOptions(issuerSection)); } } RegisterCommonServices(builder.Services, identitySection.Exists()); return new OpcUaServerBuilder(builder.Services); } /// /// Configures the default role manager used by the hosted OPC UA server. /// /// The server builder returned by /// . /// Callback used to populate /// . /// The same for chaining. /// /// or is null. public static IOpcUaServerBuilder ConfigureRoles( this IOpcUaServerBuilder builder, Action configure) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (configure is null) { throw new ArgumentNullException(nameof(configure)); } builder.Services.AddOptions().Configure(configure); return builder; } /// /// Configures the default role manager from a configuration section. /// /// The server builder. /// Configuration section bound to . /// The same for chaining. /// /// or is null. public static IOpcUaServerBuilder ConfigureRoles( this IOpcUaServerBuilder builder, IConfiguration section) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (section is null) { throw new ArgumentNullException(nameof(section)); } builder.Services.AddOptions().Bind(section); return builder; } /// /// Registers a server-side identity authenticator and adds it to the server registry on startup. /// /// The concrete authenticator type to register. public static IOpcUaServerBuilder AddIdentityAuthenticator< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TAuth>( this IOpcUaServerBuilder builder) where TAuth : class, IUserTokenAuthenticator { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } builder.Services.AddSingleton(); builder.Services.AddSingleton(new OpcUaServerIdentityAuthenticatorRegistration( (sp, _) => [sp.GetRequiredService()])); return builder; } /// /// Registers a server-side identity augmenter and adds it to the server registry on startup. /// /// The concrete augmenter type to register. public static IOpcUaServerBuilder AddIdentityAugmenter< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TAugmenter>( this IOpcUaServerBuilder builder) where TAugmenter : class, IIdentityAugmenter { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } builder.Services.AddSingleton(); builder.Services.AddSingleton(new OpcUaServerIdentityAugmenterRegistration( sp => sp.GetRequiredService())); return builder; } /// /// Registers a server-side identity augmenter factory and adds it to the server registry on startup. /// public static IOpcUaServerBuilder AddIdentityAugmenter( this IOpcUaServerBuilder builder, Func factory) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (factory is null) { throw new ArgumentNullException(nameof(factory)); } builder.Services.AddSingleton(new OpcUaServerIdentityAugmenterRegistration(factory)); return builder; } /// /// Enables the OPC 10000-12 ยง8 resource-server KeyCredential push model. /// public static IOpcUaServerBuilder WithKeyCredentialPush( this IOpcUaServerBuilder builder, Action? configure = null) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (configure != null) { builder.Services.AddOptions().Configure(configure); } else { builder.Services.AddOptions(); } builder.Services.TryAddSingleton(sp => new InMemoryKeyCredentialStore( sp.GetService() ?? new InMemorySecretStore("KeyCredentialPush"))); builder.Services.AddSingleton(sp => new KeyCredentialPushSubject( sp.GetRequiredService(), sp.GetRequiredService>().Value)); return builder; } /// /// Registers the built-in identity authenticators that can be resolved from DI and server state. /// public static IOpcUaServerBuilder AddDefaultIdentityAuthenticators( this IOpcUaServerBuilder builder, Action configure) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (configure is null) { throw new ArgumentNullException(nameof(configure)); } var options = new DefaultAuthenticatorOptions(); configure(options); RegisterDefaultIdentityAuthenticators(builder.Services, options); return builder; } /// /// Registers the built-in identity authenticators from a configuration section. /// /// The server builder. /// Configuration section bound to . /// The same for chaining. /// /// or is null. public static IOpcUaServerBuilder AddDefaultIdentityAuthenticators( this IOpcUaServerBuilder builder, IConfiguration section) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (section is null) { throw new ArgumentNullException(nameof(section)); } DefaultAuthenticatorOptions options = BindDefaultAuthenticatorOptions(section); RegisterDefaultIdentityAuthenticators(builder.Services, options); return builder; } /// /// Registers a trusted JWT issuer from code. /// /// /// When both and static keys are supplied, /// the registered resolver queries JWKS first and static keys second so online /// key rotation wins while inline keys remain available as a fallback. /// public static IOpcUaServerBuilder AddJwtIssuer( this IOpcUaServerBuilder builder, Action configure) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (configure is null) { throw new ArgumentNullException(nameof(configure)); } var options = new JwtIssuerOptions(); configure(options); RegisterJwtIssuer(builder.Services, options); return builder; } /// /// Registers a trusted JWT issuer from a configuration section. /// public static IOpcUaServerBuilder AddJwtIssuer( this IOpcUaServerBuilder builder, IConfiguration section) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } if (section is null) { throw new ArgumentNullException(nameof(section)); } RegisterJwtIssuer(builder.Services, BindJwtIssuerOptions(section)); return builder; } private static void RegisterDefaultIdentityAuthenticators( IServiceCollection services, DefaultAuthenticatorOptions options) { services.AddSingleton(); services.AddSingleton(new OpcUaServerIdentityAuthenticatorRegistration( (sp, certificateValidator) => CreateDefaultIdentityAuthenticators( sp, certificateValidator, options))); } private static IEnumerable CreateDefaultIdentityAuthenticators( IServiceProvider services, ICertificateValidatorEx? serverCertificateValidator, DefaultAuthenticatorOptions options) { if (options.EnableAnonymous) { yield return new AnonymousAuthenticator(); } if (options.EnableUserNamePassword) { IUserDatabase? userDatabase = options.UserDatabase ?? services.GetService(); IUserManagement? userManagement = options.UserManagement ?? services.GetService(); if (userDatabase != null && userManagement != null) { yield return new UserNamePasswordAuthenticator( userDatabase, userManagement, services.GetRequiredService()); } } if (options.EnableX509) { ICertificateValidatorEx? certificateValidator = services.GetService() ?? serverCertificateValidator; if (certificateValidator != null) { yield return new X509Authenticator( certificateValidator, options.UserCertificateTrustList); } } if (options.EnableJwt) { if (options.IssuerKeyResolver != null) { if (!string.IsNullOrEmpty(options.ExpectedAudience)) { yield return new JwtAuthenticator( options.IssuerKeyResolver, options.ExpectedAudience, options.ClockSkewTolerance); } yield break; } bool haveIssuerRegistrations = false; foreach (JwtIssuerRegistration registration in services.GetServices()) { haveIssuerRegistrations = true; string? audience = registration.Audience ?? options.ExpectedAudience; if (!string.IsNullOrEmpty(audience)) { yield return new JwtAuthenticator( registration.KeyResolver, audience, options.ClockSkewTolerance); } } if (!haveIssuerRegistrations && !string.IsNullOrEmpty(options.ExpectedAudience)) { foreach (IIssuerKeyResolver keyResolver in services.GetServices()) { yield return new JwtAuthenticator( keyResolver, options.ExpectedAudience, options.ClockSkewTolerance); } } } } private static void EnsureFirstRegistration(IServiceCollection services) { foreach (ServiceDescriptor d in services) { if (d.ServiceType == typeof(OpcUaServerRegistrationMarker)) { throw new InvalidOperationException( "AddServer has already been called. At most one regular OPC UA server may be registered per service collection."); } } services.AddSingleton(); } private static void RegisterCommonServices( IServiceCollection services, bool enableConfiguredIdentityAuthenticators) { services.TryAddSingleton( sp => new ServiceProviderTelemetryContext(sp)); services.AddOptions(); if (enableConfiguredIdentityAuthenticators) { services.AddSingleton(new OpcUaServerIdentityAuthenticatorRegistration( (sp, certificateValidator) => { if (sp.GetService() != null) { return []; } OpcUaServerOptions options = sp.GetRequiredService>().Value; return CreateDefaultIdentityAuthenticators( sp, certificateValidator, options.Identity.Defaults); })); } services.AddHostedService(); services.AddOpcUa().AddApplicationInstance(); } private static void BindOpcUaServerOptions(OpcUaServerOptions options, IConfiguration section) { BindString(section, nameof(OpcUaServerOptions.ApplicationName), value => options.ApplicationName = value); BindString(section, nameof(OpcUaServerOptions.ApplicationUri), value => options.ApplicationUri = value); BindString(section, nameof(OpcUaServerOptions.ProductUri), value => options.ProductUri = value); BindString(section, nameof(OpcUaServerOptions.SubjectName), value => options.SubjectName = value); BindString(section, nameof(OpcUaServerOptions.PkiRoot), value => options.PkiRoot = value); BindString(section, nameof(OpcUaServerOptions.RegistrationEndpointUrl), value => options.RegistrationEndpointUrl = value); BindBoolean(section, nameof(OpcUaServerOptions.AutoAcceptUntrustedCertificates), value => options.AutoAcceptUntrustedCertificates = value); BindBoolean(section, nameof(OpcUaServerOptions.DiagnosticsEnabled), value => options.DiagnosticsEnabled = value); BindBoolean(section, nameof(OpcUaServerOptions.IncludeSignAndEncryptPolicies), value => options.IncludeSignAndEncryptPolicies = value); BindBoolean(section, nameof(OpcUaServerOptions.IncludeUnsecurePolicyNone), value => options.IncludeUnsecurePolicyNone = value); BindBoolean(section, nameof(OpcUaServerOptions.IncludeEccPolicies), value => options.IncludeEccPolicies = value); BindBoolean(section, nameof(OpcUaServerOptions.RejectSHA1Certificates), value => options.RejectSHA1Certificates = value); BindUInt32(section, nameof(OpcUaServerOptions.MaxByteStringLength), value => options.MaxByteStringLength = value); BindUInt32(section, nameof(OpcUaServerOptions.MaxArrayLength), value => options.MaxArrayLength = value); BindNullableInt32(section, nameof(OpcUaServerOptions.MaxMessageSize), value => options.MaxMessageSize = value); BindNullableInt32(section, nameof(OpcUaServerOptions.OperationTimeoutMs), value => options.OperationTimeoutMs = value); BindUInt16(section, nameof(OpcUaServerOptions.MinCertificateKeySize), value => options.MinCertificateKeySize = value); BindEndpointUrls(options, section.GetSection(nameof(OpcUaServerOptions.EndpointUrls))); BindUserTokenPolicies(options, section.GetSection(nameof(OpcUaServerOptions.UserTokenPolicies))); BindReverseConnect(options, section.GetSection(nameof(OpcUaServerOptions.ReverseConnect))); BindOperationLimits(options, section.GetSection(nameof(OpcUaServerOptions.OperationLimits))); BindIdentity(options, section.GetSection(nameof(OpcUaServerOptions.Identity))); } private static void BindEndpointUrls(OpcUaServerOptions options, IConfigurationSection section) { if (!section.Exists()) { return; } options.EndpointUrls.Clear(); foreach (IConfigurationSection child in section.GetChildren()) { if (!string.IsNullOrEmpty(child.Value)) { options.EndpointUrls.Add(child.Value); } } } private static void BindUserTokenPolicies(OpcUaServerOptions options, IConfigurationSection section) { if (!section.Exists()) { return; } options.UserTokenPolicies.Clear(); foreach (IConfigurationSection child in section.GetChildren()) { var policy = new OpcUaUserTokenPolicy(); string? tokenType = child[nameof(OpcUaUserTokenPolicy.TokenType)]; if (!string.IsNullOrEmpty(tokenType) && Enum.TryParse(tokenType, ignoreCase: true, out UserTokenType parsedTokenType)) { policy.TokenType = parsedTokenType; } options.UserTokenPolicies.Add(policy); } } private static void BindReverseConnect(OpcUaServerOptions options, IConfigurationSection section) { if (!section.Exists()) { return; } var reverseConnect = new ServerReverseConnectOptions(); BindInt32(section, nameof(ServerReverseConnectOptions.ConnectIntervalMs), value => reverseConnect.ConnectIntervalMs = value); BindInt32(section, nameof(ServerReverseConnectOptions.ConnectTimeoutMs), value => reverseConnect.ConnectTimeoutMs = value); BindInt32(section, nameof(ServerReverseConnectOptions.RejectTimeoutMs), value => reverseConnect.RejectTimeoutMs = value); foreach (IConfigurationSection child in section.GetSection(nameof(ServerReverseConnectOptions.Clients)).GetChildren()) { var client = new ServerReverseConnectClientOptions(); BindString(child, nameof(ServerReverseConnectClientOptions.EndpointUrl), value => client.EndpointUrl = value); BindInt32(child, nameof(ServerReverseConnectClientOptions.Timeout), value => client.Timeout = value); BindInt32(child, nameof(ServerReverseConnectClientOptions.MaxSessionCount), value => client.MaxSessionCount = value); BindBoolean(child, nameof(ServerReverseConnectClientOptions.Enabled), value => client.Enabled = value); reverseConnect.Clients.Add(client); } options.ReverseConnect = reverseConnect; } private static void BindOperationLimits(OpcUaServerOptions options, IConfigurationSection section) { if (!section.Exists()) { return; } var limits = new OperationLimitsOptions(); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerRead), value => limits.MaxNodesPerRead = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerHistoryReadData), value => limits.MaxNodesPerHistoryReadData = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerHistoryReadEvents), value => limits.MaxNodesPerHistoryReadEvents = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerWrite), value => limits.MaxNodesPerWrite = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerHistoryUpdateData), value => limits.MaxNodesPerHistoryUpdateData = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerHistoryUpdateEvents), value => limits.MaxNodesPerHistoryUpdateEvents = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerMethodCall), value => limits.MaxNodesPerMethodCall = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerBrowse), value => limits.MaxNodesPerBrowse = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerRegisterNodes), value => limits.MaxNodesPerRegisterNodes = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerTranslateBrowsePathsToNodeIds), value => limits.MaxNodesPerTranslateBrowsePathsToNodeIds = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxNodesPerNodeManagement), value => limits.MaxNodesPerNodeManagement = value); BindUInt32(section, nameof(OperationLimitsOptions.MaxMonitoredItemsPerCall), value => limits.MaxMonitoredItemsPerCall = value); options.OperationLimits = limits; } private static void BindIdentity(OpcUaServerOptions options, IConfigurationSection section) { if (!section.Exists()) { return; } var identity = new OpcUaServerIdentityOptions { Defaults = BindDefaultAuthenticatorOptions(section.GetSection(nameof(OpcUaServerIdentityOptions.Defaults))) }; foreach (IConfigurationSection issuerSection in section.GetSection(nameof(OpcUaServerIdentityOptions.Issuers)).GetChildren()) { identity.Issuers.Add(BindJwtIssuerOptions(issuerSection)); } options.Identity = identity; } private static DefaultAuthenticatorOptions BindDefaultAuthenticatorOptions(IConfiguration section) { var options = new DefaultAuthenticatorOptions(); BindBoolean(section, nameof(DefaultAuthenticatorOptions.EnableAnonymous), value => options.EnableAnonymous = value); BindBoolean(section, nameof(DefaultAuthenticatorOptions.EnableUserNamePassword), value => options.EnableUserNamePassword = value); BindBoolean(section, nameof(DefaultAuthenticatorOptions.EnableX509), value => options.EnableX509 = value); BindBoolean(section, nameof(DefaultAuthenticatorOptions.EnableJwt), value => options.EnableJwt = value); string? expectedAudience = section[nameof(DefaultAuthenticatorOptions.ExpectedAudience)]; if (!string.IsNullOrEmpty(expectedAudience)) { options.ExpectedAudience = expectedAudience; } string? clockSkew = section[nameof(DefaultAuthenticatorOptions.ClockSkewTolerance)]; if (!string.IsNullOrEmpty(clockSkew) && TimeSpan.TryParse(clockSkew, CultureInfo.InvariantCulture, out TimeSpan parsedClockSkew)) { options.ClockSkewTolerance = parsedClockSkew; } string? trustList = section[nameof(DefaultAuthenticatorOptions.UserCertificateTrustList)] ?? section.GetSection(nameof(DefaultAuthenticatorOptions.UserCertificateTrustList))["Name"]; if (!string.IsNullOrEmpty(trustList)) { options.UserCertificateTrustList = new TrustListIdentifier(trustList); } return options; } private static void BindString( IConfiguration section, string key, Action assign) { string? value = section[key]; if (!string.IsNullOrEmpty(value)) { assign(value); } } private static void BindBoolean( IConfiguration section, string key, Action assign) { string? value = section[key]; if (!string.IsNullOrEmpty(value) && bool.TryParse(value, out bool parsed)) { assign(parsed); } } private static void BindInt32( IConfiguration section, string key, Action assign) { string? value = section[key]; if (!string.IsNullOrEmpty(value) && int.TryParse( value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed)) { assign(parsed); } } private static void BindNullableInt32( IConfiguration section, string key, Action assign) { string? value = section[key]; if (!string.IsNullOrEmpty(value) && int.TryParse( value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed)) { assign(parsed); } } private static void BindUInt32( IConfiguration section, string key, Action assign) { string? value = section[key]; if (!string.IsNullOrEmpty(value) && uint.TryParse( value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uint parsed)) { assign(parsed); } } private static void BindUInt16( IConfiguration section, string key, Action assign) { string? value = section[key]; if (!string.IsNullOrEmpty(value) && ushort.TryParse( value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ushort parsed)) { assign(parsed); } } private static JwtIssuerOptions BindJwtIssuerOptions(IConfiguration section) { var options = new JwtIssuerOptions(); IConfigurationSection algorithmsSection = section.GetSection("Algorithms"); if (algorithmsSection.Exists()) { options.Algorithms.Clear(); } section.Bind(options); if (options.Algorithms.Count == 0) { options.Algorithms.Add("RS256"); } return options; } private static void RegisterJwtIssuer(IServiceCollection services, JwtIssuerOptions options) { options.Validate(); ValidateAlgorithms(options); if (!string.IsNullOrEmpty(options.JwksUri)) { services.TryAddSingleton(TimeProvider.System); services.TryAddSingleton(); } string registrationId = Guid.NewGuid().ToString("N"); services.AddSingleton(sp => new JwtIssuerRegistration( registrationId, options.Audience, CreateIssuerKeyResolver(sp, options))); services.AddSingleton(sp => GetJwtIssuerRegistration(sp, registrationId).KeyResolver); } private static JwtIssuerRegistration GetJwtIssuerRegistration( IServiceProvider services, string registrationId) { foreach (JwtIssuerRegistration registration in services.GetServices()) { if (string.Equals(registration.Id, registrationId, StringComparison.Ordinal)) { return registration; } } throw new InvalidOperationException("JWT issuer registration was not found."); } private static IIssuerKeyResolver CreateIssuerKeyResolver( IServiceProvider services, JwtIssuerOptions options) { var resolvers = new List(); if (!string.IsNullOrEmpty(options.JwksUri)) { resolvers.Add(new JwksIssuerKeyResolver( options.IssuerUri, options.JwksUri, services.GetRequiredService(), services.GetRequiredService(), TimeSpan.FromMinutes(5), options.GetEffectiveAlgorithms())); } if (options.StaticKeys.Count != 0) { resolvers.Add(new StaticIssuerKeyResolver( options.IssuerUri, CreateStaticVerificationKeys(options))); } return resolvers.Count == 1 ? resolvers[0] : new CombinedIssuerKeyResolver(options.IssuerUri, resolvers); } private static ReadOnlyCollection CreateStaticVerificationKeys( JwtIssuerOptions options) { IReadOnlyList allowedAlgorithms = options.GetEffectiveAlgorithms(); var keys = new List(); foreach (JwtStaticKeyOptions keyOptions in options.StaticKeys) { if (!ContainsOrdinal(allowedAlgorithms, keyOptions.Algorithm)) { throw new InvalidOperationException( $"Static JWT key algorithm '{keyOptions.Algorithm}' is not allowed for issuer '{options.IssuerUri}'."); } keys.Add(keyOptions.CreateVerificationKey()); } return keys.AsReadOnly(); } private static void ValidateAlgorithms(JwtIssuerOptions options) { foreach (string algorithm in options.GetEffectiveAlgorithms()) { if (string.IsNullOrWhiteSpace(algorithm)) { throw new InvalidOperationException( $"JWT issuer '{options.IssuerUri}' contains an empty algorithm entry."); } } } private static bool ContainsOrdinal(IReadOnlyList values, string value) { foreach (string item in values) { if (string.Equals(item, value, StringComparison.Ordinal)) { return true; } } return false; } private sealed class JwtIssuerRegistration : IDisposable { public JwtIssuerRegistration( string id, string? audience, IIssuerKeyResolver keyResolver) { Id = id; Audience = audience; KeyResolver = keyResolver; } public string Id { get; } public string? Audience { get; } public IIssuerKeyResolver KeyResolver { get; } public void Dispose() { (KeyResolver as IDisposable)?.Dispose(); } } private sealed class CombinedIssuerKeyResolver : IIssuerKeyResolver, IDisposable { private readonly IReadOnlyList m_resolvers; private bool m_disposed; public CombinedIssuerKeyResolver(string issuerUri, IReadOnlyList resolvers) { IssuerUri = issuerUri; m_resolvers = resolvers; } public string IssuerUri { get; } public async ValueTask> GetKeysAsync( string? keyId, CancellationToken ct = default) { if (m_disposed) { throw new ObjectDisposedException(nameof(CombinedIssuerKeyResolver)); } var keys = new List(); foreach (IIssuerKeyResolver resolver in m_resolvers) { IReadOnlyList resolved = await resolver .GetKeysAsync(keyId, ct) .ConfigureAwait(false); for (int i = 0; i < resolved.Count; i++) { keys.Add(resolved[i]); } } return keys.AsReadOnly(); } public void Dispose() { if (m_disposed) { return; } m_disposed = true; foreach (IIssuerKeyResolver resolver in m_resolvers) { (resolver as IDisposable)?.Dispose(); } } } private sealed class OpcUaServerDefaultIdentityAuthenticatorsMarker; private sealed class OpcUaServerBuilder : IOpcUaServerBuilder { public OpcUaServerBuilder(IServiceCollection services) { Services = services; } public IServiceCollection Services { get; } public IOpcUaServerBuilder AddNodeManager< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFactory>() where TFactory : class, IAsyncNodeManagerFactory { Services.AddSingleton(); Services.AddSingleton(sp => new OpcUaServerNodeManagerRegistration( sp.GetRequiredService())); return this; } public IOpcUaServerBuilder AddSyncNodeManager< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TFactory>() where TFactory : class, INodeManagerFactory { Services.AddSingleton(); Services.AddSingleton(sp => new OpcUaServerNodeManagerRegistration( sp.GetRequiredService())); return this; } } private sealed class OpcUaServerRegistrationMarker; } }