Files
UA-.NETStandard/docs/IdentityProviders.md
romanett 61edaa1ed0 Remove netstandard2.1 and net472 target frameworks (#4624)
# Description

Drops support for **netstandard2.1** and **.NET Framework 4.7.2**.
Libraries now target `net48`, `net8.0`, `net9.0` and `net10.0`.
`Opc.Ua.Types` and `Opc.Ua.SourceGeneration.Core` still also target
`netstandard2.0`, but only because the Roslyn source generators load
them. There is no general netstandard2.0 support.

**Preprocessor directives** (112 files)
- Every `#if`/`#elif` mentioning `NETSTANDARD2_1*`, `NET472*`,
`NET_STANDARD_TESTS`, `SKIP_ECC_CERTIFICATE_REQUEST_SIGNING` or
`NETSTANDARD*` was evaluated against the remaining TFM set (net48,
net8/9/10, plus netstandard2.0 inside `Opc.Ua.Types`/`tools`) and
simplified. Dead branches were removed.
- Example: `NETSTANDARD2_1 || NET8_0_OR_GREATER` → `NET8_0_OR_GREATER`.
- Example: `NET8_0_OR_GREATER || NET472_OR_GREATER` → removed (always
true).
- Netstandard-only code was removed: `UnixFileModeShim`, the CRDT
reflection path in `ReplicatedGossipOptions.CreateReaderOptions`, and
the netstandard2.1-only test variants.

**Custom test logic**
- `targets.props` drops the `net472`, `netstandard2.0` and
`netstandard2.1` `CustomTestTarget` profiles, `NetStandardTests` and
`SKIP_ECC_CERTIFICATE_REQUEST_SIGNING`. Supported values are now
`net48`, `net8.0`, `net9.0`, `net10.0`.
- The `NET_STANDARD_TESTS` define is removed from 29 test projects.
- The "build netstandard shells as net10.0" overrides are removed from
18 projects.
- `RestrictForLegacyTfm` now only applies to `net48`.
- `tests/customtest.bat` is updated to the new values.

**Build / packaging / CI**
- MSBuild TFM conditions are simplified: Kestrel package choice, MQTTnet
v4 (net48 only), BouncyCastle, Di/OpenUsd/Vision/Stress test excludes,
and `System.IO.FileSystem.AccessControl` (dropped from Core).
- The MigrationAnalyzer nuspec drops its net472/netstandard2.1 lib
groups. The Azure signing lists are updated.
- `get-ci-matrix.ps1` removes the net472/netstandard test profiles
(including the PR-gating `linux-netstandard2.1`) and the matching build
legs.

**Breaking API change (2.0 preview):** removed
`HttpsTransportListener.IsWssTransportSupported` and
`ReplicatedGossipOptions.IsGossipStateDecodingSupported`. They only
returned `false` on netstandard2.1 and are now always `true`. The test
guards that used them were removed as well.

**Docs**
- NuGet READMEs, `README.md`, `DeveloperGuide.md`, `PubSub.md`, the
certificate/ECC/identity docs and the 2.0 migration guide (plus its
`.agents` skill mirror) are updated.
- The migration guide notes that 4.7.2 consumers must move to 4.8 and
netstandard2.1 consumers to net8.0+.

### Reviewer notes
- The `System.IO.FileSystem.AccessControl` `PackageVersion` pin is left
in `Directory.Packages.props`. It has no direct reference any more, but
transitive pinning is on, so it was kept to avoid shifting resolved
versions.
- The `#if` rewrite was done mechanically by a truth-table evaluator.
The diff in the `.cs` files is limited to directive lines, removed dead
branches, and stale-comment updates.

### Verified locally
- `dotnet build UA.slnx -p:CustomTestTarget=net48`: 0 errors.
- `dotnet build UA.slnx -p:CustomTestTarget=net10.0`: 0 errors.
- Default multi-target builds (no `CustomTestTarget`) of Opc.Ua.Types,
Bindings.Https, PubSub.Mqtt and Redundancy.Server succeeded.
- net10.0 tests:
  - Opc.Ua.Redundancy.Server.Tests: 840/840 passed.
  - Opc.Ua.Types.Tests: 11363 passed, 1 skipped.
  - ModelDependencyScanner tests: 20/20 passed.
- The full test suite was **not** run locally.

## Related Issues

- None

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:09:49 +02:00

1057 lines
46 KiB
Markdown

# 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<OpcUaServerOptions>)` 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<TAuth>()` 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<IServiceProvider, ICertificateValidatorEx?, IUserTokenAuthenticator>)` | 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<T>`, `AddIdentityAugmenter<T>`,
`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<UserTokenType> SupportedTokenTypes
=> new[] { UserTokenType.IssuedToken };
public IReadOnlyList<string> 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<IUserIdentity> 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<AccessToken> AcquireAsync(
AuthorizationServerMetadata metadata,
CancellationToken ct = default)
{
return new ValueTask<AccessToken>(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<string, byte[], bool> m_checkPassword;
public StaticUserNameAuthenticator(Func<string, byte[], bool> checkPassword)
{
m_checkPassword = checkPassword ?? throw new ArgumentNullException(nameof(checkPassword));
}
public UserTokenType TokenType => UserTokenType.UserName;
public string? IssuedTokenProfileUri => null;
public ValueTask<AuthenticationResult> AuthenticateAsync(
AuthenticationContext context,
CancellationToken ct = default)
{
if (context.TokenHandler is not UserNameIdentityTokenHandler u)
{
return new ValueTask<AuthenticationResult>(
AuthenticationResult.NotHandled);
}
if (string.IsNullOrEmpty(u.UserName) || u.DecryptedPassword == null)
{
return new ValueTask<AuthenticationResult>(
AuthenticationResult.Reject(
new ServiceResult(StatusCodes.BadIdentityTokenInvalid)));
}
if (!m_checkPassword(u.UserName, u.DecryptedPassword))
{
return new ValueTask<AuthenticationResult>(
AuthenticationResult.Reject(
new ServiceResult(StatusCodes.BadUserAccessDenied)));
}
var identity = new UserIdentity(u); // wraps the wire handler
return new ValueTask<AuthenticationResult>(
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<IUserIdentity> 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<IUserIdentity>(identity);
}
}
services.AddOpcUa()
.AddServer(o => o.ApplicationUri = "urn:my-server")
.AddIdentityAugmenter<MyTenantAugmenter>();
// 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<string, object> claims,
IReadOnlyList<string> groups,
IReadOnlyList<string> 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<NodeId> GrantedRoleIds => default;
public IUserIdentityTokenHandler TokenHandler { get; }
public IReadOnlyDictionary<string, object> Claims { get; }
public IReadOnlyList<string> Groups { get; }
public IReadOnlyList<string> 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<AccessToken> 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<TIssuer>(...)`; 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<Func<string, byte[], bool>>(checkPassword);
services.AddOpcUa()
.AddServer(o =>
{
o.ApplicationName = "MyServer";
o.ApplicationUri = "urn:example:my-server";
o.EndpointUrls.Add("opc.tcp://localhost:4840");
})
.AddIdentityAuthenticator<StaticUserNameAuthenticator>()
.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<T>()`, `.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<ISessionFactory>();
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<AuthenticationResult> AuthenticateAsync(
AuthenticationContext context, CancellationToken ct = default)
{
if (context.TokenHandler is not UserNameIdentityTokenHandler userName)
{
return new ValueTask<AuthenticationResult>(AuthenticationResult.NotHandled);
}
return new ValueTask<AuthenticationResult>(
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<MyUserNameAuthenticator>();
// 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://<server-app-id>`, or the target server
`ApplicationUri`; map user-delegated scopes from `AuthorizationServerMetadata.Scopes`, and use
`<server-app-id>/.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/<server-fqdn>` for host-bound deployments or `OPCUA/<server-fqdn>` 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/)