# Identity Providers (OPC UA Part 6 §6.5) The OPC UA .NET Standard stack provides a pluggable identity-provider model. It covers the user-identity mechanisms in [OPC UA Part 6 §6.5](https://reference.opcfoundation.org/Core/Part6/v105/docs/6.5) and the identity-provider handshakes in [OPC UA Part 4 §6.2](https://reference.opcfoundation.org/Core/Part4/v105/docs/6.2). The design is intentionally **symmetric**: ``` ┌──────────────────────────────────────────────┐ │ Opc.Ua.Core / Opc.Ua.Identity │ │ (shared interfaces, no behaviour change) │ └──────────────────────────────────────────────┘ ▲ ▲ ┌───────────────┘ └───────────────┐ │ │ IClientIdentityProvider IUserTokenAuthenticator IAccessTokenProvider IServerIdentityRegistry AuthorizationServerMetadata ITokenIssuer │ │ Used by: Used by: ManagedSession, SessionManager ConsoleReferenceClient ReferenceServer custom Entra/OIDC providers JwtAuthenticator, X509Authenticator custom Windows/AspNetCore providers AuthorizationServiceType ``` The model **does not replace** the existing [`IUserIdentity`](../src/Opc.Ua.Core/Stack/Client/IUserIdentity.cs) / [`IUserIdentityTokenHandler`](../src/Opc.Ua.Core/Stack/Types/IUserIdentityTokenHandler.cs) contracts that ship on the wire — those are still the canonical on-the-wire types. The provider model layers on top of those types so legacy callbacks keep working during migration, while new code can register `IUserTokenAuthenticator` instances directly. ## Contents - [Quick start — dependency injection](#quick-start--dependency-injection) - [Server example](#server-example) - [Custom authenticator registrations](#custom-authenticator-registrations) - [Client example](#client-example) - [GDS example](#gds-example) - [Configuration reference](#configuration-reference) - [Three layers, kept separate](#three-layers-kept-separate) - [Client side](#client-side) - [`IClientIdentityProvider` — what to send in `ActivateSession`](#iclientidentityprovider--what-to-send-in-activatesession) - [`IAccessTokenProvider` — orthogonal to the OPC UA stack](#iaccesstokenprovider--orthogonal-to-the-opc-ua-stack) - [`AuthorizationServerMetadata` — the JSON nobody told you about](#authorizationservermetadata--the-json-nobody-told-you-about) - [Server side](#server-side) - [`IUserTokenAuthenticator` — what to do with the incoming token](#iusertokenauthenticator--what-to-do-with-the-incoming-token) - [`IServerIdentityRegistry` — composing authenticators](#iserveridentityregistry--composing-authenticators) - [Identity augmenters](#identity-augmenters) - [Claims surface — wiring `IdentityCriteriaType.GroupId` and `Role`](#claims-surface--wiring-identitycriteriatypegroupid-and-role) - [`ITokenIssuer` — server-side JWT issuance](#itokenissuer--server-side-jwt-issuance) - [`IIssuerKeyResolver` + `IssuerVerificationKey` — JWT validation](#iissuerkeyresolver--issuerverificationkey--jwt-validation) - [How-to: server-side authentication](#how-to-server-side-authentication) - [How-to: client-side provider selection](#how-to-client-side-provider-selection) - [How-to: migrate from `SessionManager.ImpersonateUser`](#how-to-migrate-from-sessionmanagerimpersonateuser) - [1. Legacy event code](#1-legacy-event-code) - [2. Implement an authenticator for that token type](#2-implement-an-authenticator-for-that-token-type) - [3. Register via dependency injection or the server registry](#3-register-via-dependency-injection-or-the-server-registry) - [Implementing your own provider](#implementing-your-own-provider) - [Entra ID provider](#entra-id-provider) - [OIDC provider](#oidc-provider) - [Windows Integrated provider](#windows-integrated-provider) - [ASP.NET Core provider](#aspnet-core-provider) - [See also](#see-also) ## Quick start — dependency injection For applications hosted on `Microsoft.Extensions.Hosting` (the default in this stack — see [Dependency Injection](DependencyInjection.md)), the identity surface is configured through fluent extensions on `IOpcUaServerBuilder` and `IOpcUaClientBuilder`, plus matching `appsettings.json` sections. ### Server example ```csharp services.AddOpcUa() .AddServer(opt => { opt.ApplicationName = "MyServer"; opt.ApplicationUri = "urn:my-server"; }) .AddDefaultIdentityAuthenticators(opt => { opt.EnableAnonymous = true; opt.EnableUserNamePassword = true; opt.EnableX509 = true; opt.EnableJwt = true; opt.ExpectedAudience = "urn:my-server"; }) .AddJwtIssuer(opt => { opt.IssuerUri = "https://login.microsoftonline.com/{tenant}/v2.0"; opt.JwksUri = "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"; }); ``` Equivalent `appsettings.json` picked up automatically by `builder.AddServer(IConfiguration)` — that overload walks the section and (in addition to binding `OpcUaServerOptions.Identity`) **also** registers each `Issuers[]` entry through `AddJwtIssuer(...)`. The `Identity:Defaults` block is honoured because `AddServer(IConfiguration)` enables the configured-default-authenticators bridge: ```json { "OpcUa": { "Server": { "ApplicationName": "MyServer", "ApplicationUri": "urn:my-server", "Identity": { "Defaults": { "EnableAnonymous": true, "EnableUserNamePassword": true, "EnableX509": true, "EnableJwt": true, "ExpectedAudience": "urn:my-server" }, "Issuers": [ { "IssuerUri": "https://login.microsoftonline.com/{tenant}/v2.0", "JwksUri": "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys" } ] } } } } ``` > The `AddServer(Action)` overload (code-only) does > NOT wire the default authenticators or JWT issuers from > `OpcUaServerOptions.Identity` — that bridge is enabled only on the > `AddServer(IConfiguration)` path. With the `Action<>` overload, call > `AddDefaultIdentityAuthenticators(opt => …)` and `AddJwtIssuer(opt => …)` > explicitly. ### Custom authenticator registrations `AddIdentityAuthenticator()` resolves an authenticator from dependency injection. Two other extensions on `IOpcUaServerBuilder`, in `Microsoft.Extensions.DependencyInjection`, support already-created instances and configuration-aware construction: | Overload | Use | |----------|-----| | `AddIdentityAuthenticator(IUserTokenAuthenticator)` | Register an existing authenticator instance. | | `AddIdentityAuthenticator(Func)` | Construct an authenticator during startup using injected services and the effective certificate validator. | `IUserTokenAuthenticator` is in `Opc.Ua.Identity`; `ICertificateValidatorEx` is in `Opc.Ua`. The factory runs after configuration and certificate setup, including when configuration is loaded from XML or a stream. Its validator can be `null`: an authenticator that requires it should reject that case explicitly instead of bypassing validation. ```csharp using Microsoft.Extensions.DependencyInjection; using Opc.Ua; using Opc.Ua.Server.Hosting; services.AddOpcUa() .AddServer(options => { options.ApplicationName = "CertificateServer"; options.ApplicationUri = "urn:example:CertificateServer"; options.UserTokenPolicies.Add(new OpcUaUserTokenPolicy { TokenType = UserTokenType.Anonymous }); options.UserTokenPolicies.Add(new OpcUaUserTokenPolicy { TokenType = UserTokenType.Certificate }); }) .AddIdentityAuthenticator(new Opc.Ua.Server.AnonymousAuthenticator()) .AddIdentityAuthenticator((_, validator) => new Opc.Ua.Server.X509Authenticator( validator ?? throw new InvalidOperationException( "A certificate validator is required for X.509 user tokens."))); ``` Provision user-certificate trust through the [certificate manager and stores](CertificateManager.md); no certificate or secret belongs in this registration code. Register matching `UserTokenPolicies` as shown: adding an authenticator does not automatically advertise its token policy. Use the service-provider argument when the factory also needs application services. The instance and factory overloads are on `IOpcUaServerBuilder`; the existing generic registration remains available on the GDS builder. ### Client example The composite builder API takes the supporting service (an `ISecretRegistry` for passwords, an `IAccessTokenProvider` for issued tokens) directly: ```csharp ISecretRegistry secrets = /* register and populate elsewhere */; IAccessTokenProvider tokens = new JwtBearerAccessTokenProvider( authorityUri: "https://issuer.example", tokenBytes: System.Text.Encoding.UTF8.GetBytes(jwt), expiresAt: DateTime.UtcNow.AddMinutes(55)); services.AddOpcUa() .AddClient(opt => { opt.SessionName = "MyClient"; }) .AddAccessTokenProvider(tokens) .AddIdentityProvider(builder => { builder.AddAnonymous(); builder.AddUserName( configure: opt => { opt.UserName = "alice"; opt.SecretName = "alice-password"; opt.SecretStoreType = "InMemory"; }, registry: secrets); builder.AddIssuedToken( configure: opt => opt.ProfileUri = Profiles.JwtUserToken, provider: tokens); }); ``` Or, drive the composite from configuration — the `IConfiguration` overload resolves the `ISecretRegistry` and `IAccessTokenProvider` from the dependency-injection container: ```csharp services.AddOpcUa() .AddClient(configuration.GetSection("OpcUa:Client")) .AddAccessTokenProvider(tokens) .AddIdentityProvider(configuration.GetSection("OpcUa:Client:Identity")); ``` The matching `appsettings.json` (only the `AddIdentityProvider(section)` overload picks this up — `AddClient(IConfiguration)` alone binds the section into `OpcUaClientOptions.Identity` but does not register providers): ```json { "OpcUa": { "Client": { "SessionName": "MyClient", "Identity": { "EnableAnonymous": true, "UserName": { "UserName": "alice", "SecretName": "alice-password", "SecretStoreType": "InMemory" }, "IssuedToken": { "ProfileUri": "http://opcfoundation.org/UA/UserToken#JWT", "AuthorityUri": "https://issuer.example" } } } } } ``` `IssuedToken.AuthorityUri` selects which registered `IAccessTokenProvider` to use (matched against `IAccessTokenProvider.AuthorityUri`). Omit it if exactly one provider is registered. ### GDS example `IGdsServerBuilder` forwards the shared identity configuration extensions to the underlying server builder (`ConfigureRoles`, `AddIdentityAuthenticator`, `AddIdentityAugmenter`, `AddDefaultIdentityAuthenticators`, `AddJwtIssuer` — each with both `Action<>` and `IConfiguration` overloads where applicable). A GDS host configures identity the same way as a regular server, just on the GDS builder: ```csharp services.AddOpcUa() .AddGdsServer(opt => opt.ApplicationName = "MyGds") .AddDefaultIdentityAuthenticators(opt => opt.EnableJwt = false); ``` `GdsServerHostedService` consumes the forwarded authenticator and augmenter registrations during start-up and registers them with the same identity registry used by the regular hosted server. The GDS default authenticator helper also registers `GdsApplicationSelfAdminProvider` so SelfAdmin elevation works without custom host code. ### Configuration reference | Key | Type | Default | |---|---|---| | `OpcUa:Server:Identity:Defaults:EnableAnonymous` | `bool` | `true` | | `OpcUa:Server:Identity:Defaults:EnableUserNamePassword` | `bool` | `true` | | `OpcUa:Server:Identity:Defaults:EnableX509` | `bool` | `true` | | `OpcUa:Server:Identity:Defaults:EnableJwt` | `bool` | `true` | | `OpcUa:Server:Identity:Defaults:ExpectedAudience` | `string?` | `null` (required when `EnableJwt`) | | `OpcUa:Server:Identity:Defaults:ClockSkewTolerance` | `TimeSpan` | `00:01:00` | | `OpcUa:Server:Identity:Defaults:UserCertificateTrustList` | `TrustListIdentifier` | `Users` | | `OpcUa:Server:Identity:Issuers[].IssuerUri` | `string` | — (required) | | `OpcUa:Server:Identity:Issuers[].JwksUri` | `string?` | `null` | | `OpcUa:Server:Identity:Issuers[].StaticKeys[].Kid` | `string?` | `null` | | `OpcUa:Server:Identity:Issuers[].StaticKeys[].Algorithm` | `string` | `RS256` | | `OpcUa:Server:Identity:Issuers[].StaticKeys[].RsaPublicKeyPem` | `string?` | `null` (SPKI or PKCS#1 PEM) | | `OpcUa:Server:Identity:Issuers[].StaticKeys[].RsaModulus` / `.RsaExponent` | `string?` | base64url-encoded JWK `n` / `e` | | `OpcUa:Server:Identity:Issuers[].StaticKeys[].EcCurve` | `string?` | `P-256` / `P-384` / `P-521` | | `OpcUa:Server:Identity:Issuers[].StaticKeys[].EcX` / `.EcY` | `string?` | base64url-encoded JWK `x` / `y` | | `OpcUa:Server:Identity:Issuers[].Algorithms` | `string[]` | `["RS256"]` | | `OpcUa:Server:Identity:Issuers[].Audience` | `string?` | `null` (falls back to `Defaults.ExpectedAudience`) | | `OpcUa:Client:Identity:EnableAnonymous` | `bool` | `true` | | `OpcUa:Client:Identity:UserName:UserName` | `string` | — | | `OpcUa:Client:Identity:UserName:SecretName` | `string` | — | | `OpcUa:Client:Identity:UserName:SecretStoreType` | `string` | — | | `OpcUa:Client:Identity:UserName:SecretStorePath` | `string?` | `null` | | `OpcUa:Client:Identity:X509:StoreType` | `string` | — | | `OpcUa:Client:Identity:X509:StorePath` | `string` | — | | `OpcUa:Client:Identity:X509:SubjectName` | `string?` | `null` (exclusive with `Thumbprint`) | | `OpcUa:Client:Identity:X509:Thumbprint` | `string?` | `null` (exclusive with `SubjectName`) | | `OpcUa:Client:Identity:IssuedToken:ProfileUri` | `string` | `http://opcfoundation.org/UA/UserToken#JWT` | | `OpcUa:Client:Identity:IssuedToken:AuthorityUri` | `string?` | `null` (selects the matching `IAccessTokenProvider` by `AuthorityUri`) | | `OpcUa:Client:Identity:Order` | `string[]` | `[]` (registration order) | #### What gets auto-wired | Section | Path that auto-wires it | Notes | |---|---|---| | `OpcUa:Server:Identity:Defaults` | `AddServer(IConfiguration)` | The hosted service materialises the four default authenticators from the bound flags. | | `OpcUa:Server:Identity:Issuers[]` | `AddServer(IConfiguration)` | Each entry is registered through `AddJwtIssuer(...)` at builder time. | | `OpcUa:Client:Identity` | `AddClient(IConfiguration)` (fallback at session-factory resolution) | The session factory builds a composite from the bound options when **no** `IClientIdentityProvider` is registered AND at least one non-default field is set (any of `UserName`, `X509`, `IssuedToken`, `Order`, or `EnableAnonymous = false`). Pass the section to `.AddIdentityProvider(IConfiguration)` for an explicit eager registration. | | `OpcUa:Server:Identity:*` from the `AddServer(Action<>)` path | **Not auto-wired** | Use the explicit fluent calls (`AddDefaultIdentityAuthenticators`, `AddJwtIssuer`). | See [Dependency Injection](DependencyInjection.md) for the full `services.AddOpcUa()` surface and how options flow through `IConfiguration`. ## Three layers, kept separate A common pitfall when designing identity systems is to collapse three distinct responsibilities into one interface. This API keeps them separate so each layer can be replaced independently: | Layer | Interface | What it does | What it must NOT do | |---|---|---|---| | **Authentication** | `IUserTokenAuthenticator` | Validates a single token type / profile (password lookup, X.509 trust, JWT signature). Produces an `IUserIdentity`. | Pre-grant Part 18 role NodeIds. | | **Augmentation** | `IIdentityAugmenter` | Runs after an accepted authenticator and may wrap the identity with deployment-specific bindings such as GDS ApplicationSelfAdmin. | Validate a token or run after rejection. | | **Claim extraction** | `IIdentityClaims` (probe interface on the returned identity) | Surfaces OIDC / JWT / X.509 claims so role mapping has data to work with. | Decide which roles get granted. | | **Role mapping** | `IRoleManager.ResolveGrantedRoles` (already exists, see [Role-Based Security](RoleBasedUserManagement.md)) | Applies OPC UA Part 18 §4.4 identity-mapping rules to the claims and emits the granted role NodeIds. | Authenticate the token. | Do not grant roles inside an authenticator. Put role assignment in an `IRoleManager` identity-mapping rule instead. The same rule then applies whether the user authenticated with a username, X.509 certificate, JWT, or another token type. ## Client side ### `IClientIdentityProvider` — what to send in `ActivateSession` `IClientIdentityProvider` returns an `IUserIdentity` lazily, **per activation**. Long-lived OPC UA sessions outlive short OAuth2 access tokens; the provider model is built around that fact. ```csharp using Opc.Ua.Identity; public sealed class MyEntraIdentityProvider : IClientIdentityProvider { private readonly IAccessTokenProvider m_tokens; private DateTime m_lastExpiry = DateTime.MaxValue; public MyEntraIdentityProvider(IAccessTokenProvider tokens) { m_tokens = tokens; } public IReadOnlyList SupportedTokenTypes => new[] { UserTokenType.IssuedToken }; public IReadOnlyList SupportedIssuedTokenProfileUris => new[] { Profiles.JwtUserToken }; public bool CanSatisfy(UserTokenPolicy policy, IdentitySelectionContext _) => policy.TokenType == UserTokenType.IssuedToken && string.Equals(policy.IssuedTokenType, Profiles.JwtUserToken, StringComparison.Ordinal); public DateTime ExpiresAt => m_lastExpiry; public async ValueTask GetIdentityAsync( UserTokenPolicy policy, IdentitySelectionContext context, CancellationToken ct = default) { // 1. Parse the server's IssuerEndpointUrl JSON per OPC 10000-6 §6.5.2.2. if (!AuthorizationServerMetadata.TryFromPolicy(policy, out var meta)) { throw ServiceResultException.Create( StatusCodes.BadIdentityTokenRejected, "Policy is not a JWT IssuedToken policy."); } // 2. Acquire the access token via OAuth2 / MSAL / GDS / etc. using AccessToken accessToken = await m_tokens .AcquireAsync(meta, ct) .ConfigureAwait(false); m_lastExpiry = accessToken.ExpiresAt; // 3. Wrap in an IssuedIdentityToken handler. return new UserIdentity( accessToken.TokenData.ToArray(), accessToken.ProfileUri) { PolicyId = policy.PolicyId }; } } ``` Key design notes: * **Token refresh is the session's responsibility, not the provider's.** Call `await session.UpdateIdentityAsync(provider, ct)` to reactivate a raw `Session` with a fresh identity. The method serialises with the existing reactivation lock and binds the new token to the current server nonce. * **Use `ManagedSessionOptions.IdentityProvider` for managed clients.** `ManagedSession` refreshes discovery metadata before acquiring the initial identity when endpoint refresh is enabled. Selection uses the configured security policies, instance-certificate algorithm and policy registry, just like `UpdateIdentityAsync`. The first activation uses that identity; it does not require an anonymous session. A supplied single-use reverse connection without a reverse-connect manager must already have endpoint metadata, as on the direct session-factory path. The session then schedules proactive refresh at `provider.ExpiresAt - 60s` using the configured `TimeProvider`. Refresh failures are logged and retried with backoff; they do not close the session. ```csharp var options = new ManagedSessionOptions { Endpoint = endpoint, IdentityProvider = new IssuedTokenIdentityProvider(accessTokens) }; ManagedSession session = await new ManagedSessionBuilder(configuration, telemetry) .UseEndpoint(endpoint) .WithIdentityProvider(options.IdentityProvider) .ConnectAsync(ct); ``` ### `IAccessTokenProvider` — orthogonal to the OPC UA stack `IAccessTokenProvider` is decoupled from the OPC UA session entirely. A single provider serves any number of OPC UA endpoints whose `UserTokenPolicy.IssuerEndpointUrl` points at the same Authorization Service. ```csharp public sealed class StaticJwtBearerProvider : IAccessTokenProvider { private readonly byte[] m_bytes; public StaticJwtBearerProvider(string jwt, string authorityUri) { m_bytes = System.Text.Encoding.UTF8.GetBytes(jwt); AuthorityUri = authorityUri; } public string AuthorityUri { get; } public ValueTask AcquireAsync( AuthorizationServerMetadata metadata, CancellationToken ct = default) { return new ValueTask(new AccessToken( Profiles.JwtUserToken, (byte[])m_bytes.Clone(), DateTime.MaxValue, "static-jwt")); } } ``` `AccessToken` is `IDisposable` and zeroes its buffer on dispose — that is the only reliable "secure clear" path until the JWT is encrypted into the wire token. **Always** use `using` or pass ownership to a `UserIdentity`/handler that takes ownership. ### `AuthorizationServerMetadata` — the JSON nobody told you about The OPC 10000-6 §6.5.2.2 `IssuerEndpointUrl` field of a JWT `UserTokenPolicy` is, despite its name, **not a URL** — it is a JSON object that describes the Authorization Service (authority URI, resource URI, request types, scopes, optionally token / JWKS endpoints). ```csharp using Opc.Ua.Identity; if (AuthorizationServerMetadata.TryFromPolicy(policy, out var meta)) { Console.WriteLine($"Authority : {meta.AuthorityUri}"); Console.WriteLine($"Resource : {meta.ResourceUri}"); Console.WriteLine($"Flows : {string.Join(",", meta.RequestTypes)}"); Console.WriteLine($"Scopes : {string.Join(",", meta.Scopes)}"); Console.WriteLine($"Token EP : {meta.TokenEndpoint ?? "(OIDC discovery)"}"); } ``` The parser: * Tolerates the `ua:` namespace prefix used by the spec (`ua:resourceUri` → `ResourceUri`). * Accepts OIDC-style aliases (`issuer`, `token_endpoint`, `authorization_endpoint`, `jwks_uri`, `scopes_supported`) so you can feed an OIDC `.well-known/openid-configuration` payload directly. * Accepts a bare string where the spec allows an array (some servers emit a single value for `requestTypes`). * Preserves unknown fields in `AdditionalFields` for vendor-specific use (e.g. Entra `tenant_id`, PKCE configuration). * Throws `BadDecodingError` on malformed JSON or non-object root. * Returns an empty instance (no exception) for null, empty, or whitespace-only payloads. ## Server side ### `IUserTokenAuthenticator` — what to do with the incoming token ```csharp using Opc.Ua.Identity; public sealed class StaticUserNameAuthenticator : IUserTokenAuthenticator { private readonly Func m_checkPassword; public StaticUserNameAuthenticator(Func checkPassword) { m_checkPassword = checkPassword ?? throw new ArgumentNullException(nameof(checkPassword)); } public UserTokenType TokenType => UserTokenType.UserName; public string? IssuedTokenProfileUri => null; public ValueTask AuthenticateAsync( AuthenticationContext context, CancellationToken ct = default) { if (context.TokenHandler is not UserNameIdentityTokenHandler u) { return new ValueTask( AuthenticationResult.NotHandled); } if (string.IsNullOrEmpty(u.UserName) || u.DecryptedPassword == null) { return new ValueTask( AuthenticationResult.Reject( new ServiceResult(StatusCodes.BadIdentityTokenInvalid))); } if (!m_checkPassword(u.UserName, u.DecryptedPassword)) { return new ValueTask( AuthenticationResult.Reject( new ServiceResult(StatusCodes.BadUserAccessDenied))); } var identity = new UserIdentity(u); // wraps the wire handler return new ValueTask( AuthenticationResult.Accept(identity)); } } ``` The three outcomes: * `AuthenticationResult.Accept(identity)` — token validated, identity returned. * `AuthenticationResult.Reject(serviceResult)` — token decoded but refused. The status code flows back to the client as the `ActivateSession` response error. Lockout / failure counters are recorded through the `SessionManager` failed-authentication path. * `AuthenticationResult.NotHandled` — the authenticator does not own this token type / profile. The registry moves on to the next authenticator and ultimately falls back to the legacy callback when no authenticator claimed the token. ### `IServerIdentityRegistry` — composing authenticators ```csharp var registry = new ServerIdentityRegistry( new AnonymousAuthenticator(), new StaticUserNameAuthenticator(MyUserDb.CheckPassword), new X509Authenticator(myCertValidator), new JwtAuthenticator(myEntraKeyResolver, expectedAudience: applicationUri)); ``` Authenticators are tried in registration order. For `UserTokenType.IssuedToken` the registry dispatches by the `IssuedTokenProfileUri` carried on the inbound `IssuedIdentityTokenHandler` — register a JWT authenticator with `IssuedTokenProfileUri = Profiles.JwtUserToken` and it only sees JWT tokens; a SAML or Kerberos authenticator on the same channel is left to handle the rest. Register with `IssuedTokenProfileUri = null` for a catch-all (useful when bridging to a legacy `ITokenValidator`). `Register` replaces the existing registration with the same token type and profile, so application authenticators replace hosted defaults rather than being shadowed by them. Issuer-backed `JwtAuthenticator` instances implement `IIssuerTokenAuthenticator`: distinct issuers coexist, while re-registering the same issuer replaces its old verifier. Issuer matching is ordinal and never substitutes for signature, audience, or lifetime validation. A custom, unqualified authenticator replaces every issuer for its type/profile; a later issuer-qualified registration replaces any unqualified registration. Rejections stop dispatch and preserve their status and diagnostic message. If neither the registry nor the legacy callback handles a non-anonymous token, session activation fails closed. ### Identity augmenters `IIdentityAugmenter` is a post-authentication hook. It runs only after an authenticator returns `AuthenticationOutcome.Accepted`; `NotHandled` and `Rejected` skip the chain. Augmenters may return the same identity or wrap it with extra deployment-specific state. ```csharp public sealed class MyTenantAugmenter : IIdentityAugmenter { public ValueTask AugmentAsync( IUserIdentity identity, AuthenticationContext context, CancellationToken ct = default) { // Inspect context.ChannelCertificate, context.ChannelApplicationUri, // claims, or deployment state, then return identity or a wrapper. return new ValueTask(identity); } } services.AddOpcUa() .AddServer(o => o.ApplicationUri = "urn:my-server") .AddIdentityAugmenter(); // Manual host alternative: server.CurrentInstance.IdentityRegistry.RegisterAugmenter( new MyTenantAugmenter()); ``` The GDS server ships `GdsApplicationSelfAdminProvider`, an augmenter that implements OPC 10000-12 §7.2 ApplicationSelfAdmin by matching the secure-channel ApplicationInstance certificate to the registered application certificate. ### Claims surface — wiring `IdentityCriteriaType.GroupId` and `Role` `IRoleManager.ResolveGrantedRoles` probes returned identities for `IIdentityClaims` and uses that surface to satisfy OPC 10000-18 §4.4.4 claim criteria: * `IdentityCriteriaType.GroupId` matches entries in `IIdentityClaims.Groups`. * `IdentityCriteriaType.Role` matches entries in `IIdentityClaims.Roles`. The criterion may be either `roleName` or issuer-qualified as `issuerUri/roleName`; the issuer-qualified form also checks `IIdentityClaims.Issuer`. Authenticators should populate `IIdentityClaims` when the token contains claims. Role assignment remains a role-manager decision; authenticators validate credentials and return an identity. ```csharp internal sealed class JwtUserIdentity : IUserIdentity, IIdentityClaims { public JwtUserIdentity( IUserIdentityTokenHandler handler, IReadOnlyDictionary claims, IReadOnlyList groups, IReadOnlyList roles, string issuer, string subject) { TokenHandler = handler; Claims = claims; Groups = groups; Roles = roles; Issuer = issuer; Subject = subject; } public string DisplayName => Subject; public string PolicyId => TokenHandler.Token.PolicyId; public UserTokenType TokenType => UserTokenType.IssuedToken; public XmlQualifiedName IssuedTokenType => IssuedTokenType.JWT; public bool SupportsSignatures => false; public ArrayOf GrantedRoleIds => default; public IUserIdentityTokenHandler TokenHandler { get; } public IReadOnlyDictionary Claims { get; } public IReadOnlyList Groups { get; } public IReadOnlyList Roles { get; } public string Issuer { get; } public string Subject { get; } } ``` A role rule such as `CriteriaType = IdentityCriteriaType.GroupId` and `Criteria = "engineering-leads"` matches when `Groups` contains `engineering-leads`. See [Role-Based Security](RoleBasedUserManagement.md) for a worked Entra JWT example. ### `ITokenIssuer` — server-side JWT issuance `ITokenIssuer` is the server-side counterpart of `IAccessTokenProvider`. It backs the modern Part 12 v1.05 `AuthorizationServiceType` `StartRequestToken` / `FinishRequestToken` flow and the legacy `RequestAccessToken` compatibility path. ```csharp public sealed class CertificateJwtIssuer : ITokenIssuer { public string IssuerUri => "https://my-gds.example.com"; public string ProfileUri => Profiles.JwtUserToken; public async ValueTask IssueAsync( TokenIssuanceRequest request, CancellationToken ct = default) { byte[] jws = await SignJwsAsync(request, ct).ConfigureAwait(false); return new AccessToken( Profiles.JwtUserToken, jws, DateTime.UtcNow + request.RequestedLifetime, request.Subject); } } ``` GDS hosts register the default issuer with `WithAuthorizationService(...)` or a custom issuer with `WithAuthorizationService(...)`; see [AuthorizationService](AuthorizationService.md). ### `IIssuerKeyResolver` + `IssuerVerificationKey` — JWT validation Server-side JWT validation in the GDS `JwtAuthenticator` resolves verification keys through `IIssuerKeyResolver`. Consumers receive each key as a non-disposable `IIssuerVerificationKey` view. `JwksIssuerKeyResolver` retains only its current snapshot; retired keys and their native handles are collected when their last reader releases them. Refresh and resolver disposal do not invalidate already returned JWKS keys. Other resolver owners dispose their concrete `IssuerVerificationKey` instances according to their lifetime. The helper deliberately uses `byte[]` overloads (no `System.IdentityModel.Tokens.Jwt`) so it works on net48 / net8+/net9+/net10+ and is AOT-friendly. Supported JWS algorithms (RFC 7518 §3.1): | Algorithm | Key type | Notes | |---|---|---| | `RS256` / `RS384` / `RS512` | RSA | PKCS#1 v1.5 padding | | `PS256` / `PS384` / `PS512` | RSA | RSA-PSS padding | | `ES256` / `ES384` / `ES512` | ECDSA | IEEE P1363 fixed-size r‖s format | HS256 (HMAC) is **intentionally unsupported** because the OPC UA multi-party authorization model would require sharing a symmetric secret with every verifier. ```csharp using RSA rsa = LoadRsaPublicKey(); using var key = new IssuerVerificationKey("kid-1", rsa, "RS256"); bool isValid = key.VerifySignature(signingInputBytes, signatureBytes); ``` ## How-to: server-side authentication Use dependency injection when the .NET Generic Host owns the server. This example wires the shipped defaults, a custom authenticator, and a trusted JWT issuer. ```csharp using Microsoft.Extensions.DependencyInjection; using Opc.Ua; using Opc.Ua.Identity; using Opc.Ua.Server; using Opc.Ua.Server.Hosting; services.AddSingleton>(checkPassword); services.AddOpcUa() .AddServer(o => { o.ApplicationName = "MyServer"; o.ApplicationUri = "urn:example:my-server"; o.EndpointUrls.Add("opc.tcp://localhost:4840"); }) .AddIdentityAuthenticator() .AddDefaultIdentityAuthenticators(o => { o.EnableAnonymous = true; o.EnableUserNamePassword = true; o.EnableX509 = true; o.EnableJwt = true; o.ExpectedAudience = "urn:example:my-server"; }) .AddJwtIssuer(o => { o.IssuerUri = "https://issuer.example"; o.JwksUri = "https://issuer.example/.well-known/jwks.json"; o.Audience = "urn:example:my-server"; }); ``` This uses `StaticUserNameAuthenticator` from the [server-side example](#server-side). Supply `checkPassword` from your application's credential-verification service before registering it. It must verify the supplied password against the user's stored credentials; a recognized username or a nonempty password is not sufficient. Do not log or retain the decrypted password. Manual hosts can register against the running server instance. Prefer dependency injection for hosted applications, but this is useful for existing `StandardServer` subclasses: ```csharp server.CurrentInstance.IdentityRegistry.Register(new StaticUserNameAuthenticator(checkPassword)); server.CurrentInstance.IdentityRegistry.Register( new JwtAuthenticator(keyResolver, expectedAudience: "urn:example:my-server")); ``` `GdsServerHostedService` consumes the same dependency-injection registrations, so a GDS host uses `.AddIdentityAuthenticator()`, `.AddDefaultIdentityAuthenticators()`, and `.AddJwtIssuer(...)` exactly like a regular server. ## How-to: client-side provider selection The shipped client providers are composable. `CompositeClientIdentityProvider` tries providers in order and picks the first one that satisfies the server's selected `UserTokenPolicy`. ```csharp using Opc.Ua; using Opc.Ua.Identity; using Opc.Ua.Security.Certificates; ISecretRegistry secrets = BuildSecretRegistry(); IAccessTokenProvider accessTokens = BuildAccessTokenProvider(); ICertificateProvider certificates = configuration.CertificateManager.CertificateProvider; ICertificatePasswordProvider passwords = new CertificatePasswordProvider(); IClientIdentityProvider provider = new CompositeClientIdentityProvider( new UserNamePasswordIdentityProvider( "alice", secrets, new SecretIdentifier("alice-password", "InMemory")), new X509ClientIdentityProvider( new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = "pki/user", SubjectName = "CN=Alice" }, passwords, certificates), new IssuedTokenIdentityProvider(accessTokens, Profiles.JwtUserToken)); await session.UpdateIdentityAsync(provider, ct).ConfigureAwait(false); ``` The access-token-backed shipped type is `IssuedTokenIdentityProvider`, which wraps an `IAccessTokenProvider` and materializes a UA `IssuedIdentityToken` for `ActivateSession`. For managed clients, put the same provider on `ManagedSessionOptions.IdentityProvider` so `ManagedSession` can refresh proactively: ```csharp var options = new ManagedSessionOptions { Endpoint = endpoint, IdentityProvider = provider }; ISessionFactory sessionFactory = sp.GetRequiredService(); ManagedSession managed = await ManagedSession.CreateAsync( configuration, endpoint, sessionFactory, identityProvider: provider, ct: ct); ``` ## How-to: migrate from `SessionManager.ImpersonateUser` The event remains as a compatibility fallback, but it is marked `[Obsolete]`. New code should move validation into one `IUserTokenAuthenticator` per token type. See [2.0 migration guide — User Identity Providers](migrate/2.0.x/identity.md#user-identity-providers) for the full migration table. ### 1. Legacy event code ```csharp server.CurrentInstance.SessionManager.ImpersonateUser += SessionManager_ImpersonateUser; private void SessionManager_ImpersonateUser( Session session, ImpersonateEventArgs args) { if (args.NewIdentity is UserNameIdentityToken userName && ValidatePassword(userName.UserName, userName.DecryptedPassword)) { args.Identity = new UserIdentity(userName); return; } args.Identity = null; } ``` ### 2. Implement an authenticator for that token type ```csharp public sealed class MyUserNameAuthenticator : IUserTokenAuthenticator { public UserTokenType TokenType => UserTokenType.UserName; public string? IssuedTokenProfileUri => null; public ValueTask AuthenticateAsync( AuthenticationContext context, CancellationToken ct = default) { if (context.TokenHandler is not UserNameIdentityTokenHandler userName) { return new ValueTask(AuthenticationResult.NotHandled); } return new ValueTask( ValidatePassword(userName.UserName, userName.DecryptedPassword) ? AuthenticationResult.Accept(new UserIdentity(userName)) : AuthenticationResult.Reject(new ServiceResult(StatusCodes.BadUserAccessDenied))); } } ``` ### 3. Register via dependency injection or the server registry ```csharp services.AddOpcUa() .AddServer(o => o.ApplicationUri = "urn:example:my-server") .AddIdentityAuthenticator(); // Without dependency injection: server.CurrentInstance.IdentityRegistry.Register(new MyUserNameAuthenticator()); ``` Migrate one token type at a time. If no authenticator handles a token, the registry falls back to the obsolete event so existing deployments can stage the change safely. ## Implementing your own provider The shipped interfaces are intentionally small so deployments can build provider packages without changing stack internals. The sections below show how to implement common provider integrations on top of the core OPC UA identity abstractions plus the provider SDK you need. ### Entra ID provider - Implement `EntraIdAccessTokenProvider : IAccessTokenProvider` by wrapping MSAL `IPublicClientApplication` for native/user-delegated clients or `IConfidentialClientApplication` for daemon services. Public clients should call `AcquireTokenSilent` first, then fall back to interactive or device-code flows; services should use `AcquireTokenForClient`. - Let MSAL own token caching and refresh. Return the acquired JWT as an `AccessToken`, mapping the audience from `AuthorizationServerMetadata.ResourceUri`, `api://`, or the target server `ApplicationUri`; map user-delegated scopes from `AuthorizationServerMetadata.Scopes`, and use `/.default` for client credentials. - Implement `EntraIdClientIdentityProvider : IClientIdentityProvider` by composing the access-token provider with `IssuedTokenIdentityProvider` and `Profiles.JwtUserToken`. - On the server, validate Entra JWTs with `JwksIssuerKeyResolver` (or a provider-specific `EntraIdJwtIssuerKeyResolver`) against `https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys`. Register it through `AddJwtIssuer(...)` or `JwtAuthenticator`, cache keys by `kid`, and refresh once on unknown keys or signature failure to handle tenant key rotation. ### OIDC provider - Implement `OidcAccessTokenProvider : IAccessTokenProvider` from an issuer or authority URI. Read `.well-known/openid-configuration` to discover the authorization, token, and JWKS endpoints, and map the discovered values into `AuthorizationServerMetadata` so existing OPC UA token-policy metadata still drives provider selection. - For desktop and native clients, use authorization code + PKCE: generate a high-entropy `code_verifier`, send `BASE64URL(SHA256(code_verifier))` as the challenge, exchange the returned code at the token endpoint, and return the access token as an OPC UA `AccessToken`. - Persist refresh tokens and related state through `ISecretStore` or `ISecretRegistry` so deployments can use DPAPI, Keychain, Key Vault, Kubernetes secrets, or another durable store. Try the refresh-token grant before launching a new authorization-code flow. - Implement `OidcClientIdentityProvider : IClientIdentityProvider` as an `IssuedTokenIdentityProvider` wrapper. For server validation, use `JwksIssuerKeyResolver` or an `OidcJwksKeyResolver : IIssuerKeyResolver` against the discovered `jwks_uri`, cache by `kid` and algorithm, honor HTTP cache headers, and refresh once before rejecting an unknown `kid`. ### Windows Integrated provider - Implement `WindowsIntegratedClientIdentityProvider : IClientIdentityProvider` to run a SPNEGO/Kerberos exchange and emit the resulting AP-REQ or wrapped Negotiate token as an issued user token profile owned by the package. - Use `System.Net.Security.NegotiateAuthentication` on `net8.0+`; use `System.Net.NegotiateStream` on down-level TFMs where the newer API is unavailable. - Derive the service principal name from the target OPC UA endpoint and `ApplicationUri`, for example `HOST/` for host-bound deployments or `OPCUA/` where an OPC UA-specific SPN is registered. - Implement `KerberosUserTokenAuthenticator : IUserTokenAuthenticator` to validate the AP-REQ or wrapped token, verify the ticket target matches the expected server identity, and return an `IUserIdentity`. - Decode PAC group SIDs into `IIdentityClaims.Groups` so Part 18 `IdentityCriteriaType.GroupId` and role criteria can map Windows groups without changing `IRoleManager`. ### ASP.NET Core provider - Implement `AspNetCoreAccessTokenProvider : IAccessTokenProvider` by adapting `Microsoft.Identity.Web` `ITokenAcquisition.GetAccessTokenForUserAsync(...)`. Resolve the current `ClaimsPrincipal` from `IHttpContextAccessor` or from an explicit accessor delegate supplied by the host. - Map `AuthorizationServerMetadata.Scopes` to the requested scope list and use `AuthorizationServerMetadata.ResourceUri` as the additional resource or audience hint when the identity provider requires one. - Use the ASP.NET Core / Microsoft.Identity.Web token cache for user-delegated tokens. If the host persists refresh-token references in cookies, session, or a server-side token cache, keep raw token material behind `ISecretStore` or `ISecretRegistry` abstractions. - Implement `AspNetCoreClientIdentityProvider : IClientIdentityProvider` by composing `AspNetCoreAccessTokenProvider` with `IssuedTokenIdentityProvider`, then register both as scoped services so request handlers can resolve the identity provider and call `Session.UpdateIdentityAsync(...)`. - Server-side JWT validation is the same as other OIDC providers: configure `AddJwtIssuer(...)` or `JwtAuthenticator` with the issuer, audience, and JWKS endpoint used by the ASP.NET Core authority. ## See also * [2.0 migration guide — User Identity Providers](migrate/2.0.x/identity.md#user-identity-providers) for source migrations from obsolete identity APIs. * [2.0 migration guide — User Identity Token Handlers](migrate/2.0.x/identity.md#user-identity-token-handlers) for the 1.6 token-handler async / non-disposable refactor. * [Role-Based Security (OPC UA Part 18)](RoleBasedUserManagement.md) for the role-mapping layer that consumes `IIdentityClaims`. * [AuthorizationService](AuthorizationService.md) for the OPC 10000-12 §9 service that issues access tokens. * [KeyCredentialService](KeyCredentialService.md) for the OPC 10000-12 §8 service, Push model, and experimental bridge. * [Dependency Injection](DependencyInjection.md) for the `services.AddOpcUa()` hosting surface. * [Implementing your own provider](#implementing-your-own-provider) for Entra ID, generic OIDC, Windows Integrated, and ASP.NET Core provider guidance. * OPC UA specification references: * [Part 4 §6.2 — Authorization Services](https://reference.opcfoundation.org/Core/Part4/v105/docs/6.2) * [Part 4 §7.40 — UserIdentityToken parameters](https://reference.opcfoundation.org/Core/Part4/v105/docs/7.40) * [Part 6 §6.5 — Issued User Identity Tokens](https://reference.opcfoundation.org/Core/Part6/v105/docs/6.5) * [Part 6 §6.5.2.2 — JWT UserTokenPolicy IssuerEndpointUrl](https://reference.opcfoundation.org/Core/Part6/v105/docs/6.5.2.2) * [Part 12 §8 — KeyCredentialManagement](https://reference.opcfoundation.org/GDS/v105/docs/8) * [Part 12 §9 — AuthorizationServices](https://reference.opcfoundation.org/GDS/v105/docs/9) * [Part 18 — Role-Based Security](https://reference.opcfoundation.org/Core/Part18/v105/docs/)