A2A authentication
This guide covers how to configure authentication for Agent-to-Agent (A2A) communication. There are two supported authentication methods:
- DataRobot API key — bearer token auth for DataRobot-hosted agents.
- Okta cross-application access (XAA) — two-step token exchange for federated Okta environments (hybrid RFC 8693 / RFC 7523 flow).
Both methods use the authenticated_a2a_client function group on the client
side and the cross_application_access server-side config to publish
requirements in the agent card.
Installation
# With Okta XAA support (adds okta-client-python)
pip install "datarobot-genai[dragent,langgraph,auth]>=0.15.40"
Replace langgraph with crewai or llamaindex depending on the target framework.
Option 1: DataRobot API key authentication
Use this when calling a DataRobot-hosted A2A agent that accepts a standard DataRobot API token.
Environment variables
| Variable | Description |
|---|---|
DATAROBOT_API_TOKEN |
DataRobot API token. |
The token is loaded automatically — no need to put it in workflow.yaml.
workflow.yaml
general:
front_end:
_type: dragent_fastapi
step_adaptor:
mode: 'off'
a2a:
server:
name: My Agent
description: A helpful assistant agent.
function_groups:
remote_agent:
_type: authenticated_a2a_client
url: "https://app.datarobot.com/api/v2/deployments/<deployment-id>/directAccess/a2a/"
auth_provider: datarobot_auth
llms:
datarobot_llm:
_type: datarobot-llm-component
workflow:
_type: langgraph_agent
llm_name: datarobot_llm
description: My agent
tool_names:
- remote_agent
authentication:
datarobot_auth:
_type: datarobot_api_key
How it works
- On each A2A call, the
datarobot_api_keyauth provider injects theDATAROBOT_API_TOKENas anAuthorization: Bearer <token>header. - The remote agent's A2A endpoint validates the token against the DataRobot platform.
This is the simplest setup — no agent card extensions or multi-step flows involved.
Important:
datarobot_api_keyis the default authentication mechanism for DataRobot-hosted agents. Every agent card includessecuritySchemes; agents without explicit OAuth configuration advertise HTTP Bearer auth (bearerAuth) for DataRobot API tokens. When the remote agent card declares OAuth2 viacross_application_access, security-scheme negotiation validates and requires a matching auth provider on the client side. Useokta_cross_app_access(Option 2) for OAuth2-protected agents.
Option 2: Okta cross-application access (XAA)
Use this when calling an agent protected by Okta's federated identity model. The flow obtains a scoped access token through a two-step exchange.
Prerequisites
- An Okta organization with Cross-Application Access enabled.
- A registered AI agent principal in Okta with a private key pair.
- The
authextra installed (pip install "datarobot-genai[auth]").
Environment variables
| Variable | Description |
|---|---|
IDP_AGENT_ID |
Okta AI agent principal ID (used as iss/sub in JWT client assertions). |
IDP_AGENT_PRIVATE_KEY_JWK |
Base64-encoded or raw-JSON private JWK (signs JWT client assertions). |
Both are loaded automatically from env vars, .env, DataRobot Runtime
Parameters, or file_secrets.
Full workflow.yaml example
general:
front_end:
_type: dragent_fastapi
step_adaptor:
mode: 'off'
a2a:
server:
name: Blog Content Writer
description: >-
An AI content writing agent that researches and writes
well-structured blog posts.
skills:
- id: write_blog
name: Write Blog Post
description: Researches and writes a blog post on the given topic.
tags: []
examples:
- Write a blog post about the future of AI in healthcare
- Create an article about sustainable energy trends
# Opt in to enforcing the inbound token's claims (all API routes, not just /a2a).
# When enabled, a token's aud claim must match
# cross_application_access.token_request.audience
oauth_claim_validation: true
# Server-side: advertise XAA requirements in the agent card
cross_application_access:
# Step 1: Token exchange to fetch the ID-JAG (RFC 8693)
token_exchange:
trusted_issuer: "https://your-org.okta.com"
audience: "https://your-org.okta.com/oauth2/ausXXXXXXXXXXXXXXX"
# Step 2: Execute the Final Grant (RFC 7523)
token_request:
token_url: "https://your-org.okta.com/oauth2/ausXXXXXXXXXXXXXXX/v1/token"
audience: "https://app.datarobot.com/<org_id>/<agent_id>"
scopes:
- "blog:write"
# Optional: external identity and URL overrides published on the agent card
external:
id: "my-agent-id" # Emitted as urn:datarobot:agent:identity:external
url: "https://my-agent-id.example.com/a2a/" # Overrides the auto-generated card URL
# Optional: per-agent opt-in for unauthenticated agent-card access
# (also requires platform-level opt-in per cluster)
enable_unauthenticated_well_known_route: true
# Optional: options shaping the card served to unauthenticated callers
redacted_agent_card:
# Advertise skills on the redacted card (has no effect unless
# enable_unauthenticated_well_known_route is also true)
enable_skills: true
# Client-side: call a remote XAA-protected agent
function_groups:
remote_agent:
_type: authenticated_a2a_client
url: "https://app.datarobot.com/api/v2/deployments/<deployment-id>/directAccess/a2a/"
auth_provider: okta_auth
llms:
datarobot_llm:
_type: datarobot-llm-component
workflow:
_type: langgraph_agent
llm_name: datarobot_llm
description: LangGraph planner/writer agent
tool_names:
- remote_agent
authentication:
okta_auth:
_type: okta_cross_app_access
How it works
The XAA flow operates in two steps:
-
Token Exchange (RFC 8693) — The caller's incoming Okta access token (from
x-datarobot-external-access-token) is exchanged for an ID-JAG (Identity Assertion Authorization Grant) via the org-level Authorization Server (token_exchange.trusted_issuer). -
JWT Bearer Grant (RFC 7523) — The ID-JAG is exchanged for a scoped access token at the resource AS token endpoint (
token_request.token_url), granting access to the target agent with the requested scopes.
Both steps authenticate the client using the same method
(private key jwt), signing JWT client assertions
with the private key from IDP_AGENT_PRIVATE_KEY_JWK.
With a central registry peer (registry), request-time agent card lookups try the caller's Okta
token first and fall back to DATAROBOT_API_TOKEN. See
Registry lookup credentials.
Server-side configuration reference: cross_application_access
These fields are declared in the serving agent's workflow.yaml and published
on the A2A agent card.
| Field | Required | Default | Purpose |
|---|---|---|---|
token_exchange.trusted_issuer |
Yes | — | Org-level Authorization Server issuer URL. |
token_exchange.audience |
Yes | — | Resource AS base URL (where ID-JAG is fetched from). |
token_request.token_url |
Yes | — | Token endpoint of the resource AS. |
token_request.audience |
Yes | — | Final resource identifier for the agent. Advertised on the agent card and enforced on inbound requests — see Inbound audience validation. |
token_request.scopes |
No | ["read_data"] |
Scopes the caller must request. |
Note:
grant_typeURNs are injected automatically by the generator — do not set them inworkflow.yaml. The agent card will always containurn:ietf:params:oauth:grant-type:token-exchange(Step 1) andurn:ietf:params:oauth:grant-type:jwt-bearer(Step 2).
Inbound audience validation
The serving agent enforces token_request.audience, not just publishes it. The gateway
validates an incoming JWT and forwards it as-is, so without this a token minted for a
different agent would be accepted and exchanged here. It is an audience check, not an
authentication check: a caller with no IdP token — using a DataRobot API token, Option 1: DataRobot API key authentication
— passes through.
| Condition | Result |
|---|---|
aud contains token_request.audience |
Proceeds |
aud does not |
401 {"detail": "Authorization audience claim validation failed"} |
| Present but not a decodable JWT | 422 {"detail": "Malformed authorization token: ..."} |
| No IdP token | Proceeds |
The token is read from x-datarobot-external-access-token only (bare or Bearer-prefixed).
authorization is never read as an IdP-token carrier — it carries DataRobot's own credentials
instead, some of which are JWTs too.
- Applies to every route, not just
/a2a. - Signature, issuer and expiry are not re-verified — the gateway owns that.
- No route is exempt, agent-card discovery included. Auth there is optional, so an
unauthenticated request still reaches the handler and
enable_unauthenticated_well_known_routedecides whether it sees a redacted card or a generic404; a request that does carry a token is validated first, and one naming another agent gets401instead of a card. - Off unless
a2a.oauth_claim_validation: trueis set. With the flag on but nocross_application_access.token_request.audienceto enforce, startup fails rather than pretending to. Either state is logged at startup. - Audience comparison is exact — no case or trailing-slash leniency.
Server-side configuration reference: external
Optional fields under general.frontend.a2a.external that control additional
identity metadata and the agent card URL.
| Field | Purpose |
|---|---|
external.id |
Catalog discovery identifier. Emitted as the urn:datarobot:agent:identity:external extension on the agent card. |
external.url |
Overrides the auto-generated agent card endpoint URL. Used as-is — no normalization is applied. |
Server-side configuration reference: enable_unauthenticated_well_known_route
Unauthenticated access to GET /.well-known/agent-card.json requires two
independent opt-ins:
- Platform (per cluster) — cluster administrators enable routing of
unauthenticated traffic to agents. This is configured outside
workflow.yaml. - Agent (per deployment) — set
enable_unauthenticated_well_known_route: trueundergeneral.front_end.a2ain the agent'sworkflow.yaml.
Both must be enabled for anonymous callers to reach the agent card endpoint. The agent-side flag is enforced by the A2A server in this library; platform routing is enforced before the request reaches the agent process.
When the agent-side flag is off, an anonymous request gets the same generic
404 {"detail": "Not Found"} as a request for an agent that does not exist —
never a 401, and never a body naming the flag. A distinguishable refusal is an
existence oracle: it would let an anonymous scanner enumerate live agents by
status code alone and tell it which knob unlocks the card. The reason is logged
server-side instead, so the refusal stays debuggable from the agent's own logs.
| Field | Default | Purpose |
|---|---|---|
oauth_claim_validation |
false |
Opt in to enforcing the inbound token's claims — aud today, scope later — on every route, not just /a2a. The value enforced comes from cross_application_access.token_request.audience. Not published on the agent card. |
enable_unauthenticated_well_known_route |
false |
Per-agent developer opt-in. When true, unauthenticated requests that reach the agent receive a redacted agent card. When false, they receive the generic 404 {"detail": "Not Found"} — indistinguishable from a nonexistent agent, so the refusal reveals nothing. Authenticated callers always receive the full card regardless of this setting. |
Server-side configuration reference: redacted_agent_card
Fields under general.front_end.a2a.redacted_agent_card shape the card served
to unauthenticated callers — the reduced view produced by redact_agent_card(),
never the full card. It's a placeholder group: new options that change what the
redacted card contains are added here going forward, rather than as unrelated
top-level a2a.* fields.
| Field | Default | Purpose |
|---|---|---|
enable_skills |
false |
Per-agent developer opt-in to include skills on the redacted card served to unauthenticated callers. Has no effect unless a2a.enable_unauthenticated_well_known_route is also true. When false (default), the redacted card's skills list is empty. When true, it matches the authenticated card's. |
Client-side configuration reference: okta_cross_app_access
These fields configure how the calling agent authenticates when invoking a remote XAA-protected agent.
| Field | Default | Purpose |
|---|---|---|
principal_id |
IDP_AGENT_ID env var |
Okta AI agent principal ID. |
private_jwk |
IDP_AGENT_PRIVATE_KEY_JWK env var |
Base64-encoded or raw-JSON private JWK. |
okta_token_header |
— | Deprecated, no effect. Removed in a future release; delete it. |
fallback_token_headers |
— | Deprecated, no effect. Removed in a future release; delete it. |
The incoming token headers are not configurable. The access token is read from
x-datarobot-external-access-token. That header lives indragent/inbound_token.pyand is shared with inbound audience validation, so the agent cannot exchange a token from a header validation did not inspect.The former
okta_token_headerandfallback_token_headersfields are retired. Setting one to the value it used to default to still loads, with a deprecation warning in the startup log. Any other value — includingfallback_token_headers: [], which used to disable the fallback — is rejected at startup, because honouring it would read a header audience validation does not inspect. To upgrade, remove theokta_token_headerandfallback_token_headerslines from theauthentication:entry in the agent'sworkflow.yaml; nothing replaces them.
Agent card mapping
The cross_application_access configuration is split across two parts of the
published A2A agent card:
securitySchemes.oauth2.flows.clientCredentials— Standard OpenAPI fields:tokenUrl(fromtoken_request.token_url) andscopes(fromtoken_request.scopes).capabilities.extensions[](URI:urn:ietf:params:oauth:grant-type:jwt-bearer) — Non-standard XAA parameters for SDK consumption:token_endpoint_auth_method,token_exchange.*(including hardcodedgrant_typeandrequested_token_typeURNs), andtoken_request.audience.
In addition, the agent card may include up to two identity extensions:
urn:datarobot:agent:identity:internal— Emitted automatically in deployed environments (whenMLOPS_DEPLOYMENT_IDis set). Carriesdeployment_idfor internal DataRobot routing. Not emitted in local development.urn:datarobot:agent:identity:external— Emitted whenexternal.idis set inworkflow.yaml. Carries the developer-supplied catalog identifier.
This separation follows the A2A spec convention: standard OAuth fields belong
in securitySchemes, while flow-specific parameters go in
capabilities.extensions.
Troubleshooting
Common issues
| Symptom | Cause | Fix |
|---|---|---|
Authorization header missing on A2A RPC calls |
The remote agent card declares securitySchemes but the client uses datarobot_api_key. When securitySchemes are present, the A2ACredentialService performs OAuth2 security-scheme negotiation and drops incompatible credentials. |
Switch to an OAuth2-compatible auth provider (e.g. okta_cross_app_access) that matches the security scheme advertised by the remote agent card. |
401 Authorization audience claim validation failed |
The token was issued for a different resource. | Request a token whose aud matches the serving agent's token_request.audience. |
422 Malformed authorization token |
The value in x-datarobot-external-access-token is not a decodable JWT. |
Check what the caller forwards; an opaque token or API key in that header will not decode. |
RuntimeError: No IdP access token in request context |
x-datarobot-external-access-token holds no token. If the message also mentions an ignored okta_token_header override, that is the cause. |
Forward the Okta token in x-datarobot-external-access-token; authorization is never read as an IdP carrier. |
ValueError: principal_id is required |
IDP_AGENT_ID env var not set. |
Set IDP_AGENT_ID in your environment or Runtime Parameters. |
ValueError: Could not parse private_jwk |
IDP_AGENT_PRIVATE_KEY_JWK is neither valid base64-encoded JSON nor raw JSON. |
Verify your JWK — try echo $IDP_AGENT_PRIVATE_KEY_JWK | base64 -d | python -m json.tool. |
ValueError: Agent card ... missing required fields |
Remote agent card doesn't have the XAA extension. | Verify the remote agent has cross_application_access configured. |
RuntimeError: Failed to fetch agent card |
Network/auth issue reaching the agent card URL. | Check the url in the function_groups config and network connectivity. |