Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion docs/source/reference/rest-api/websockets.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ to the client.

## Reconnecting an Active Conversation

To resume an active workflow, reconnect with the workflow's `conversation_id` query parameter and an identity credential that resolves to the same user who started it. The server restores state only when both values match. A connection with another identity, or no identity, does not receive the existing workflow state or its pending Human-in-the-Loop prompt.
To resume an active workflow, reconnect using the `conversation_id` query parameter for that workflow and an identity credential that resolves to the same user who started the workflow. The server restores the workflow state only when both values match. A connection with another identity, or no identity, does not receive the existing workflow state or the pending Human-in-the-Loop prompt for that workflow.

The server accepts the following identity credentials:

Expand All @@ -76,6 +76,42 @@ The server accepts the following identity credentials:

Clients can also send a JWT, API key, or Basic credentials through an `auth_message`. Send the message before expecting restoration; restoration occurs after authentication succeeds.

By default, all listed identity credential methods are accepted. To restrict WebSocket identity credentials, set `accepted_identity_credentials` in the FastAPI front-end configuration:

```yaml
general:
front_end:
_type: fastapi
accepted_identity_credentials:
- session_cookie
- jwt
```

Supported values are `session_cookie`, `jwt`, `api_key`, and `basic`. The `api_key` value covers both Bearer API keys and the `X-API-Key` header. An empty list rejects every supplied identity credential. When a client supplies a disabled credential method, the server returns an authentication error and does not restore workflow state.

JWT signature and claim verification is optional. Define one or more named JWT authentication providers, then select them with `identity_authentication` in the FastAPI front-end configuration. Each selected provider verifies JWTs from its configured issuer before the server resolves the user identity:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

```yaml
authentication:
corporate_jwt:
_type: jwt
issuer_url: https://identity.example.com
jwks_uri: https://identity.example.com/.well-known/jwks.json
audience: nemo-agent-toolkit
scopes:
- workflow:resume

general:
front_end:
_type: fastapi
accepted_identity_credentials:
- jwt
identity_authentication:
- corporate_jwt
```

Each JWT provider requires `issuer_url`, `jwks_uri`, and `audience`. It can also require `scopes` and configure `timeout` and `leeway`. Add another named provider and select it in `identity_authentication` to accept JWTs from another issuer. The issuer claim selects the matching provider; an unknown issuer or a failed signature, time, audience, or scope check returns an authentication error and does not restore workflow state. If `identity_authentication` is omitted, JWTs retain the existing decode-only behavior. Configuring `identity_authentication` while excluding `jwt` from `accepted_identity_credentials` is invalid.

## Auth Message
This message allows clients to authenticate over a WebSocket connection when header-based or
cookie-based authentication is not feasible (e.g., browser WebSocket APIs that do not support custom headers).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Named JWT identity verification provider."""

from nat.authentication.credential_validator.bearer_token_validator import BearerTokenValidator
from nat.authentication.interfaces import AuthProviderBase
from nat.authentication.jwt.jwt_auth_provider_config import JwtAuthProviderConfig
from nat.data_models.authentication import AuthResult
from nat.data_models.authentication import TokenValidationResult


class JwtAuthProvider(AuthProviderBase[JwtAuthProviderConfig]):
"""Validate inbound JWTs using a named issuer policy."""

def __init__(self, config: JwtAuthProviderConfig) -> None:
super().__init__(config)
self._validator = BearerTokenValidator(
issuer=config.issuer_url,
audience=config.audience,
jwks_uri=config.jwks_uri,
scopes=config.scopes,
timeout=config.timeout,
leeway=config.leeway,
)

async def verify(self, token: str) -> TokenValidationResult:
"""Verify a JWT against this provider's configured trust policy."""
return await self._validator.verify(token)

@property
def validator(self) -> BearerTokenValidator:
"""Return the cached validator used by this named provider."""
return self._validator

async def authenticate(self, user_id: str | None = None, **kwargs) -> AuthResult:
"""Validate the supplied ``token`` through the authentication-provider interface."""
token = kwargs.get("token")
if not isinstance(token, str) or not token:
raise ValueError("JWT authentication requires a token")

result = await self.verify(token)
if not result.active:
raise ValueError("JWT verification failed")
return AuthResult(raw=result.model_dump())
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Configuration for validating inbound JWT identity credentials."""

from urllib.parse import urlparse

from pydantic import Field
from pydantic import field_validator

from nat.data_models.authentication import AuthProviderBaseConfig


class JwtAuthProviderConfig(AuthProviderBaseConfig, name="jwt"):
"""Named JWT verification policy for inbound identity credentials."""

issuer_url: str = Field(description="Expected JWT issuer claim.")
jwks_uri: str = Field(description="Endpoint containing trusted public keys for signature verification.")
audience: str = Field(description="Expected JWT audience claim.")
scopes: list[str] = Field(default_factory=list, description="Scopes required in a verified JWT.")
timeout: float = Field(default=10.0, gt=0, description="HTTP timeout for JWKS requests.")
leeway: int = Field(default=60, ge=0, description="Clock-skew allowance for JWT time claims, in seconds.")

@field_validator("issuer_url", "jwks_uri")
@classmethod
def require_secure_url(cls, value: str, info) -> str:
Comment thread
ericevans-nv marked this conversation as resolved.
parsed = urlparse(value)
is_local_http = parsed.scheme == "http" and parsed.hostname in {"localhost", "127.0.0.1", "::1"}
if not parsed.netloc or (parsed.scheme != "https" and not is_local_http):
raise ValueError(f"{info.field_name} must use HTTPS (HTTP is allowed only for localhost)")
return value
13 changes: 13 additions & 0 deletions packages/nvidia_nat_core/src/nat/authentication/jwt/register.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from nat.authentication.jwt.jwt_auth_provider_config import JwtAuthProviderConfig
from nat.builder.builder import Builder
from nat.cli.register_workflow import register_auth_provider


@register_auth_provider(config_type=JwtAuthProviderConfig)
async def jwt_auth_provider(config: JwtAuthProviderConfig, builder: Builder):
from nat.authentication.jwt.jwt_auth_provider import JwtAuthProvider

yield JwtAuthProvider(config)
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@

from nat.authentication.api_key import register as register_api_key
from nat.authentication.http_basic_auth import register as register_http_basic_auth
from nat.authentication.jwt import register as register_jwt
from nat.authentication.oauth2 import register as register_oauth2
Loading
Loading