> ## Documentation Index
> Fetch the complete documentation index at: https://docs.veadk.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Inbound authentication

Inbound authentication verifies the identity of requests coming into the agent. VeADK supports API keys and OAuth2.

<Note>
  Direct reuse through `from_veidentity(client_secret=...)`, A2A 1.0 AgentCard parsing, and managed API key resolution are unreleased Preview additions verified at the revision below. They are not included in VeADK 1.1.13. Install this revision in a separate virtual environment when using these additions

  ```bash theme={null}
  pip install "veadk-python @ git+https://github.com/volcengine/veadk-python.git@adcdfdcc6a5a213b249a8caad435b939c01df7f6"
  ```
</Note>

## API key authentication

<Warning>
  URL parameters can appear in browser history, access logs, and forwarded links. Use HTTPS and redact credential-bearing URLs from logs. Successful key validation still requires resource authorization
</Warning>

An API key verifies the caller's identity and authorizes access to API resources with a single string secret. The VeFaaS API gateway deployment described here passes the API key in the URL's `token` parameter. Follow the corresponding API documentation for other services.

<Note>
  API keys apply only to the A2A / MCP Server deployment mode; they are not recommended for the VeADK Web mode, which should use OAuth2 instead.
</Note>

Select API-key auth when scaffolding an agent, or add `--auth-method=api-key` when deploying an existing project. The API gateway then validates the API key carried in the `token` URL parameter on each request.

## OAuth2 single sign-on

OAuth2 is an open authorization framework: instead of exposing account credentials, it grants limited access via tokens, so a single login lets users reach multiple related applications without re-authenticating. VeADK offers two integration paths:

| Path | When to use | Notes |
| - | - | - |
| API gateway | VeFaaS cloud deployment | Deployed via scaffolding; the API gateway handles auth |
| Starlette / FastAPI middleware | Local dev / self-hosted | Integrate the OAuth2 middleware in the app |

### API gateway

For VeADK Web apps deployed on VeFaaS, where the API gateway runs the OAuth2 flow.

<Note>
  The API gateway mode requires API gateway version 4.0.0 or later.
</Note>

Select OAuth2 when scaffolding an agent, or add `--auth-method=oauth2` when deploying an existing project; VeADK creates the Identity user pool and client for you. To reuse existing resources, pass `--user-pool-name` and `--client-name` at deploy time.

After deploying, create users in Agent Identity:

<Steps>
  <Step title="Open the user pool">
    In the Volcengine console, go to Agent Identity, select **Authentication › User Pools**, and choose your pool.
  </Step>

  <Step title="Create a user">
    On the pool's **Users** tab, click **New User**, fill in the details, and confirm.
  </Step>
</Steps>

When a user visits the app, the API gateway guides them through login; afterwards you can read the user's JWT from the `Authorization` header.

### Starlette / FastAPI middleware

For local development or self-hosted deployment, VeADK's middleware handles OAuth2 inside the app and supports any Starlette-based framework (including FastAPI). Prefer `OAuth2Config.from_veidentity()`, which auto-configures a VeIdentity user pool:

Install the example dependencies with `pip install fastapi uvicorn`. Call `setup_oauth2()` once per application; the later fragments are alternative configurations, not additional setup calls

Prepare an existing user pool and Web client, and register `http://localhost:8000/oauth2/callback`. Set `OAUTH2_USER_POOL_NAME` and `OAUTH2_CLIENT_NAME`, and provide cloud credentials allowed to read those resources. This example is for local HTTP development only; use HTTPS and restore `cookie_secure=True` for deployment

```python title="app.py" lines theme={null}
import os
from fastapi import FastAPI
from veadk.auth.middleware.oauth2_auth import OAuth2Config, setup_oauth2

app = FastAPI()

@app.get("/api/profile")
async def profile():
    return {"authenticated": True}

setup_oauth2(
    app,
    OAuth2Config.from_veidentity(
        user_pool_name=os.environ["OAUTH2_USER_POOL_NAME"],
        client_name=os.environ["OAUTH2_CLIENT_NAME"],
        redirect_uri="http://localhost:8000/oauth2/callback",
        auto_create=False,
        auto_register_callback=False,
        cookie_secure=False,
    ),
)
```

