Skip to content
Back to skills

Ovstorage User Authenticate To Backend

DSecurity

Use when configuring credentials or driving interactive authentication for a direct ovstorage backend connection.

  • 15 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 19, 2026
ai-agentsrustgoshellawsazureapibackend

Works with

  • terminal
  • cli
  • api
  • mcp

Security analysis

D50/100
  • criticalAccesses sensitive system or user directories
  • criticalAccesses sensitive system or user directories

Pro shows the line behind each finding and how to fix it

Scanned October 19, 2026

npx -y skills add NVIDIA-Omniverse/ovstorage --skill ovstorage-user-authenticate-to-backend --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ovstorage User Authenticate To Backend?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ovstorage User Authenticate To Backend
[![Security: D — Skills Directory](https://www.skillsdirectory.com/api/skills/nvidia-omniverse-ovstorage-user-authenticate-to-backend/badge)](https://www.skillsdirectory.com/skills/nvidia-omniverse-ovstorage-user-authenticate-to-backend)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ovstorage-user-authenticate-to-backend
description: Use when configuring credentials or driving interactive authentication for a direct ovstorage backend connection.
license: CC-BY-4.0
version: "0.1.0"
author: NVIDIA Omniverse
tags: [ovstorage, authentication, credentials]
tools: [Read]
compatibility: Requires ovstorage MCP tools or equivalent library calls. Backend credentials, browser/device auth, network access, and service URLs are user-supplied when needed.
---

# 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`, `huggingface`,
`opendal`, or any
other backend kind), or when an existing connection reports
`AuthRequired`, `CredentialExpired`, or an inactive `auth_state_kind`
in `ovstorage_doctor`.

## Recipe

1. Inspect the configured backend layers and connections with
   [`ovstorage-user-getting-started`](../ovstorage-user-getting-started/SKILL.md).
2. 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 the `connect` wizard.
3. For `file`, no external credential is normally required. Register the root
   path and keep credentials empty.
4. 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`, or
   `secret_headers` — never through the address string; a query in `root_url`
   or `prefix` is rejected as `InvalidArgument` at `instantiate`.
5. If `authenticate_connection` returns an auth event stream, surface
   `OpenBrowser`, `DeviceCode`, `Progress`, `Succeeded`, `Failed`, and
   `Cancelled` events to the user exactly as events, then stop on a terminal
   event.
6. 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`, `huggingface`, `opendal`; broker connections on a direct endpoint (any
   address that is not `http(s)://`); and `file` and `http`, which have no
   connection-auth driver at all. `Unsupported` can 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 via `AuthEvent`, 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. Populate
  `aws_access_key_id` + `aws_secret_access_key`.
- `session` — short-lived STS / SSO credentials. Populate
  `aws_access_key_id` + `aws_secret_access_key` +
  `aws_session_token`.
- `aws_credentials_file` — read from an INI section. Populate
  `file_path` (defaults to `~/.aws/credentials`) and `profile`
  (defaults to `default`); the per-connection `profile` config 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 an ADC JSON blob into the
  `service_account_key` field. The plugin auto-detects the JSON
  `type`: `service_account` triggers JWT-bearer exchange (and is
  the only path that can mint V4-signed read URLs);
  `authorized_user` triggers an OAuth2 refresh-token exchange;
  `external_account` runs the workload-identity STS exchange (the
  subject token is re-fetched from its `file` or `url` source on
  every exchange, with an optional impersonation hop; token and
  impersonation endpoints must be https `*.googleapis.com` or
  loopback, the subject-token url https or loopback, and
  workforce-pool configs are rejected);
  `impersonated_service_account` mints its source credential's
  bearer and spends it on `generateAccessToken`. Every type except
  `service_account` reads through the plugin (`alt=media`) because
  only an RSA private key can V4-sign.
- `gcloud_adc_file` — populate `file_path` with the path to an
  ADC JSON file of any of those four types — e.g. written by
  `gcloud auth application-default login` (optionally with
  `--impersonate-service-account`) or by
  `gcloud iam workload-identity-pools create-cred-config`
  (defaults to `~/.config/gcloud/application_default_credentials.json`,
  with `~` expanded against `HOME` / `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-server identity and the AWS / `executable`
`external_account` source flavors 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 as `Authorization: Bearer <token>`
  on every request. Populate `bearer_token`.
- `basic` — HTTP Basic authentication. Populate both `username` and
  `password`; either value may be empty, but not both.
- `signed_query` — pre-issued query string appended verbatim to every
  request URL (leading `?` optional). Populate `signed_query`, and set
  the `signed_query_scope` **config** key to declare the token's scope
  family.
- `secret_headers` — credential-bearing headers in wire form, one
  `Name: Value` per line, sent with every request. Populate
  `secret_headers`. The separator is a line break, not a comma, so a
  value may contain commas. This is the secret-bearing counterpart to
  the `default_headers` config key, which rejects `Authorization`,
  `Cookie`, and `Proxy-Authorization`; those names are accepted here,
  while `Host`, `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. Populate `account_key`.
- `sas_token` — pre-issued shared-access signature appended verbatim
  to request URLs. Populate `sas_token`.
- `workload_identity` — Entra OAuth2 federated workload identity.
  Populate `federated_token_file` + `client_id` + `tenant_id`.
- `service_principal` — Entra OAuth2 client-credentials. Populate
  `client_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.

### `huggingface`

A single static access token in the `token` field (`hf_…`; a
read-scoped token is sufficient). The descriptor default is
`${HF_TOKEN}`, resolved by the host at TOML load — the plugin never
reads the environment itself. An empty bundle connects anonymously,
which serves public repositories; private and gated repositories
answer 404/403 without a token. The token is verified once at add
time with a lenient `whoami` check (only a definitive 401 parks the
connection); rotation goes through `update_connection_credentials`,
and `reauth` answers `Unsupported` because no interactive flow
exists.

### `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`](../../docs/public/plugin-storage/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`)
  calls `Tokens::subscribe` to register a one-shot login intent,
  receives a server-generated `nonce`, and surfaces
  `AuthEvent::OpenBrowser` with 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. Binds `username` and
  `password`; drives Nucleus's `Credentials::auth` flow. For
  service accounts and headless deployments.
- **`api_token`** — API token. Binds a single `api_token`
  credential. Drives `Tokens::auth_with_api_token`; takes
  precedence over `username` / `password`. The `Auth` envelope's
  `username` is 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. Binds
  `client_id` + `client_secret`. Only valid when the configured
  `address` is a discovery URL (not a direct gRPC endpoint): the
  plugin fetches `/api/v1/auth-config`, follows the published
  `openid_configuration` URL 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_tokens` row + `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 a `PrincipalView` at 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`](../../docs/public/broker-operator/README.md)
for the daemon side.

## References

- [`docs/public/library-rust/README.md`](../../docs/public/library-rust/README.md) for
  `add_connection`, credential persistence, and `authenticate_connection`.
- [`plugin-file`](../../docs/public/plugin-storage/plugin-file.md)
  for local filesystem configuration.
- [`plugin-http`](../../docs/public/plugin-storage/plugin-http.md)
  for HTTP route behavior.
- [`plugin-services-client`](../../docs/public/plugin-storage/plugin-services-client.md)
  for the `omniverse-storage-service` OIDC flow.
- [`plugin-nucleus`](../../docs/public/plugin-storage/plugin-nucleus.md)
  for the Omniverse Nucleus auth flows.
- [`plugin-broker`](../../docs/public/plugin-storage/plugin-broker.md)
  for the broker-client Layer surface, discovery URL normalization,
  and the broker-held three-tier OAuth model.
- [`docs/public/broker-operator/README.md`](../../docs/public/broker-operator/README.md)
  for the daemon-side listener authn modes a broker operator
  configures.
- [`ovstorage-user-handle-errors`](../ovstorage-user-handle-errors/SKILL.md) for interpreting
  auth and permission failures.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.