Files
UA-.NETStandard/Docs/KeyCredentialService.md
Marc Schier 0000513cde Pluggable user-identity provider model (#3807)
# Description

Introduces a pluggable user-identity provider model covering all OPC UA
Part 6 §6.5 mechanisms (Anonymous, UserName, X509, IssuedToken/JWT) on
both client and server, with full
`Microsoft.Extensions.DependencyInjection` integration, plus the Part 12
§9 `AuthorizationService` two-phase token-request flow **including
refresh-token rotation**, the Part 12 §8 KeyCredential Push model, and a
clean provider model for OPC 10000-12 §7.2 `ApplicationSelfAdmin`. The
work is split across phase-by-phase commits so it can be reviewed
independently; nothing here changes wire-level behaviour or forces
existing applications to migrate.

### Documentation

All under `Docs/`:

- `IdentityProviders.md` — full developer guide: three-layer model,
recipes, configuration reference, "what gets auto-wired" matrix, JWS
algorithm table, identity-augmenters section, **"Implementing your own
provider"** how-to subsections for Entra / OIDC / Windows / AspNetCore
(sibling files merged in per review feedback).
- `DependencyInjection.md` — `Identity` sections with `appsettings.json`
examples, identity-extension matrix, `WithAuthorizationService` +
`WithKeyCredentialPush` + `AddIdentityAugmenter` extensions documented.
- `RoleBasedUserManagement.md` — claim-criteria callout describing
`IIdentityClaims` probe surface.
- `AuthorizationService.md` — Architecture diagram +
`IAccessTokenProvider` conceptual narrative restored from master; modern
`StartRequestToken` / `FinishRequestToken` flow leads; refresh-token
recipe added.
- `KeyCredentialService.md` — Push model + Pull vs Push vs Bridge
decision matrix + Hybrid (Push + Pull) section; explicit experimental
warning for the bridge.
- `GDS.md` — KeyCredential Push, `StartRequestToken`,
`GdsApplicationSelfAdminProvider`, **shipped Refresh-token support with
rotation policy and audit semantics** all documented.
- `MigrationGuide.md` — User Identity Providers section listing
`[Obsolete]` markers + mechanical recipes; SelfAdmin migration via
`IIdentityAugmenter`; `EnableRefreshTokens` default note.
- `README.md` index entries.

## Checklist

- [x] 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.
- [x] 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.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.
2026-06-02 01:14:54 +02:00

315 lines
13 KiB
Markdown

# KeyCredentialService Developer Guide
OPC 10000-12 §8 defines the **KeyCredentialService** — a mechanism for
a GDS to issue and manage credentials (e.g. username/password, API keys,
tokens) on behalf of applications that need to authenticate to
non-OPC UA services such as MQTT brokers or REST APIs.
> **Scope note**: this service is for credentials targeting **non-OPC
> UA** resources (MQTT brokers, REST APIs, etc. — see `ResourceUri`
> + `ProfileUris` on `KeyCredentialServiceType`). The optional bridge
> authenticator described below is **EXPERIMENTAL** under the vendor URI
> `urn:opcfoundation:netstandard:profile:authentication:keycredential`;
> it is **not** an OPC UA Part 6 §6.5.3 conformance claim. For
> standards-conformant OPC UA session authentication, use
> [AuthorizationService](AuthorizationService.md) (JWT) and the
> [Identity Providers](IdentityProviders.md) infrastructure.
## Architecture
```
┌─────────────────┐ StartRequest ┌────────────────────────┐
│ OPC UA Client │ ──────────────────► │ ApplicationsNodeManager │
│ (or application)│ ◄────────────────── │ (GDS Server) │
│ │ FinishRequest │ │
│ │ ──────────────────► │ ┌──────────────────────┐ │
│ │ Revoke │ │ IKeyCredentialRequest │ │
└─────────────────┘ │ │ Store │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ ISecretStore │ │
│ └──────────────────────┘ │
└────────────────────────────┘
```
## Client API
### KeyCredentialServiceClient
A thin client proxy for the three KeyCredentialService methods. Requires
an active `ISession` and the `NodeId` of the KeyCredentialService
instance in the server's address space.
```csharp
using Opc.Ua.Gds.Client;
// After connecting a session to the GDS server:
var kcClient = new KeyCredentialServiceClient(session, serviceNodeId);
// 1. Request a new credential
NodeId requestId = await kcClient.StartRequestAsync(
applicationUri: "urn:my-app",
publicKey: myPublicKey,
securityPolicyUri: null, // server chooses
requestedRoles: default); // server chooses
// 2. Finish (collect) the credential
var (credentialId, credentialSecret, thumbprint, policyUri, grantedRoles)
= await kcClient.FinishRequestAsync(requestId, cancelRequest: false);
// Use credentialId + credentialSecret to authenticate to the
// target service (MQTT broker, REST API, etc.)
// 3. Revoke when no longer needed
await kcClient.RevokeAsync(credentialId);
```
### Discovering the Service NodeId
The KeyCredentialService instance is typically found under the
server's `Directory` or `ServerConfiguration` node. Browse for
objects of type `KeyCredentialServiceType` (namespace 0):
```csharp
// Browse the server for KeyCredentialServiceType instances
var browser = new Browser(session);
// ... or use the well-known NodeId if your GDS exposes one.
```
## Server-Side: Implementing a Provider
### IKeyCredentialRequestStore
The server-side abstraction for the credential lifecycle:
```csharp
public interface IKeyCredentialRequestStore
{
// Begin a new credential request. Returns a request ID.
NodeId StartRequest(
string applicationUri,
ByteString publicKey,
string? securityPolicyUri,
ArrayOf<NodeId> requestedRoles);
// Complete or cancel a request. Returns issued credential data.
KeyCredentialRequestState FinishRequest(
NodeId requestId,
bool cancelRequest,
out string? credentialId,
out ByteString credentialSecret,
out string? certificateThumbprint,
out string? securityPolicyUri,
out ArrayOf<NodeId> grantedRoles);
// Revoke a previously issued credential.
void Revoke(string credentialId);
}
```
### Built-in Implementation
`InMemoryKeyCredentialRequestStore` provides a ready-to-use
in-process implementation. It auto-approves requests and generates
random 32-byte secrets.
**With ISecretStore integration** — credential secrets are persisted
through any `ISecretStore` backend (Key Vault, DPAPI, etc.):
```csharp
// Use the default in-memory secret store
var store = new InMemoryKeyCredentialRequestStore();
// Or plug in a production secret store
ISecretStore vault = new MyKeyVaultSecretStore("KeyCredential");
var store = new InMemoryKeyCredentialRequestStore(vault);
```
### Writing a Custom Implementation
For production systems with approval workflows:
```csharp
public class DatabaseKeyCredentialStore : IKeyCredentialRequestStore
{
private readonly IDbConnection _db;
public NodeId StartRequest(
string applicationUri,
ByteString publicKey,
string? securityPolicyUri,
ArrayOf<NodeId> requestedRoles)
{
// 1. Insert a pending request into the database
// 2. Return the generated request ID
// 3. Optionally notify an admin for approval
var id = _db.InsertPendingRequest(applicationUri, publicKey);
return new NodeId(id);
}
public KeyCredentialRequestState FinishRequest(
NodeId requestId,
bool cancelRequest,
out string? credentialId, ...)
{
var record = _db.GetRequest(requestId);
if (record.State == "Pending")
{
// Still awaiting approval
credentialId = null;
// ... set other out params to defaults
return KeyCredentialRequestState.New;
}
// Return the approved credential
credentialId = record.CredentialId;
// ...
return KeyCredentialRequestState.Completed;
}
public void Revoke(string credentialId)
{
_db.RevokeCredential(credentialId);
// Optionally notify the target service to invalidate
}
}
```
### Wiring into the GDS Server
Set the `KeyCredentialRequestStore` property on the
`ApplicationsNodeManager` (done via `GlobalDiscoverySampleServer`
or a custom server):
```csharp
// In your INodeManagerFactory or server setup:
var appNodeManager = new ApplicationsNodeManager(server, configuration, ...);
appNodeManager.KeyCredentialRequestStore =
new InMemoryKeyCredentialRequestStore(mySecretStore);
```
## Decision matrix: Push vs Pull vs Bridge
| Model | Primary actor | Use when | Standard / conformance posture |
|---|---|---|---|
| Pull (`KeyCredentialServiceType`) | Client/application asks a GDS to issue a credential. | The GDS owns approval and issuance; the caller later retrieves the issued secret with `FinishRequest`. | OPC 10000-12 §8 KeyCredentialService. |
| Push (`KeyCredentialConfigurationFolderType`) | Administrator or provisioning agent writes credentials to a resource server. | A resource server must store, rotate, or delete credentials locally through `CreateCredential`, `UpdateCredential`, and `DeleteCredential`. | OPC 10000-12 §8 Push model; enabled with `WithKeyCredentialPush()`. |
| Bridge (`KeyCredentialBridgeAuthenticator`) | OPC UA client presents a KeyCredential proof as an issued identity token. | Closed deployments need an interim bridge between GDS-issued secrets and UA session authentication. | **Experimental vendor extension only; not a Part 6 §6.5.3 conformance claim.** |
### Hybrid (Push + Pull)
- Enable both models when a GDS issues credentials (Pull) and hosts the Push subject. Admin tools can push
freshly issued credentials to target resource servers in one round-trip.
- For wiring in the same process, use `services.AddOpcUa().AddGdsServer(...).WithKeyCredentialPush()`.
The Pull side stays on the GDS, and the Push side is enabled on the same node so admin clients write
credentials directly to the GDS's resource-server view.
- For wiring across processes, the GDS uses the default Pull path (`InMemoryKeyCredentialRequestStore`),
while each resource server enables Push with `WithKeyCredentialPush()`. The admin tool reads from the
GDS and writes to each resource server.
- The bridge authenticator is independent of Push/Pull: it lets the resource server accept KeyCredential
blobs as a UA identity. The hybrid model addresses issuance and distribution; the bridge addresses
consumption.
## Resource Server Push Binding
A regular OPC UA resource server can opt in to the Part 12 §8 Push model
with `WithKeyCredentialPush()`. The binding mounts on the standard
`ServerConfiguration/KeyCredentialConfiguration` folder by default and
stores secrets through `IKeyCredentialStore` (default:
`InMemoryKeyCredentialStore` backed by `ISecretStore`).
```csharp
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua;
using Opc.Ua.Server.Hosting;
services.AddOpcUa()
.AddServer(options =>
{
options.ApplicationUri = "urn:my-resource-server";
options.ProductUri = "uri:my-company:resource-server";
options.EndpointUrls.Add("opc.tcp://localhost:4840");
})
.WithKeyCredentialPush();
```
Production deployments should register an `IKeyCredentialStore` backed by
a durable secret store before calling `WithKeyCredentialPush()`.
## Experimental KeyCredential Issued-Token Bridge
> **WARNING — EXPERIMENTAL**: `KeyCredentialBridgeAuthenticator` is a
> vendor extension under
> `urn:opcfoundation:netstandard:profile:authentication:keycredential`.
> It is **not** a Part 6 §6.5.3 conformance claim. Deployments that
> claim OPC UA conformance **MUST NOT** use this bridge as their
> standards-conformant user-token mechanism. Use it only in closed
> deployments where the GDS, resource server, and clients are controlled
> by the same operator.
The bridge lets a server validate a JSON issued-token payload containing
a `credentialId`, nonce timestamp, and HMAC proof. The server looks up
the credential in `IKeyCredentialStore`, validates the proof with the
stored secret, and returns an identity populated from the credential's
subject claims.
Client-side, `GdsKeyCredentialAccessTokenProvider` adapts a
`KeyCredentialServiceClient` to `IAccessTokenProvider`, so the standard
`IssuedTokenIdentityProvider` can materialize a UA `IssuedIdentityToken`
for the bridge profile.
## Audit Events
All KeyCredentialService operations emit audit events automatically:
| Operation | Audit Event Type |
|-----------|-----------------|
| StartRequest | `KeyCredentialRequestedAuditEventType` |
| FinishRequest | `KeyCredentialDeliveredAuditEventType` |
| Revoke | `KeyCredentialRevokedAuditEventType` |
Credential secrets are **never** included in audit payloads.
## End-to-End Example
```csharp
// ── Server Side ──────────────────────────────────────
// 1. Create the credential store
var secretStore = new InMemorySecretStore("KeyCredentials");
var kcStore = new InMemoryKeyCredentialRequestStore(secretStore);
// 2. Create the GDS server with credential support
var gdsServer = new GlobalDiscoverySampleServer(
database, requestStore, certGroup, userDb, telemetry);
// 3. After server starts, wire the store
// (done automatically by ApplicationsNodeManager when
// KeyCredentialRequestStore is set before CreateAddressSpace)
// ── Client Side ──────────────────────────────────────
// 1. Connect to the GDS
var gdsClient = new GlobalDiscoveryServerClient(config);
await gdsClient.ConnectAsync(gdsEndpointUrl);
// 2. Find the KeyCredentialService node
// (browse or use a well-known NodeId)
// 3. Create the client proxy
var kcClient = new KeyCredentialServiceClient(
gdsClient.Session, serviceNodeId);
// 4. Request → Finish → Use → Revoke
NodeId reqId = await kcClient.StartRequestAsync(
"urn:my-mqtt-bridge", default, null, default);
var (credId, secret, _, _, _) =
await kcClient.FinishRequestAsync(reqId, false);
// Use credId/secret to connect to MQTT broker...
// When done:
await kcClient.RevokeAsync(credId);
```