Save as `app.py` and run `uvicorn app:app --host 127.0.0.1 --port 8000`. Before login, `/api/profile` should return `401`. Open `/oauth2/login` in a browser, complete login, then request the profile again to receive `authenticated: true`

`from_veidentity()` enables resource creation and callback registration by default; this example disables both. Automatic creation requires the corresponding permissions and a valid callback address. Starlette usage is the same

To reuse existing resources, disable auto-creation:

```python lines theme={null}
setup_oauth2(
    app,
    OAuth2Config.from_veidentity(
        user_pool_name="existing-pool",
        client_name="existing-client",
        redirect_uri="https://myapp.com/oauth2/callback",
        auto_create=False,             # error if the resource doesn't exist
        auto_register_callback=False,  # don't modify the callback URL
    ),
)
```

Local development needs HTTPS cookies disabled:

```python lines theme={null}
setup_oauth2(
    app,
    OAuth2Config.from_veidentity(
        user_pool_name="my-app",
        client_name="my-app-web",
        redirect_uri="http://localhost:8000/oauth2/callback",
        cookie_secure=False,  # local HTTP development
    ),
)
```

To reuse an existing user pool and client by UID, provide both `client_uid` and `client_secret` to skip the client lookup during startup:

```python lines theme={null}
setup_oauth2(
    app,
    OAuth2Config.from_veidentity(
        user_pool_uid="pool-xxxx",
        client_uid="client-xxxx",
        client_secret=os.environ["OAUTH2_CLIENT_SECRET"],
        redirect_uri="https://myapp.com/oauth2/callback",
        auto_create=False,
    ),
)
```

For a provider other than VeIdentity, construct `OAuth2Config` directly. This alternative configuration fragment is for providers that issue JWT access tokens. Set endpoint, issuer, and public-key environment variables from the provider's OpenID Connect metadata, and set `OAUTH2_AUDIENCE` to the audience accepted by this API. Do not assume that any client ID is a valid audience

```python lines theme={null}
setup_oauth2(
    app,
    OAuth2Config(
        authorize_url=os.environ["OAUTH2_AUTHORIZE_URL"],
        token_url=os.environ["OAUTH2_TOKEN_URL"],
        userinfo_url=os.environ["OAUTH2_USERINFO_URL"],
        issuer=os.environ["OAUTH2_ISSUER"],
        jwks_uri=os.environ["OAUTH2_JWKS_URI"],
        audience=os.environ["OAUTH2_AUDIENCE"],
        client_id=os.environ["OAUTH2_CLIENT_ID"],
        client_secret=os.environ["OAUTH2_CLIENT_SECRET"],
        redirect_uri="https://myapp.com/oauth2/callback",
    ),
)
```

Direct construction does not discover `jwks_uri`; JWT validation cannot complete without a public key set. For opaque access tokens, set `use_introspection=True` and `introspection_url`, and supply introspection client credentials required by the provider

The middleware registers these routes automatically:

| Route | Description |
| - | - |
| `/oauth2/login` | Start the OAuth2 login flow |
| `/oauth2/callback` | Handle the OAuth2 callback |
| `/oauth2/logout` | Log out and clear the session |
| `/oauth2/userinfo` | Get the current user's info |

You can exempt paths from auth: `exempt_paths=["/health", "/metrics"]` (exact match) and `exempt_prefixes=["/public/", "/static/"]` (prefix match). The middleware responds by request type: browser requests redirect to the login page; API requests get a `401`. API requests are detected via the `Accept: application/json` header, a path prefix (default `/api/`), or the `X-Requested-With: XMLHttpRequest` header, and can be customized with `api_path_prefixes`.

### Application integration parameters

`setup_oauth2()` returns an `OAuth2Handler` that manages OAuth2 and registers authentication routes and middleware

| Parameter | Type | Default | Description |
| - | - | - | - |
| `app` | `Starlette` | Required | Starlette or FastAPI application |
| `config` | `OAuth2Config` | Required | Authentication configuration |
| `routes` | `OAuth2RoutePaths / None` | `None` | Custom login, callback, logout, and user information paths; defaults to the routes above |
| `exempt_paths` | `Iterable[str] / None` | `None` | Exact paths exempt from authentication |
| `exempt_prefixes` | `Iterable[str] / None` | `None` | Path prefixes exempt from authentication |
| `state_store` | `StateStore / None` | `None` | OAuth state store; defaults to process memory |

