Authenticate to a Backend
Goal
Create or refresh credentials for a backend connection without leaking secrets into logs, route metadata, or tool output.
When to use this
Use this when adding a new direct-mode connection (file, http,
omniverse-storage-service, s3, gcs, azure, opendal, or any
other backend kind), or when an existing connection reports
AuthRequired, CredentialExpired, or an inactive auth_state_kind
in ovstorage_doctor.
Recipe
- Inspect the configured backend layers and connections with
ovstorage-user-getting-started. - Read the backend descriptor and credential schema before prompting for
anything secret. Stack callers get this from the root Layer's
list_kinds; CLI users can use theconnectwizard. - For
file, no external credential is normally required. Register the root path and keep credentials empty. - For
http, prefer public URLs where they exist. If an HTTP route needs credentials, pass them through the connection credential schema —bearer_token,username+password,signed_query, orsecret_headers— never through the address string; a query inroot_urlorprefixis rejected asInvalidArgumentatinstantiate. - If
authenticate_connectionreturns an auth event stream, surfaceOpenBrowser,DeviceCode,Progress,Succeeded,Failed, andCancelledevents to the user exactly as events, then stop on a terminal event. - If it returns
Unsupported, no flow ran and the connection's state is unchanged, so keep the registration and report the state rather than treating it as a failure. Backends that always answer this way:s3,gcs,azure,opendal; broker connections on a direct endpoint (any address that is nothttp(s)://); andfileandhttp, which have no connection-auth driver at all.Unsupportedcan also come from a layer that does not implement the call — a credentialless visibility-override row, or a remote service that has not implemented the RPC — so read it as "no flow was offered here", not as a statement about the backend kind. Where a credential is what is missing, supply one the origin accepts; re-running the flow is not the fix.
Backend-specific auth
omniverse-storage-service
The omniverse-storage-service backend authenticates over OIDC when its
address names a discovery root. The plugin reads /api/v1/auth-config
from that root to learn the OIDC issuer and client; the standard OIDC discovery doc supplies the
auth / token / device endpoints.
Two interactive flows:
- Browser (
InteractiveAuthCapability::Browser): PKCE with a loopback redirect. The plugin opens the user's browser to the authorization endpoint, captures the code at a transient localhost callback, and exchanges it for the token bundle. - Headless (
InteractiveAuthCapability::Headless): RFC 8628 device flow. The plugin requests a device code, surfaces a user-code + verification URL viaAuthEvent, and polls the token endpoint until the user completes the flow on a separate device.
In both cases the resulting OAuthToken lands as the connection's
oauth credential bundle. The plugin proactively refreshes the access
token in a background task at ~90% of the token's TTL using the
refresh-token grant (or the client-credentials grant if that's the
configured method). No intervention required for long-running processes.
To pre-seed credentials (e.g. for CI), populate oauth with a long-lived
refresh token; the plugin will exchange it for an access token on first
use.
When address names a grpc:// or grpcs:// endpoint instead, there is
no discovery root and therefore no OIDC configuration: no sign-in flow,
no client-credentials grant, and no background refresh. Such a connection
takes an access token the host already holds — put it in the oauth
field with no refresh token — and the host rotates it by calling
update_connection_credentials again with a fresh one. An empty bundle
removes it.
s3
The descriptor exposes three credential methods:
static_key— long-lived IAM user key. Populateaws_access_key_id+aws_secret_access_key.session— short-lived STS / SSO credentials. Populateaws_access_key_id+aws_secret_access_key+aws_session_token.aws_credentials_file— read from an INI section. Populatefile_path(defaults to~/.aws/credentials) andprofile(defaults todefault); the per-connectionprofileconfig key picks the section.
Each credential field's descriptor default is the matching ${AWS_*}
env-var placeholder (e.g. ${AWS_ACCESS_KEY_ID}); the host's
bundle-resolution layer expands these before the bundle reaches the
plugin. From the plugin's point of view the resolution chain is
bundle fields → env vars (via placeholder expansion) → shared
credentials file. IMDS, AWS SSO, web-identity tokens, and STS role
assumption are not implemented in-process; operators that need them
run their session helper (aws sso login, etc.) to materialise a
static-key profile.
gcs
The descriptor exposes two credential methods:
service_account_key— paste a service-account JSON keyfile into theservice_account_keyfield. The plugin auto-detects the JSONtype:service_accounttriggers JWT-bearer exchange (and is the only path that can mint V4-signed read URLs);authorized_usertriggers an OAuth2 refresh-token exchange (reads stream throughalt=mediaunder the cached bearer because authorized-user creds cannot V4-sign).gcloud_adc_file— populatefile_pathwith the path to an ADC JSON file written bygcloud auth application-default login(defaults to~/.config/gcloud/application_default_credentials.json, with~expanded againstHOME/USERPROFILE+HOMEDRIVE).
The plugin does not consult GOOGLE_APPLICATION_CREDENTIALS or
any other env var; if you need env-var expansion, do it in the host's
bundle-resolution layer before handing the bundle to the plugin.
GCE/GKE metadata, workload-identity federation
(external_account), and service-account impersonation
(impersonated_service_account) are not implemented in-process.
http
The descriptor exposes four credential methods. Distinct channels combine — for example, a connection may hold a signed query and secret headers at once — and a connection with none of them is anonymous:
bearer— opaque token sent asAuthorization: Bearer <token>on every request. Populatebearer_token.basic— HTTP Basic authentication. Populate bothusernameandpassword; either value may be empty, but not both.signed_query— pre-issued query string appended verbatim to every request URL (leading?optional). Populatesigned_query, and set thesigned_query_scopeconfig key to declare the token's scope family.secret_headers— credential-bearing headers in wire form, oneName: Valueper line, sent with every request. Populatesecret_headers. The separator is a line break, not a comma, so a value may contain commas. This is the secret-bearing counterpart to thedefault_headersconfig key, which rejectsAuthorization,Cookie, andProxy-Authorization; those names are accepted here, whileHost,Range,If-Match, and the framing and hop-by-hop headers are refused.
A signature is a credential with a declared scope, not part of the
address. Writing a query into root_url or prefix returns
InvalidArgument at instantiate: the object key is appended after the
route prefix, and a signature buried in config can be neither scoped nor
rotated. signed_query_scope accepts prefix (the token authorizes
everything under root_url — an Azure account, container, or directory
SAS, a CloudFront signed URL with a custom policy) or object (a
per-object presign, which a connection cannot hold and which returns
Unsupported; dispatch such a URL as a per-request address). The plugin
also refuses a token whose parameters name a per-object signature —
X-Amz-Signature, sr=b, a CloudFront canned policy — while
signed_query_scope says prefix. Supplying signed_query with no
scope, or a scope with no signed_query, is InvalidArgument; so is
setting Basic or Bearer credentials together with a secret_headers entry
named Authorization. Userinfo in root_url is the legacy Basic channel and
likewise conflicts with another Authorization writer, while it may coexist
with a signed query or non-Authorization secret header.
All channels are hot-swappable through
update_connection_credentials while the connection is live, but the
rotation may not change the connection's exact shape: the Authorization
method, presence of a signed query, and secret-header names and multiplicity
must remain fixed. Remove and re-add the connection to change that shape.
signed_query_scope is configuration rather than a credential, so rotating
into a signed query without that key (or supplying the key with no token) is
InvalidArgument. A replacement is probed before the atomic swap; reads keep
using the old snapshot while the probe runs. Credentials over a plaintext
http:// route are refused except on loopback, and any secret-bearing
connection refuses an https:// to http:// redirect downgrade.
azure
The descriptor exposes four distinct credential methods. The plugin resolves them in this order:
account_key— Shared Key signing. Populateaccount_key.sas_token— pre-issued shared-access signature appended verbatim to request URLs. Populatesas_token.workload_identity— Entra OAuth2 federated workload identity. Populatefederated_token_file+client_id+tenant_id.service_principal— Entra OAuth2 client-credentials. Populateclient_id+client_secret+tenant_id.
Each credential field's descriptor default is the matching
${AZURE_*} env-var placeholder
(${AZURE_STORAGE_ACCOUNT_KEY}, ${AZURE_STORAGE_SAS_TOKEN},
${AZURE_TENANT_ID}, ${AZURE_CLIENT_ID}, ${AZURE_CLIENT_SECRET},
${AZURE_FEDERATED_TOKEN_FILE}); the host's bundle-resolution layer
expands these before the bundle reaches the plugin. The plugin
itself never reads AZURE_* env vars at runtime. Managed Identity
(IMDS), az login, VS Code, and PowerShell credential sources from
DefaultAzureCredential are not implemented in-process.
opendal
Auth depends on the chosen service (one of fs, s3, webdav —
the only services compiled in). fs requires no credentials.
The OpenDAL s3 driver wants access_key_id + secret_access_key
(bare names, not aws_*-prefixed — these are NOT
interchangeable with the first-party s3 plugin's credential
shape). webdav uses HTTP Basic credentials (password, plus
username passed through config_json). The adapter accepts only
SecretValue::Bytes, SecretValue::OAuthToken, and
SecretValue::File for these fields; SecretValue::SystemIdentity
and SecretValue::MtlsCertPair are rejected as Unsupported.
Ambient-identity flows (Kerberos ticket cache, etc.) are not
threaded through. See
plugin-opendal.md
for per-driver detail.
nucleus
Omniverse Nucleus's auth shape predates the unified OAuth pattern used by the Omniverse Storage Service and the cloud backends. The descriptor exposes three user-selectable credential methods:
sso— single sign-on (browser). Recommended. Binds no credential fields. The plugin (establish_interactive_auth) callsTokens::subscribeto register a one-shot login intent, receives a server-generatednonce, and surfacesAuthEvent::OpenBrowserwith the Nucleus login URL annotated with?nonce=<nonce>. The host UI opens the URL; once the user completes the form, the second response on the same subscription carries the authentication result. The plugin never touches raw passwords or 2FA challenges on this path.userpass— username and password. Bindsusernameandpassword; drives Nucleus'sCredentials::authflow. For service accounts and headless deployments.api_token— API token. Binds a singleapi_tokencredential. DrivesTokens::auth_with_api_token; takes precedence overusername/password. TheAuthenvelope'susernameis recorded as the authenticated principal. The recommended automation path.
Successful authentication via sso produces a refresh token that
the plugin persists in the OS keyring keyed by the server
hostname (not the host-minted, unstable ConnectionId). On
subsequent connects, warm-continuation reads refresh_token from
the keyring, rediscovers tokens_url, and exchanges the refresh
token without re-prompting; on a service-call Expired /
InvalidToken, the plugin invokes Tokens::refresh once per
token-generation epoch (concurrent callers share a single refresh
under a per-shared mutex). AuthRequired / AuthExpired /
PermissionDenied clear the keyring entry; Transient preserves
it for retry. SSO (SSO::*) and DeviceFlow (DeviceFlow::*) are
declared as target / deferred flows; they are not wired today.
broker
The broker plugin authenticates to an upstream ovstorage-broker
daemon; the broker itself holds the long-lived cloud credentials and
runs its own authn substrate against whatever listener-authn mode
operators configured. From the calling host's perspective the
client-side credential surface is intentionally minimal:
client_credentials— OIDC client credentials grant. Bindsclient_id+client_secret. Only valid when the configuredaddressis a discovery URL (not a direct gRPC endpoint): the plugin fetches/api/v1/auth-config, follows the publishedopenid_configurationURL to the IdP's standard OIDC discovery doc, and drives a non-interactive client-credentials grant. Suited for service accounts and headless deployments.
Interactive flows (PKCE loopback for Browser, RFC 8628 device flow
for Headless) are driven by Factory::authenticate against the
same broker-published auth-config. The host signals which mode is
available through the gRPC x-ov-iauth: browser|headless|none
metadata header attached by the broker-client SDK on every RPC; the
broker reads this on its streaming Auth RPC, threads the parsed
InteractiveAuthCapability into the upstream-OAuth flow, and emits
AuthEvent::OpenBrowser / DeviceCode / Progress / Succeeded
back to the host. None mode causes the broker to emit a terminal
Failed { AuthRequired } envelope without opening a browser tab or
device prompt — the fail-fast shape render workers and CI hosts
need.
Three-tier OAuth, broker-held. Successful interactive auth lands
a refresh token on the broker via the unary RegisterCredential
round-trip; the broker persists it through its OAuthCredentialProvider
secret_tokensrow +SecretStore(per-(BackendId, PrincipalView)slot). The host process holds only the short-lived broker bearer — never the upstream cloud credentials. Workers and a coordinator that share aPrincipalViewat the broker hit the same cache slot, which covers the multi-tenant SaaS and render-worker shapes without any host-side coordination.
The broker daemon's listener authn mode (jwt_verify,
trusted_unsigned_jwt, trusted_forwarded_headers, peer_cred, or
the reserved mtls) decides how the host's bearer is validated —
that's operator-owned config and is not part of the
broker-client-side credential schema. See
docs/public/broker-operator/README.md
for the daemon side.
References
docs/public/library-rust/README.mdforadd_connection, credential persistence, andauthenticate_connection.plugin-filefor local filesystem configuration.plugin-httpfor HTTP route behavior.plugin-services-clientfor theomniverse-storage-serviceOIDC flow.plugin-nucleusfor the Omniverse Nucleus auth flows.plugin-brokerfor the broker-client Layer surface, discovery URL normalization, and the broker-held three-tier OAuth model.docs/public/broker-operator/README.mdfor the daemon-side listener authn modes a broker operator configures.ovstorage-user-handle-errorsfor interpreting auth and permission failures.