Roles and access
A hosting server that several applications and tenants share needs to keep each of them to its own DIDs, its own domains, and a fair share of storage, while operators keep full control.
WebVH Hosting does this with one access control list (ACL). Every caller authenticates to obtain a short-lived access token, and the caller’s ACL entry sets its role, the domains it may publish on, and optional quotas. Deploy the unified daemon →
Affinidi operates the appliance’s ACL and management UI. With the appliance, you manage your applications’ access in your VTA instead. See Grant and revoke access. The rest of this page describes access on a self-hosted deployment.
How callers authenticate
Each mechanism below issues the same kind of token: a short-lived JSON Web Token (JWT) access token with a refresh token.
| Mechanism | When it applies |
|---|---|
| Challenge-response | VTAs, applications, and backend services, for example a standalone edge server registering with the control plane. The caller signs a server-issued challenge as either a DIDComm v2.1 signed message or a SIOPv2 ID token. |
| Passkey (WebAuthn) | Human operators signing in to the management UI. An admin creates an enrolment invite with did-hosting-daemon invite --did <DID> --role <admin|owner>, did-hosting-control invite, or from the management UI. Redeeming the invite registers the passkey and adds the DID to the ACL if it is not already there. Requires public_url to be set. |
| VTA Wallet | Human operators signing in to the management UI with their VTA instead of a passkey. |
Requests sent over DIDComm or TSP, rather than REST, are checked against the ACL message by message: a sender with no ACL entry is refused.
A watcher is outside this model. It accepts DID updates pushed from an edge server authenticated with a shared push token, and it has no ACL.
What each role can do
Roles are stored in each ACL entry as admin, owner, or service.
| Role | Who it is for | What it can do |
|---|---|---|
| Admin | Operators | Full management: act on any DID, including the root .well-known DID, and manage the ACL, hosting domains, the service registry, and passkey invites. |
| Owner | DID owners, such as applications or tenants | Create, update, publish, disable and enable, roll back, transfer, and delete their own DIDs, and manage agent names and witness files for them. Limited to the domains and quotas set on their ACL entry. Cannot manage the ACL, domains, or registry. |
| Service | Backend service accounts, such as a standalone edge server | Register with the control plane and push usage statistics. Cannot use admin routes or DID management routes. |
When a unified daemon is set up in VTA-managed mode, the provisioning VTA is added to the ACL automatically as an admin with access to all domains. It can then create and manage DIDs on your behalf. DIDs your VTA creates are owned by the VTA’s DID.
The REST API refuses to delete your own ACL entry or to demote the last remaining admin. Through Trust Tasks, any revocation or role change that would leave no admin is refused with last_authority_protected.
How owners are scoped to domains and quotas
An ACL entry carries more than a role. Its fields are did, role, an optional label, a domain scope, and optional quotas.
The domain scope applies to owners only, because admins and service accounts can act on every domain. It takes one of three forms:
- All domains: the owner can publish on any hosting domain on the server.
- Allowed domains: the owner can publish only on the listed domains, and must name one on every request.
- Allowed domains with a default: as above, but requests that name no domain use the entry’s default.
An owner added to the ACL without a scope is limited to the server’s default domain. An owner created by redeeming a passkey invite can publish on all domains. For how hosting domains work, see Hosting domains.
Quotas cap how much an owner can store:
| Quota | Limits | When no value is set |
|---|---|---|
max_did_count | Number of DIDs. | No limit on the control plane and unified daemon. 20 on a standalone edge server, from default_max_did_count under [limits]. |
max_total_size | Total size of the owner’s DID content, in bytes. | No limit on the control plane and unified daemon. 1 MB on a standalone edge server, from default_max_total_size under [limits]. |
Admins are exempt from quotas. A request that would exceed a quota is refused with 403 and a message such as DID count limit reached (20).
In the management UI, these fields appear on the Access page as Domain scope, Default domain, Max DIDs, and Max size (MB).
How sessions and tokens expire
By default, a challenge is valid for 30 seconds, an access token for 15 minutes, and a session can be refreshed for 24 hours. A session idle for 15 minutes is refused on its next refresh, and an enrolment invite is valid for 24 hours. Each of these is configurable under [auth].
A caller’s role is read from its access token on each REST request, rather than from the ACL:
- ACL changes take effect at the caller’s next authentication or token refresh.
- An issued access token stays valid until it expires.
- Signing out of the management UI discards the token in the browser. The server keeps no sign-out record.
Challenge requests are rate limited to 30 per minute from each IP address. Behind a reverse proxy, set trusted_proxies under [server] so the limit applies to the real client address instead of the proxy’s.
Which operations need a passkey
Signing in with a passkey gives a session a higher assurance level (AAL2) than challenge-response authentication (AAL1). A session can raise its level mid-session through a passkey or VTA step-up. Deleting a hosting domain is the one control plane operation that requires the higher level, and is refused with step_up_required otherwise.
Related
Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.