After creating `app` and `config`, replace the original `setup_oauth2()` call with this fragment to customize paths. Update both `config.redirect_uri` and the provider registration to match the new callback URL

```python theme={null}
from veadk.auth.middleware.oauth2_auth import OAuth2RoutePaths

setup_oauth2(
    app,
    config,
    routes=OAuth2RoutePaths(
        login="/auth/login",
        callback="/auth/callback",
        logout="/auth/logout",
        userinfo="/auth/userinfo",
    ),
    exempt_paths=["/health"],
    exempt_prefixes=["/public/"],
)
```

### VeIdentity configuration parameters

`OAuth2Config.from_veidentity()` accepts the parameters below. When `session_timeout_seconds` is omitted, it attempts to use the client's configured refresh-token lifetime; if unavailable, the `OAuth2Config` default remains

| Parameter | Type | Default | Description |
| - | - | - | - |
| `user_pool_name` | `str / None` | `None` | User pool name; provide a name or UID |
| `user_pool_uid` | `str / None` | `None` | User pool UID; takes precedence over the name |
| `client_name` | `str / None` | `None` | Client name; provide a name or UID |
| `client_uid` | `str / None` | `None` | Client UID; takes precedence over the name |
| `client_secret` | `str / None` | `None` | Unreleased Preview parameter; skips the client-secret lookup when provided with client\_uid |
| `redirect_uri` | `str` | Required | OAuth2 callback URL |
| `auto_create` | `bool` | `True` | Create missing named user pools and clients; UID-only references do not create resources |
| `auto_register_callback` | `bool` | `True` | Register the callback URL with the client |
| `client_type` | `UserPoolClientType` | `WEB_APPLICATION` | Type of a newly created client |
| `web_origin` | `str / None` | `None` | Web Origin for callback registration; derived from redirect\_uri when unset |
| `scope` | `str` | `"openid profile email"` | Space-separated authorization scopes |
| `identity_client` | `IdentityClient / None` | `None` | Use a supplied Identity client; defaults to global settings or a default client |
| `**extra_config` | `Any` | — | Additional OAuth2Config options such as cookie\_secure, use\_pkce, and session\_timeout\_seconds; do not repeat endpoint options populated by discovery |

### OAuth2Config parameters

#### Endpoints and authorization requests

| Parameter | Type | Default | Description |
| - | - | - | - |
| `authorize_url` | `str` | Required | Authorization endpoint URL |
| `token_url` | `str` | Required | Token endpoint URL |
| `client_id` | `str` | Required | Client ID registered with the provider |
| `client_secret` | `str / None` | `None` | Client secret; optional for public clients |
| `redirect_uri` | `str` | Required | Callback URL matching the registered client value |
| `scope` | `str` | `"openid profile"` | Space-separated authorization scopes |
| `response_type` | `str` | `"code"` | Authorization response type; this integration uses the authorization code flow |
| `extra_authorize_params` | `dict[str, str]` | `{}` | Additional authorization request parameters |
| `extra_token_params` | `dict[str, str]` | `{}` | Additional token request parameters |
| `extra_token_headers` | `dict[str, str]` | `{}` | Additional token request headers |
| `use_pkce` | `bool` | `False` | Enable PKCE when supported by the provider |
| `userinfo_url` | `str / None` | `None` | User information endpoint; not requested when unset |
| `end_session_url` | `str / None` | `None` | Provider logout endpoint |
| `logout_redirect_url` | `str` | `"/"` | Redirect destination after logout |
| `user_id_field` | `str` | `"sub"` | User information field containing the user identifier |
| `user_id_cookie_name` | `str` | `"veadk_user_id"` | Cookie name for the user identifier |

#### Sessions and cookies

| Parameter | Type | Default | Description |
| - | - | - | - |
| `session_cookie_name` | `str` | `"veadk_session"` | Session cookie name |
| `session_timeout_seconds` | `int` | `3600` | Absolute browser session lifetime in seconds |
| `cookie_secure` | `bool` | `True` | Send cookies only over HTTPS |
| `cookie_samesite` | `str` | `"lax"` | Cookie SameSite policy |
| `cookie_domain` | `str / None` | `None` | Cookie domain; defaults to the current host in the browser |
| `cookie_path` | `str` | `"/"` | Cookie path scope |
| `cookie_signing_secret` | `str / None` | `None` | Session signing secret; falls back to client\_secret |
| `auto_refresh_token` | `bool` | `True` | Refresh access tokens automatically when a refresh token is available |
| `token_refresh_threshold_seconds` | `int` | `300` | Seconds before access token expiry at which refresh starts |

#### Access token validation

| Parameter | Type | Default | Description |
| - | - | - | - |
| `issuer` | `str / None` | `None` | Expected token issuer; use provider metadata |
| `jwks_uri` | `str / None` | `None` | Public key set URL required for JWT validation |
| `audience` | `str / list[str] / None` | `None` | Allowed token audiences; no audience restriction when unset |
| `allowed_algorithms` | `list[str]` | `["RS256"]` | Allowed JWT signing algorithms |
| `jwks_cache_ttl_seconds` | `int` | `300` | Public key set cache lifetime in seconds |
| `jwks_kid_miss_cooldown_seconds` | `int` | `30` | Minimum interval in seconds between public key refreshes for unknown key IDs |
| `use_introspection` | `bool` | `False` | Validate access tokens through the provider introspection endpoint |
| `introspection_url` | `str / None` | `None` | Required introspection endpoint URL when introspection is enabled |
| `introspection_client_id` | `str / None` | `None` | Introspection client ID; used only with the introspection secret, otherwise the original client credentials apply |
| `introspection_client_secret` | `str / None` | `None` | Introspection client secret; used only with the introspection client ID, otherwise the original client credentials apply |
| `introspection_cache_ttl_seconds` | `int` | `300` | Maximum introspection cache lifetime in seconds, also bounded by token expiry |
| `introspection_cache_max_entries` | `int` | `1000` | Maximum number of introspection cache entries |

#### Storage and request behavior

| Parameter | Type | Default | Description |
| - | - | - | - |
| `state_ttl_seconds` | `int` | `300` | Default in-memory state lifetime in seconds |
| `state_max_entries` | `int` | `10000` | Maximum entries in the default in-memory state store |
| `http_timeout_seconds` | `float` | `30.0` | OAuth2 HTTP request timeout in seconds |
| `http_max_connections` | `int` | `100` | Maximum OAuth2 HTTP client connections |
| `http_max_keepalive_connections` | `int` | `20` | Maximum OAuth2 HTTP keep-alive connections |
| `api_path_prefixes` | `list[str]` | `["/api/"]` | API path prefixes that return 401 instead of redirecting unauthenticated requests |

<Note>
  When fetching user info from the userinfo endpoint, only fields safe to store in a browser session cookie are retained: `sub`, `email`, `email_verified`, `name`, `given_name`, `family_name`, `preferred_username`, `picture`, `locale`, `updated_at`, plus the field configured via `user_id_field`. Only values of type string, integer, float, or boolean are kept, to prevent the session cookie from exceeding browser size limits. The `/oauth2/userinfo` endpoint returns these filtered fields.
</Note>

### Sharing OAuth state across processes

The default `InMemoryStateStore` is for a single process. All instances of a distributed application need a shared state store that consumes each state only once

This fragment replaces the `setup_oauth2()` call and reuses `app` and `config` from above. Install the `redis` package, prepare Redis 6.2 or later with `GETDEL` support, and set `REDIS_URL`. Use an application-specific key prefix shared by all its instances, and configure production connections and credentials for your deployment

```python theme={null}
import json
import os
import secrets

from redis import Redis

class RedisStateStore:
    def __init__(self, client, ttl_seconds=300):
        self.client = client
        self.ttl_seconds = ttl_seconds
        self.prefix = "oauth2:my-app:"

    def create_state(self, redirect_after_auth="/", code_verifier=None):
        state = secrets.token_urlsafe(32)
        data = json.dumps({
            "redirect_after_auth": redirect_after_auth,
            "code_verifier": code_verifier,
        })
        self.client.setex(self.prefix + state, self.ttl_seconds, data)
        return state

    def validate_and_consume_state(self, state):
        data = self.client.getdel(self.prefix + state)
        return json.loads(data) if data else None

redis_client = Redis.from_url(os.environ["REDIS_URL"], decode_responses=True)
setup_oauth2(
    app,
    config,
    state_store=RedisStateStore(redis_client, ttl_seconds=config.state_ttl_seconds),
)
```

`create_state()` returns a random state and stores the redirect destination and PKCE verifier. `validate_and_consume_state()` atomically reads and deletes the record, returning `None` for an invalid state. A custom store manages its own expiry, capacity, and connection lifecycle; `state_max_entries` does not limit Redis records automatically

## OAuth2 JWT authentication

OAuth2 JWT auth combines the OAuth2 framework with JWT, carrying the authorization token as a JWT. It applies to A2A / MCP Server.

Select OAuth2 when scaffolding an agent, or add `--auth-method=oauth2` when deploying; VeADK creates the Identity user pool, or reuses one via `--user-pool-name`. Then create an M2M client in the pool and exchange its credentials for a JWT:

```bash lines theme={null}
REGION="cn-beijing"
USER_POOL_ID="FILL_IN_YOUR_USER_POOL_ID"
CLIENT_ID="FILL_IN_YOUR_CLIENT_ID"
CLIENT_SECRET="FILL_IN_YOUR_SECRET"

curl --location "https://userpool-${USER_POOL_ID}.userpool.auth.id.${REGION}.volces.com/oauth/token" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --header "Authorization: Basic $(echo -n "${CLIENT_ID}:${CLIENT_SECRET}" | base64)" \
  --data-urlencode "grant_type=client_credentials"
```

When a user calls the app, the API gateway validates the JWT they carry; read it from the `Authorization` header.

## Identity propagation in A2A calls

In a call chain that uses the AgentKit A2A registry, VeADK can pass credentials
from the current request to a downstream agent so that it can authorize the call
under the original user identity and trust context.

<Warning>
  Credentials are sent to the resolved downstream A2A endpoint. Call only trusted downstream agents, and give inbound tokens only the permissions required for the task.
</Warning>

| Inbound credential | Downstream behavior |
| - | - |
| `X-Ve-TIP-Token` | Forwarded to the downstream A2A agent in the same header. |
| `Authorization: Bearer <JWT>` | When the downstream AgentCard declares OAuth2, the Bearer JWT is preferred as the downstream `Authorization` header. Other authentication schemes are not propagated as a user JWT. |

For a downstream agent that declares OAuth2, VeADK first sends the propagated
Bearer JWT. It falls back to an OAuth2 M2M token and retries once only when the
downstream endpoint explicitly returns `401 Unauthorized`. Other status codes
and call failures do not trigger M2M fallback, which prevents application or
service failures from being treated as an expired identity token.

### Managed API Key credential authentication

When the downstream AgentCard declares a credential provider in `capabilities.extensions` (containing `credentialProviderName` and `poolName`), and `security` / `securitySchemes` did not produce a usable authentication header, VeADK queries the identity service for the API Key managed by that credential provider and constructs the authentication header according to the credential metadata returned (header name, prefix, etc.). This mechanism uses Agent Identity to centrally manage API keys, requiring no manual API Key configuration on the VeADK side.

<Note>
  VeADK supports parsing A2A 1.0 AgentCards: when the AgentCard does not provide a top-level `url` field, VeADK resolves the endpoint from `supportedInterfaces` by protocol binding type and version, preferring JSONRPC bindings with protocol version 1.0.
</Note>

### Identity propagation to the skills sandbox

When `execute_skills` calls the skills sandbox, it likewise forwards the
inbound identity credential. VeADK reads the credential stored under the
`inbound_auth` credential key from the credential service and sends it in the
`inbound_auth` request header to the sandbox's A2A endpoint, so the sandbox
workflow runs under the original user identity. If the current request carries
no inbound credential, the header is omitted. See the skill sandbox execution
section in [Code sandboxes](/productions/veadk/preview/en/components/tools/code-sandbox).

## Session and deployment limits

`cookie_signing_secret` signs browser sessions and falls back to `client_secret`. Public clients should set a stable signing secret explicitly. Signing prevents tampering; it is not encryption. Multiple instances need a shared signing secret and OAuth state store. Public callback URLs must exactly match the registered client URL
